文章目录
activemq 简介
JMS中的角色
- Broker
消息服务器,作为server提供消息核心服务
- provider
生产者
消息生产者是由会话创建的一个对象,用于把消息发送到一个目的地。
- Consumer
消费者
消息消费者是由会话创建的一个对象,它用于接收发送到目的地的消息。消息的消费可以采用以下两种方法之一:
- 同步消费。通过调用消费者的receive方法从目的地中显式提取消息。receive方法可以一直阻塞到消息到达。
- 异步消费。客户可以为消费者注册一个消息监听器,以定义在消息到达时所采取的动作。
- p2p
基于点对点的消息模型
消息生产者生产消息发送到 queue 中,然后消息消费者从 queue 中取出并且消费消息。 消息被消费以后,queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消
息。
Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费、其它 的则不能消费此消息了。 当消费者不存在时,消息会一直保存,直到有消费消费
- pub/sub
基于订阅/发布的消息模型
消息生产者(发布)将消息发布到 topic 中,同时有多个消息消费者(订阅)消费该消
息。
和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。 当生产者发布消息,不管是否有消费者。都不会保存消息 一定要先有消息的消费者,后有消息的生产者。
两者对比:
Topic | Queue | |
---|---|---|
模式 | Publish Subscribe messaging 发布 订阅消息 | Point-to-Point 点对点 |
有无状态 | topic 数据默认不落地,是无状态的。 | Queue 数据默认会在 mq 服 务器上以文件形式保存,比如 Active MQ 一 般 保 存 在 $AMQ_HOME\data\kahadb 下 面。也可以配置成 DB 存储。 |
完整性保障 | 并不保证 publisher 发布的每条数 据,Subscriber 都能接受到。 | Queue 保证每条数据都能 被 receiver 接收。消息不超时。 |
消息是否会丢失 | 一般来说 publisher 发布消息到某 一个 topic 时,只有正在监听该 topic 地址的 sub 能够接收到消息;如果没 有 sub 在监听,该 topic 就丢失了。 | Sender 发 送 消 息 到 目 标 Queue, receiver 可以异步接收这 个 Queue 上的消息。Queue 上的 消息如果暂时没有 receiver 来 取,也不会丢失。前提是消息不 超时。 |
消息发布接 收策略 | 一对多的消息发布接收策略,监 听同一个topic地址的多个sub都能收 到 publisher 发送的消息。Sub 接收完 通知 mq 服务器 | 一对一的消息发布接收策 略,一个 sender 发送的消息,只 能有一个 receiver 接收。 receiver 接收完后,通知 mq 服务器已接 收,mq 服务器对 queue 里的消 息采取删除或其他操作。 |
存储
KahaDB存储(默认)
KahaDB是默认的持久化策略,所有消息顺序添加到一个日志文件中,同时另外有一个索引文件记录指向这些日志的存储地址,还有一个事务日志用于消息回复操作。是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。
在data/kahadb这个目录下,会生成四个文件,来完成消息持久化
1.db.data 它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-*.log里面存储的消息
2.db.redo 用来进行消息恢复 *
3.db-.log 存储消息内容。新的数据以APPEND的方式追加到日志文件末尾。属于顺序写入,因此消息存储是比较 快的。默认是32M,达到阀值会自动递增
4.lock文件 锁,写入当前获得kahadb读写权限的broker ,用于在集群环境下的竞争处理
<persistenceAdapter> <!--directory:保存数据的目录;journalMaxFileLength:保存消息的文件大小 --> <kahaDBdirectory="${activemq.data}/kahadb"journalMaxFileLength="16mb"/> </persistenceAdapter>
特性:
1、日志形式存储消息;
2、消息索引以 B-Tree 结构存储,可以快速更新;
3、 完全支持 JMS 事务;
4、支持多种恢复机制kahadb 可以限制每个数据文件的大小。不代表总计数据容量。
AMQ 方式
只适用于 5.3 版本之前。 AMQ 也是一个文件型数据库,消息信息最终是存储在文件中。内存中也会有缓存数据。
性能高于 JDBC,写入消息时,会将消息写入日志文件,由于是顺序追加写,性能很高。
为了提升性能,创建消息主键索引,并且提供缓存机制,进一步提升性能。
每个日志文件的 大小都是有限制的(默认 32m,可自行配置) 。
当超过这个大小,系统会重新建立一个文件。
当所有的消息都消费完成,系统会删除这 个文件或者归档。
主要的缺点是 AMQ Message 会为每一个 Destination 创建一个索引,如果使用了大量的 Queue,索引文件的大小会占用很多磁盘空间。
而且由于索引巨大,一旦 Broker(ActiveMQ 应用实例)崩溃,重建索引的速度会非常 慢。
虽然 AMQ 性能略高于 Kaha DB 方式,但是由于其重建索引时间过长,而且索引文件 占用磁盘空间过大,所以已经不推荐使用。
<persistenceAdapter> <!--directory:保存数据的目录 ;maxFileLength:保存消息的文件大小 --> <amqPersistenceAdapterdirectory="${activemq.data}/amq"maxFileLength="32mb"/> </persistenceAdapter>
JDBC存储
使用JDBC持久化方式,数据库默认会创建3个表,每个表的作用如下:
activemq_msgs:queue和topic的消息都存在这个表中
表字段信息:
container:消息的destination
sub_dest:如果是使用static集群,这个字段会有集群其他系统的信息
client_id:每个订阅者都必须有一个唯一的客户端id用以区分
sub_name:订阅者名称
selector:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性and和or操作
last_acked_id:记录消费过的消息的id。
activemq_acks:存储持久订阅的信息和最后一个持久订阅接收的消息ID
id:自增的数据库主键
container:消息的destination
msgid_prod:消息发送者客户端的主键
msg_seq:是发送消息的顺序,msgid_prod+msg_seq可以组成jms的messageid
expiration:消息的过期时间,存储的是从1970-01-01到现在的毫秒数
msg:消息本体的java序列化对象的二进制数据
priority:优先级,从0-9,数值越大优先级越高
xid:用于存储订阅关系。如果是持久化topic,订阅者和服务器的订阅关系在这个表保存。
activemq_lock:跟kahadb的lock文件类似,在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker。
- 修改配置文件activemq.xml
- Beans中添加
<bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>
<property name="username" value="activemq"/>
<property name="password" value="activemq"/>
<property name="maxActive" value="200"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
- 修改persistenceAdapter
<persistenceAdapter>
<!-- <kahaDB directory="${activemq.data}/kahadb"/> -->
<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true" />
</persistenceAdapter>
LevelDB存储
LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供 了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。 但是在ActiveMQ官网对LevelDB的表述:LevelDB官方建议使用以及不再支持,推荐使用的是KahaDB
Memory 消息存储
顾名思义,基于内存的消息存储,就是消息存储在内存中。persistent=”false”,表示不设置持 久化存储,直接存储到内存中
在broker标签处设置。
JDBC Message store with ActiveMQ Journal
这种方式克服了JDBC Store的不足,JDBC存储每次消息过来,都需要去写库和读库。 ActiveMQ Journal,使用延迟存储数据到数据库,当消息来到时先缓存到文件中,延迟后才写到数据库中。
当消费者的消费速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。 举个例子,生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况 下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上的消息,那么这个时候只需要同步剩余的 10%的消息到DB。 如果消费者的消费速度很慢,这个时候journal文件可以使消息以批量方式写到DB。
协议
协议地址:https://activemq.apache.org/configuring-version-5-transports.html
ActiveMQ支持的client-broker通讯协议有:TCP、NIO、UDP、SSL、Http(s)、VM。
Transmission Control Protocol (TCP)
1:这是默认的Broker配置,TCP的Client监听端口是61616。
2:在网络传输数据前,必须要序列化数据,消息是通过一个叫wire protocol的来序列化成字节流。默认情况下,ActiveMQ把wire protocol叫做OpenWire,它的目的是促使网络上的效率和数据快速交互。
3:TCP连接的URI形式:tcp://hostname:port?key=value&key=value,加粗部分是必须的
4:TCP传输的优点:
(1)TCP协议传输可靠性高,稳定性强
(2)高效性:字节流方式传递,效率很高
(3)有效性、可用性:应用广泛,支持任何平台
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
New I/O API Protocol(NIO)
1:NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的client调用和服务端有更多的负载。
2:适合使用NIO协议的场景:
(1)可能有大量的Client去链接到Broker上一般情况下,大量的Client去链接Broker是被操作系统的线程数所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议
(2)可能对于Broker有一个很迟钝的网络传输NIO比TCP提供更好的性能
3:NIO连接的URI形式:nio://hostname:port?key=value
4:Transport Connector配置示例:
<transportConnectors>
<transportConnector
name="tcp"
uri="tcp://localhost:61616?trace=true" />
<transportConnector
name="nio"
uri="nio://localhost:61618?trace=true" />
</transportConnectors>
上面的配置,示范了一个TCP协议监听61616端口,一个NIO协议监听61618端口
User Datagram Protocol(UDP)
1:UDP和TCP的区别
(1)TCP是一个原始流的传递协议,意味着数据包是有保证的,换句话说,数据包是不会被复制和丢失的。UDP,另一方面,它是不会保证数据包的传递的
(2)TCP也是一个稳定可靠的数据包传递协议,意味着数据在传递的过程中不会被丢失。这样确保了在发送和接收之间能够可靠的传递。相反,UDP仅仅是一个链接协议,所以它没有可靠性之说
2:从上面可以得出:TCP是被用在稳定可靠的场景中使用的;UDP通常用在快速数据传递和不怕数据丢失的场景中,还有ActiveMQ通过防火墙时,只能用UDP
3:UDP连接的URI形式:udp://hostname:port?key=value
4:Transport Connector配置示例:
<transportConnectors>
<transportConnector
name="udp"
uri="udp://localhost:61618?trace=true" />
</transportConnectors>
Secure Sockets Layer Protocol (SSL)
1:连接的URI形式:ssl://hostname:port?key=value
2:Transport Connector配置示例:
<transportConnectors>
<transportConnector name="ssl" uri="ssl://localhost:61617?trace=true"/>
</transportConnectors>
Hypertext Transfer Protocol (HTTP/HTTPS)
1:像web和email等服务需要通过防火墙来访问的,Http可以使用这种场合
2:连接的URI形式:http://hostname:port?key=value或者https://hostname:port?key=value
3:Transport Connector配置示例:
<transportConnectors>
<transportConnector name="http" uri="http://localhost:8080?trace=true" />
</transportConnectors>
VM Protocol(VM)
1、VM transport允许在VM内部通信,从而避免了网络传输的开销。这时候采用的连 接不是socket连接,而是直接的方法调用。
2、第一个创建VM连接的客户会启动一个embed VM broker,接下来所有使用相同的 broker name的VM连接都会使用这个broker。当这个broker上所有的连接都关闭 的时候,这个broker也会自动关闭。
3、连接的URI形式:vm://brokerName?key=value
4、Java中嵌入的方式: vm:broker:(tcp://localhost:6000)?brokerName=embeddedbroker&persistent=fal se , 定义了一个嵌入的broker名称为embededbroker以及配置了一个 tcptransprotconnector在监听端口6000上
5、使用一个加载一个配置文件来启动broker vm://localhost?brokerConfig=xbean:activemq.xml
activemq api
- maven依赖
<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-all -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.11</version>
</dependency>
签收模式
Session.AUTO_ACKNOWLEDGE : 当客户端从receiver或onMessage成功返回时,Session自动签收客户端的这条消息的收条。
Session.CLIENT_ACKNOWLEDGE:客户端通过调用消息(Message)的acknowledge方法签收消息。在这种情况下,签收发生在Session层面:签收一个已经消费的消息会自动地签收这个Session所有已消费的收条。
Session.DUPS_OK_ACKNOWLEDGE:Session不必确保对传送消息的签收,这个模式可能会引起消息的重复,但是降低了Session的开销,所以只有客户端能容忍重复的消息,才可使用。
持久化
JMS 支持以下两种消息提交模式:
- PERSISTENT。指示JMS Provider持久保存消息,以保证消息不会因为JMS Provider的失败而丢失。
- NON_PERSISTENT。不要求JMS Provider持久保存消息。
优先级
可以打乱消费顺序
producer.setPriority
配置文件需要指定使用优先级的目的地:< policyEntry queue=“queue1” prioritizedMessages=“true” />
消息超时/过期
设置了消息超时的消息,消费端在超时后无法在消费到此消息。
producer.setTimeToLive
JMS的消息格式
JMS消息由以下三部分组成的:
- 消息头。
每个消息头字段都有相应的getter和setter方法。- 消息属性。
如果需要除消息头字段以外的值,那么可以使用消息属性。- 消息体。
JMS定义的消息类型有TextMessage、MapMessage、BytesMessage、StreamMessage和ObjectMessage。
事务
session.commit();
session.rollback();
死信
此类消息会进入到
ActiveMQ.DLQ
队列且不会自动清除,称为死信
此处有消息堆积的风险
- 修改死信队列名称
<policyEntry queue="f" prioritizedMessages="true" >
<deadLetterStrategy>
<individualDeadLetterStrategy queuePrefix="DLxxQ." useQueueForQueueMessages="true" />
</deadLetterStrategy>
</policyEntry>
useQueueForQueueMessages: 设置使用队列保存死信,还可以设置useQueueForTopicMessages,使用Topic来保存死信
- 让非持久化的消息也进入死信队列
<individualDeadLetterStrategy queuePrefix="DLxxQ." useQueueForQueueMessages="true" processNonPersistent="true" />
processNonPersistent=“true”
- 过期消息不进死信队列
<individualDeadLetterStrategy processExpired="false" />
- 独占消费者
Queue queue = session.createQueue(“xxoo?consumer.exclusive=true”);
- 生产者
public class Producer {
@Test
public void produce() throws Exception{
//建立工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"tcp://localhost:61616"
);
//创建连接
Connection connection = connectionFactory.createConnection();
//transacted:是否开启事务
//Session.AUTO_ACKNOWLEDGE
//创建session
Session session = connection.createSession(true, Session.SESSION_TRANSACTED);
try{
//找目的地,获取destination
Queue test = session.createQueue("test");
//创建生产者
MessageProducer producer = session.createProducer(test);
//发送TextMessage(延迟10秒发送)
TextMessage message = session.createTextMessage("test textmessage");
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,10000);
producer.send(message);
//发送bytemessage
BytesMessage bytesMessage = session.createBytesMessage();
bytesMessage.writeBytes("bytemessage".getBytes());
producer.send(bytesMessage);
//发送MapMessage
MapMessage mapMessage = session.createMapMessage();
mapMessage.setString("name","张三");
mapMessage.setInt("age",23);
//设置属性,可用于过滤
mapMessage.setIntProperty("age",23);
producer.send(mapMessage);
//发送ObjectMessage
Student student = new Student().setName("tony").setAge(12).setGender("男");
ObjectMessage objectMessage = session.createObjectMessage();
objectMessage.setObject(student);
producer.send(objectMessage);
//提交事务
session.commit();
}catch (Exception e){
e.printStackTrace();
//消息回滚
session.rollback();
}
connection.close();
}
}
- 消费者
public class consumer {
@Test
public void consume() throws Exception{
//建立工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"tcp://localhost:61616"
);
//添加所有包信任
connectionFactory.setTrustAllPackages(true);
//创建连接
Connection connection = connectionFactory.createConnection();
connection.start();
//transacted:是否开启事务
//Session.AUTO_ACKNOWLEDGE
//创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//找目的地,获取destination
Queue test = session.createQueue("test");
//创建消费者
MessageConsumer consumer = session.createConsumer(test);
while (true){
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("textMessage:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}else if (message instanceof BytesMessage){
BytesMessage bm = (BytesMessage)message;
byte[] b = new byte[1024];
int len = -1;
while (true) {
try {
if (!((len = bm.readBytes(b)) != -1)) break;
} catch (JMSException e) {
e.printStackTrace();
}
System.out.println("BytesMessage:"+new String(b, 0, len));
}
}else if (message instanceof MapMessage){
MapMessage mes = (MapMessage) message;
try {
System.out.println("MapMessage name:"+mes.getString("name")+"age:"+mes.getInt("age"));
} catch (JMSException e) {
e.printStackTrace();
}
}else if (message instanceof ObjectMessage){
Student student = null;
try {
student = (Student) ((ActiveMQObjectMessage) message).getObject();
} catch (JMSException e) {
e.printStackTrace();
}
System.out.println("ObjectMessage:"+student);
}
}
});
}
}
}
springboot 集成
spring:
activemq:
broker-url: tcp://localhost:61616
user: admin
password: admin
packages:
trust-all: true
jms:
pub-sub-domain: true
- 配置
@Configuration
@EnableJms
public class ActiveMqConfig {
// topic模式的ListenerContainer
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setPubSubDomain(true);
bean.setConnectionFactory(activeMQConnectionFactory);
return bean;
}
// queue模式的ListenerContainer
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerQueue(ConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setConnectionFactory(activeMQConnectionFactory);
return bean;
}
}
- 生产者
@SpringBootTest
@RunWith(SpringRunner.class)
public class Producer {
@Autowired
private JmsTemplate jmsTemplate;
@Test
public void produce(){
jmsTemplate.convertAndSend(new ActiveMQQueue("springboot"),"测试springboot activemq");
jmsTemplate.convertAndSend(new ActiveMQQueue("createqueue"),"queue-哈哈哈");
jmsTemplate.convertAndSend(new ActiveMQTopic("createtopic"),"topic-啊啊啊啊");
}
@Test
public void produce2(){
jmsTemplate.send(new ActiveMQQueue("springboot"), new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage();
textMessage.setText("textMessage:测试");
return textMessage;
}
});
}
}
- 消费者
@Component
public class Consumer {
@JmsListeners(value = {@JmsListener(destination = "springboot",containerFactory = "jmsListenerContainerQueue"),
@JmsListener(destination = "createqueue",containerFactory = "jmsListenerContainerQueue")})
//@JmsListener(destination = "createtopic")
public void consumer(String message){
System.out.println("message:"+message);
}
}