1 JMS简介
- JMS全称:Java Message Service ,中文:Java消息服务,是Java的一套API标准
- 最初的目的是为了使应用程序能够访问现有的 MOM系统
- MOM:Message Oriented Middleware,即消息中间件,它可以利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成
- 常见MOM系统包括Apache的ActiveMQ、 阿里巴巴的RocketMQ、IBM的MQSeries、Microsoft的MSMQ、BEA的RabbitMQ 等。并非全部的 MOM 系统都遵循JMS规范,即并非所有MOM系统都提供了JMS实现,提供了JMS实现的MOM,又被称为JMSProvider
- JMS与MOM的关系类似JDBC和数据库之间的关系
- 参考配置参考资料:https://www.lan-luo.pw/index.php/2017/05/26/activemq配置中文注释文档/
2 消息中间件的应用场景
- 异步通信:消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它
- 过载保护:不将请求直接发送给服务端,而是由服务端自己来取,这样防止大量请求同时到达服务端,使整个系统崩溃
- 解耦:A和B直接相连时,一旦B死掉,那么A的功能也都不好用了,采用消息中间件解耦,可以保证B死掉,A也能正常发送给队列,当B复活后,又可以继续完成之前队列中的任务
- 消息通讯:客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果
- 顺序保证:在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。
- 数据流处理:分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后进行大数据分析是当前互联网的必备技术,通过消息队列完成此类数据收集是最好的选择
- 扩展性:消息中间件可以很容易横向扩容
3 常用消息队列比较
特性MQ | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
生产者消费者模式 | 支持 | 支持 | 支持 | 支持 |
发布订阅模式 | 支持 | 支持 | 支持 | 支持 |
请求回应模式 | 支持 | 支持 | 不支持 | 不支持 |
Api完备性 | 高 | 高 | 高 | 高 |
多语言支持 | 支持 | 支持 | java | 支持 |
单机吞吐量 | 万级 | 万级 | 万级 | 十万级 |
消息延迟 | 无 | 微秒级 | 毫秒级 | 毫秒级 |
可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 非常高(分布式) |
消息丢失 | 低 | 低 | 理论上不会丢失 | 理论上不会丢失 |
文档的完备性 | 高 | 高 | 高 | 高 |
提供快速入门 | 有 | 有 | 有 | 有 |
社区活跃度 | 高 | 高 | 有 | 高 |
商业支持 | 无 | 无 | 商业云 | 商业云 |
4 JMS中的角色
-
Broker:消息服务器,相当于server,提供消息核心服务
-
Provider:消息生产者,是由会话创建的一个对象,用于把消息发送到一个目的地
-
Consumer:消息消费者,是由会话创建的一个对象,它用于接收发送到目的地的消息
- 消费消息的两种方式:
- 同步消费:调用消费者的receive方法,从目的地中显式提取消息。receive方法可以一直阻塞到消息到达
- 异步消费:客户端可以为消费者注册一个消息监听器,以定义在消息到达时所采取的动作
- 消费消息的两种方式:
-
p2p:基于点对点的消息模型
- 消息生产者生产消息发送到 queue 中,然后消息消费者从 queue 中取出并且消费消息
- 消息被消费以后,queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息
- Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费、其它的则不能消费此消息了
- 当消费者不存在时,消息会一直保存,直到有消费消费
-
pub/sub:基于订阅/发布的消息模型
- 消息生产者将消息发布到topic中,同时有多个消息消费者消费该消息
-
和点对点方式不同,发布到 topic 的消息会被所有订阅者消费
- 当生产者发布消息,不管是否有消费者。都不会保存消息 一定要先有消息的消费者,后有消息的生产者
- 当生产者发布消息,不管是否有消费者。都不会保存消息 一定要先有消息的消费者,后有消息的生产者
-
p2p和pub/sub的简单对比
1 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 里的消 息采取删除或其他操作。 -
Queue:队列存储,常用与点对点消息模型 ,默认只能由唯一的一个消费者处理。一旦处理消息删除
-
Topic:主题存储,用于订阅/发布消息模型主题中的消息,会发送给所有的消费者同时处理。只有在消息可以重复处理的业务场景中可使用。Queue/Topic都是Destination的子接口
-
ConnectionFactory :连接工厂,jms中用它创建连接。连接工厂是客户用来创建连接的对象,例如ActiveMQ提供的ActiveMQConnectionFactory
-
Connection:JMS Connection封装了客户与JMS提供者之间的一个虚拟的连接
-
Destination:消息的目的地。在点对点消息传递域中,目的地被成为队列(Queue),在发布/订阅消息模型中,目的地被成为主题(Topic)
-
Session:JMS Session是生产和消费消息的一个单线程上下文。可以用会话创建消息生产者(Producer)、消息消费者(Consumer)和消息(Message)等。会话提供了一个事务性的上下文,在这个上下文中,一组发送和接收被组合到了一个原子操作中
5 HelloWorld
-
下载ActiveMQ
#windows http://activemq.apache.org/ #mac brew install activemq #查看activemq安装位置 brew list brew list activemq
-
启动ActiveMQ
#windows #实际上调用的就是bin\win64\wrapper.exe,只不过直接调wrapper.exe的话,如果有异常,会直接闪退,而不会打印错误信息 bin/win64/activemq.bat #mac activemq start
-
进入管理界面:http://localhost:8161/
-
修改访问端口
#windows conf/jetty.xml #mac libexec/conf/jetty.xml
-
创建maven项目
- 会自动帮你下载、并引入需要的activemq-all-5.15.12.jar以及源码,如果创建正常项目,需要将E:\Program Files (x86)\apache-activemq-5.15.12\下的该jar包引入
- File–new–Project–Maven Project–选中create a simple project–填写Goup Id(com.mashibing.mq)与Artifact ID(activemq02)
- 导入pom依赖
<dependencies> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-all</artifactId> <version>5.15.13</version> </dependency> </dependencies>
-
Sender
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Sender { public static void main(String[] args) throws Exception { // 1.获取连接工厂,主要连接不是连接到控制台(8161端口) ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://localhost:61616" ); // 2.获取一个向ActiveMQ的连接 Connection connection = connectionFactory.createConnection(); // 3.获取session Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4. 找目的地,获取destination,消费端,也会从这个目的地取消息 Queue queue = session.createQueue("user"); // 5.消息创建者 MessageProducer producer = session.createProducer(queue); // 6.创建消息 for (int i = 0; i < 1000; i++) { TextMessage textMessage = session.createTextMessage("hi: " + i); //向目的地写入消息 producer.send(textMessage); Thread.sleep(3000); } // 6.关闭连接 //如果调用了connectino.start、启用了监听、不关闭连接,那么程序会一直阻塞,不会退出虚拟机 //consumer中,一般来讲,不调用connection.close,因为consumer需要长期等着去消费消息 connection.close(); System.out.println("System exit...."); } }
-
Receiver
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Receiver { public static void main(String[] args) throws Exception { //1. 建立工厂对象 ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory( ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://localhost:61616" ); //2. 从工厂里拿一个连接 Connection connection = activeMQConnectionFactory.createConnection(); //消费者的connection必须start,否则执行失败 connection.start(); //3. 从连接中获取Session(会话) Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); //4. 从会话中获取目的地(Destination)消费者会从这个目的地取消息 Queue queue = session.createQueue("user"); //5. 从会话中创建消息提供者 MessageConsumer consumer = session.createConsumer(queue); //6. 从会话中创建文本消息(也可以创建其它类型的消息体) while (true) { TextMessage receive = (TextMessage) consumer.receive(); System.out.println("TextMessage:" + receive.getText()); } } }
6 Active MQ的安全机制
-
web控制台安全:重启后生效
#conf/jetty.xml中id="securityLoginService"中name为config的property的value所指向的文件,就是登录用户相关的配置文件 conf/jetty-realm.properties
#用户名:密码,角色 admin: admin, admin user: user, user
-
消息安全机制:配置建立连接工厂时的用户和密码
conf/activemq.xml
<!--添加在</shutdownHooks>后,与其平级--> <plugins> <simpleAuthenticationPlugin> <users> <authenticationUser username="admin" password="admin" groups="admins,publishers,consumers"/> <authenticationUser username="publisher" password="publisher" groups="publishers,consumers"/> <authenticationUser username="consumer" password="consumer" groups="consumers"/> <authenticationUser username="guest" password="guest" groups="guests"/> </users> </simpleAuthenticationPlugin> </plugins>
7 消息结构与类型
7.1 结构
- 消息头:其实就是一些常用的属性,集中都放在了消息头中
- Timestamp:超时时间基于该时间计算
- Redelivered:被消费过变为true,未被消费过为false
- Expiration:超时时间
- Reply To:保证消息健壮性
- 消息属性
- 消息体:存放具体消息内容
7.2 类型
7.2.1 TextMessage:文本消息
7.2.2 MapMessage:k-v
-
发送端
MapMessage mapMessage = session.createMapMessage(); mapMessage.setString("name","lucy"); mapMessage.setBoolean("yihun",false); mapMessage.setInt("age", 17); producer.send(mapMessage);
-
接收端
Message message = consumer.receive(); MapMessage mes = (MapMessage) message; System.out.println(mes); System.out.println(mes.getString("name"));
7.2.3 BytesMessage:字节流,一般用于传输小文件、图片
-
发送端
BytesMessage bytesMessage = session.createBytesMessage(); bytesMessage.writeBytes("str".getBytes()); bytesMessage.writeUTF("哈哈");
-
接收端
//方法一 if(message instanceof BytesMessage) { BytesMessage bm = (BytesMessage)message; byte[] b = new byte[1024]; int len = -1; while ((len = bm.readBytes(b)) != -1) { System.out.println(new String(b, 0, len)); } } //方法二:使用ActiveMQ给提供的便捷方法,但要注意读取和写入的顺序,写入是什么顺序,读取时就是什么顺序 bm.readBoolean(); bm.readUTF();
7.2.4 StreamMessage:java原始的数据流
7.2.5 ObjectMessage:序列化的java对象
-
发送端
//必须先将要序列化的对象,添加到信任列表,否则反序列化时,会抛出如下异常 //Exception in thread "main" javax.jms.JMSException: Failed to build body from content. Serializable class not available to broker. Reason: java.lang.ClassNotFoundException: Forbidden class com.mashibing.mq.Girl! This class is not trusted to be serialized as ObjectMessage payload. Please take a look at http://activemq.apache.org/objectmessage.html for more information on how to configure trusted classes. connectionFactory.setTrustedPackages( new ArrayList<String>(Arrays.asList(new String[] { Girl.class.getPackage().getName() }))); Girl girl = new Girl("qiqi",25,398.0); Message message = session.createObjectMessage(girl);
-
接收端
if(message instanceof ActiveMQObjectMessage) { Girl girl = (Girl)((ActiveMQObjectMessage)message).getObject(); System.out.println(girl); System.out.println(girl.getName()); }
8 消息的持久化
-
持久化消息后,即使ActiveMQ宕机,消息也不会消失,消息被消费者消费掉后,数据库中内容才会消失
-
持久化后,MQ接受消息后,还需要将消息写入数据库,会影响效率,所以不建议使用大型数据库,而是推荐使用kahadb这种小型数据库,速度非常快
-
生产环境几乎不可能用mysql或oracle进行消息持久化存储,此处用oracle是为了方便观察消息在数据库中的存储形式
-
JMS中的持久化
//MessageProducer //DeliveryMode.PERSISTENT:持久化消息 //DeliveryMode.NON_PERSISTENT:不持久化消息 producer.setDeliveryMode(DeliveryMode.PERSISTENT);
8.1 使用KahaDB持久化
-
KahaDB是默认的持久化策略,所有消息顺序添加到一个日志文件中,同时另外有一个索引文件记录指向这些日志的存储地址,还有一个事务日志用于消息回复操作。是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化
-
在data/kahadb这个目录下,会生成四个文件,来完成消息持久化
- db.data:消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-*.log里面存储的消息
- db.redo:用来进行消息恢复
- db-.log:存储消息内容。新的数据以APPEND的方式追加到日志文件末尾。属于顺序写入,因此消息存储是比较 快的。默认是32M,达到阀值会自动递增
- lock:锁,写入当前获得kahadb读写权限的broker ,用于在集群环境下的竞争处理
-
配置:activemq.xml
<persistenceAdapter> <!--directory:保存数据的目录;journalMaxFileLength:保存消息的文件大小,是每个数据文件大小,超过后滚动 --> <kahaDBdirectory="${activemq.data}/kahadb"journalMaxFileLength="16mb"/> </persistenceAdapter>
-
特性
-
日志形式存储消息
-
消息索引以 B-Tree 结构存储,可以快速更新
-
完全支持 JMS 事务
-
支持多种恢复机制
8.2 使用JDBC持久化
-
配置:activemq.xml
<!--设置数据源,名为oracle-ds,使用org.apache.commons.dbcp.BasicDataSource来管理连接池--> <bean id="oracle-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="oracle.jdbc.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@192.168.15.110:1521:fcrhost"/> <property name="username" value="c50hst"/> <property name="password" value="c50hst"/> <property name="maxActive" value="200"/> <property name="poolPreparedStatements" value="true"/> </bean> ... <persistenceAdapter> <!--屏蔽之前使用的kahadb进行持久化,并如果数据库中没有表,自动建立表--> <!--<kahaDB directory="${activemq.data}/kahadb"/>--> <jdbcPersistenceAdapter dataSource="#oracle-ds" createTablesOnStartup="true" /> </persistenceAdapter>
-
数据库连接池和数据库jdbc连接,需要依赖如下jar包
-
commons-dbcp-1.4.jar
-
commons-pool-1.6.jar
-
ojdbc6.jar
-
使用JDBC持久化方式,数据库默认会创建3个表
-
activemq_msgs:用于存储消息,Queue和Topic都存储在这个表中
- 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_acks:用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存
- container:消息的destination
- sub_dest:如果是使用static集群,这个字段会有集群其他系统的信息
- client_id:每个订阅者都必须有一个唯一的客户端id用以区分
- sub_name:订阅者名称
- selector:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性and和or操作
- last_acked_id:记录消费过的消息的id。
-
activemq_lock:在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker
-
8.3 JDBC Message store with ActiveMQ Journal方式持久化
- 消息中间件收到消息后,向内存中存储消息同时,往文件里写,然后就返回ack
- 此时有consumer来消费,就会从文件中删除
- 如果指定时间内未被消费,会通过jdbc写入数据库,而且是批量地写和删(不是单条),减少写入数据库中的数据量
9 消息的可靠性
-
消息的成功消费包含三个阶段
- 客户接收消息:
consumer.receive()
- 客户处理消息(消费消息):
System.out.println(message)
- 确认消息:
message.acknowledge()
、session.commit()
等
- 客户接收消息:
-
消息中间件收到确认包后,才会将该消息从消息队列中移除(如果为持久化消息,还会同时从数据库中移除)
-
如果某个session创建的consumer接收到某个消息,那么该消息无法再被其他consumer重复接收,如果第一个客户端始终没确认消息,且最后该客户端对应session断开,那么消息会重新投递给第二个客户端
-
在开启了事务的会话中,当一个事务被提交的时候,确认消息自动发生
-
在非事务性会话中,消息何时被确认取决于创建会话时的签收模式(acknowledgeMode mode),acknowledgement其实就是ack,也就是确认包
//1. Session.AUTO_ACKNOWLEDGE //1. 消费者调用consumer.receive方法成功后 //2. 或消费者中,MessageListener.onMessage方法成功返回后,会话自动确认客户收到的消息 //2. Session.CLIENT_ACKNOWLEDGE //1. 消费者调用message.acknowledge()方法成功后,消息被确认 //2. 确认是在会话层上进行:确认一个被消费的消息将自动确认所有已被会话消费的消息。例如,如果一个消息消费者消费了10个消息,然后确认第5个消息,那么所有10个消息都被确认 //3. Session.DUPS_ACKNOWLEDGE:不需要确认消息 //4. Session.SESSION_TRANSACTED:开启事务后默认使用,事务提交同时消息被确认 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
10 本地事务
-
JMS客户端可以使用本地事务来组合消息的发送和接收
-
可以在创建session时,指定开启事务
-
当事务设置为开启后,应答模式默认只能是Session.SESSION_TRANSACTED,即使设置为其他的,也不会生效,当设置为其他值时,手动调用message.acknowledge(),会和producer.send类似,只要没最终通过session.commit提交事务,就无法通知ActiveMQ该消息被确认
-
开启事务可以避免频繁发送消息造成的网络连接问题,也可以在出现问题时,及时回滚
//1. true:表示开启事务 //如果事务开启,只要不调用session.commit方法,即使调用了producer.send()或message.acknowledge(),消息也不会被发送到消息队列、消息也无法被确认 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
11 消息的优先级
-
可以设置消息优先级,这样就可以不按发送消息的顺序去消费
-
优先级分10个级别,从0(最低)到9(最高)。如果不指定优先级,默认级别是4
-
开启优先级:activemq.xml
<!--为queue1开启消息优先级,xml中如果不设置不会生效--> <policyEntry queue="queue1" prioritizedMessages="true" />
-
设置优先级
//方案一:设置producer发送的所有消息的优先级 producer.setPriority(9); //方案二:设置指定消息的优先级 producer.send(textMessage,DeliveryMode.PERSISTENT,9,1000 * 100);
12 消息超时/过期
-
消息未被消费时,会存放于内存中,长期堆积存在内存装不下的风险,为防止这种情况产生,通常可以设置消息的超时时间
//单位毫秒 producer.setTimeToLive(1000);
-
ActiveMQ会隔一段时间、或在消费端尝试访问某队列中消息时,检查该队列中消息是否超时,如果超时,不会让消费者消费此消息,而是让死信队列消费该消息,同时自动将该消息放入死信队列
12.1 死信队列
-
死信队列保存一些因为业务逻辑处理失败,而导致消息的失败或者说是消息发送过期的消息,有了死信队列能够防止在发送消息和接收消息过程中因为某些异常导致消息丢失
-
对于非持久化消息,系统认为这些消息并不重要,丢不丢失无所谓,所以默认情况下,非持久化消息不会进入死信队列
-
对于不进死信队列的消息,超时后,控制台上会发现该消息被莫名其妙消费
-
死信队列和正常队列功能相同,本质上就是一个默认名为ActiveMQ.DLQ的队列,客户端同样可以从该队列中获取消息
-
修改死信队列名称与设置非持久化消息进入死信队列
<policyEntry queue="user" prioritizedMessages="true" > <deadLetterStrategy> <!--queuePrefix:指定死信队列前缀,也就是将死信队列名设为"DLxxQ.user",useQueueForQueueMessages开启死信队列,默认开启,processNonPersistent:非持久化消息也进入死信队列--> <individualDeadLetterStrategy queuePrefix="DLxxQ." useQueueForQueueMessages="true" processNonPersistent="true" /> </deadLetterStrategy> </policyEntry>
-
关闭死信队列
<individualDeadLetterStrategy processExpired="false" />
13 独占消费者
-
创建Queue时,可以设置其创建出的消费者独占这个Queue中的所有消息
-
也就是说,当这个独占消费者开始消费这个队列中的消息后,只要这个消费者没挂掉,剩下所有消息,必须都由这个消费者来消费, 其他消费者无法消费到该消息
//xxoo为队列名,该语句是创建consumer时用的,而不是producer Queue queue = session.createQueue("xxoo?consumer.exclusive=true"); //同时设置优先级,Broker会根据consumer的优先级来发送消息到较高的优先级的Consumer上,此处优先级和上面消息的优先级概念不同,它是消费者的优先级 Queue queue = session.createQueue("xxoo?consumer.exclusive=true&consumer.priority=10");
-
使用selector
-
可以为消息分组,同时设定consumer只消费某组下的消息
-
可以达到定向分发、消费消息,也就是负载均衡的感觉
-
如果我们已知一个服务处理消息的速度,我们就可以动态的去调整给每个服务器发多少消息
//在producer端,设置消息的属性,其实就是在消息头中,设定了properties={week=xx}
textMessage.setLongProperty("week", i%7);
//在consumer端,可以为consumer设置selector,设置只接收头中week属性值为1的消息,selector本质上就是一个字符串形式的表达式
MessageConsumer consumer = session.createConsumer(queue,"week=1");
//selector里面也可以写and,类似sql的语法
//MessageConsumer consumer = session.createConsumer(queue,"age > 17 and price<200");
14 消息发送原理
producer.send(textMessage)
方法会根据事务是否开启、消息是否持久化,来动态决定选择同步发送消息还是异步发送消息- 同步(阻塞)发送:关闭事务且消息开启持久化时触发,方法会一直阻塞,直到broker返回确认消息。消息不回丢失,但效率低
- 由于为持久化消息,所以将消息都放到数据库中后,broker才会返回确认消息
- 只是阻塞到mq将数据都存放到数据库中,而不是阻塞到consumer处理完消息
- 异步(非阻塞)发送:方法不会阻塞,不需要broker反馈,可能会造成消息丢失,但效率较高
- 调用的是ActiveMQSession中send方法中的this.connection.asyncSendPacket(msg)
- 而同步发送,调用的是其下面的this.connection.syncSendPacket(msg,sendTimeout)
开启事务 | 关闭事务 | |
---|---|---|
持久化 | 异步 | 同步 |
非持久化 | 异步 | 异步 |
-
可以用以下几种方式设置为异步发送
//方法一 ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("admin", "admin", "tcp://localhost:61616?jms.useAsyncSend=true"); //方法二 ((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true); //方法三 ((ActiveMQConnection)connection).setUseAsyncSend(true)
-
设置异步发送时的windowSize
-
用来约束在异步发送时,producer端允许积压的(尚未收到broker返回ACK确认消息)的消息的尺寸,
-
只对异步发送有意义,因为同步发送根本不会积压ACK
-
每次发送消息之后,都将会导致memoryUsage尺寸增加(+message.size),当broker返回producerAck时,memoryUsage尺寸减少producerAck.size,此size表示先前发送消息的大小
-
发送消息时,会检测memoryUsage中是否还有足够空间,如果足够,正常发送,如果不足,将会阻塞
-
可以通过如下2种方式设置
//方法一:会对所有producer生效
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("admin", "admin",
"tcp://localhost:61616?jms.producerWindowSize=1048576");
//方法二:会对使用该目的地的所有producer生效,会覆盖方法一的设置
Queue queue = session.createQueue("user?producer.windowSize=1048576");
15 延迟消息投递
-
消息发送给消息中间件,然后消息中间件决定这个信息多久后才生效
-
需要在配置文件中开启延迟和调度
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}" schedulerSupport="true">
-
编程时设置消息的属性为延迟投递
//其实就是给消息头,加了一对属性,key就是ScheduledMessage.AMQ_SCHEDULED_DELAY对应的字符串,value就是10*1000 message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 10*1000);
-
带间隔的重复发送
long delay = 10 * 1000; long period = 2 * 1000; int repeat = 9; message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay); //间隔时间 message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period); //额外重复几次,第一次不算,repeat必须设置为int,不能设为long,会失效 message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat); createProducer.send(message);
16 Cron表达式指定时间发送消息
-
使用方式
message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
-
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式
- Seconds Minutes Hours DayofMonth Month DayofWeek Year
- Seconds Minutes Hours DayofMonth Month DayofWeek
-
每一个域可出现的字符如下
- Seconds:可出现", - * /"四个字符,有效范围为0-59的整数
- Minutes:可出现", - * /"四个字符,有效范围为0-59的整数
- Hours:可出现", - * /"四个字符,有效范围为0-23的整数
- DayofMonth:可出现", - * / ? L W C"八个字符,有效范围为0-31的整数
- Month:可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEc
- DayofWeek:可出现", - * / ? L C #"四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推
- Year:可出现", - * /"四个字符,有效范围为1970-2099年
-
特殊字符的含义
- *:表示匹配该域的任意值,假如在Minutes域使用* , 即表示每分钟都会触发事件
- ?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和 DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样
- -:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
- /:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次
- ,:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次
- L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发
- W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一 到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份
- LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五
- #:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三
-
cron表达式举例
//每天中午12点触发 "0 0 12 * * ?" //每天上午10:15触发 "0 15 10 ? * *" //每天上午10:15触发 "0 15 10 * * ?" //每天上午10:15触发 "0 15 10 * * ? *" //2005年的每天上午10:15触发 "0 15 10 * * ? 2005" //在每天下午2点到下午2:59期间的每1分钟触发 "0 * 14 * * ?" //在每天下午2点到下午2:55期间的每5分钟触发 "0 0/5 14 * * ?" //在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 "0 0/5 14,18 * * ?" //在每天下午2点到下午2:05期间的每1分钟触发 "0 0-5 14 * * ?" //每年三月的星期三的下午2:10和2:44触发 "0 10,44 14 ? 3 WED" //周一至周五的上午10:15触发 "0 15 10 ? * MON-FRI" //每月15日上午10:15触发 "0 15 10 15 * ?" //每月最后一日的上午10:15触发 "0 15 10 L * ?" //每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6L" //2002年至2005年的每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6L 2002-2005" //每月的第三个星期五上午10:15触发 "0 15 10 ? * 6#3"
17 activemq.xml中的memoryUsage
<!--启动broker时,相当于启动一个java程序,该参数设定最多使用分配给broker的内存空间的百分之多少,来存储消息数据-->
<memoryUsage>
<memoryUsage percentOfJvmHeap="70" />
</memoryUsage>
<!--表示activemq那些持久化消息,可最多使用磁盘空间的大小,如果持久化到mysql中,该参数不生效,持久化到kahadb中生效-->
<!--如果启动时,硬盘只有20g,那么最大空间如果设置为100g,那么此时就算再腾出空间,也只能用到20g-->
<storeUsage>
<storeUsage limit="100 gb" />
</storeUsage>
<!--不持久化的消息,会先写入内存,如果超过了memoryUsage值,就会写入到一块临时空间,该参数用于限制这块临时空间大小-->
<tempUsage>
<tempUsage limit="50 gb" />
</tempUsage>
18 监听器
- 使用for(true)接收消息时
- 如果一条消息特别大,会接收很久,receive方法会一直阻塞
- for循环中的业务处理逻辑,如果需要处理很久,其他消息也同样无法进来
- 无法高并发处理
- 可以使用监听器来异步(非阻塞)接收消息,当收到消息后会回调自定义的onMessage方法对消息进行业务处理,替代手动调用consumer.receive的方式接收消息
consumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
try {
System.out.println("message2:" + ((TextMessage)message).getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
19 springboot整合activemq
-
新建springboot项目,Dependencies选spring web、Spring for Apache ActiveMQ 5
-
额外引入连接池相关pom依赖
<!--用于初始化sessionfactory--> <dependency> <groupId>org.messaginghub</groupId> <artifactId>pooled-jms</artifactId> </dependency> <!--实现jms连接池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
-
yaml配置文件
spring: activemq: broker-url: tcp://localhost:61616 user: admin password: admin pool: enabled: true #连接池最大连接数 max-connections: 5 #空闲的连接过期时间,默认为30秒 idle-timeout: 0 #对象序列化时,信任哪些包下的对象,不信任的对象无法被转化为消息并发送 packages: trust-all: true #springboot整合的activemq,默认不支持pub/sub模式,只支持queue模式,需要配置文件中开启 jms: pub-sub-domain: true
-
ActiveMqConfig:将用于产生监听的JmsListenerContainerFactory注入到spring中,下面配置监听时使用
package com.mashibing.activemq03; import javax.jms.ConnectionFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jms.annotation.EnableJms; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.jms.config.JmsListenerContainerFactory; @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; } }
-
Receiver:配置监听以及监听的回调方法
package com.mashibing.activemq03.service; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Service; @Service public class Receiver { //@JmsListener表示配置下方方法为监听后回调方法(与onMessage方法功能相同),利用jmsListenerContainerTopic获取ConnectionFactory,从而获取消费者,乃至设置监听 //1. 监听名为springboot的topic @JmsListener(destination = "springboot", containerFactory = "jmsListenerContainerTopic") public void receiveStringQueue(String msg) { System.out.println("收到topic消息:" + msg); } //2. 监听名为ooo的queue @JmsListener(destination = "ooo", containerFactory = "jmsListenerContainerQueue") public void receiveStringTopic(String msg) { System.out.println("收到queue消息:" + msg); } }
-
SenderService:发送消息的服务
package com.mashibing.activemq03.service; import java.util.ArrayList; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.Session; import javax.jms.TextMessage; import org.apache.activemq.command.ActiveMQQueue; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jms.JmsProperties.AcknowledgeMode; import org.springframework.jms.core.JmsMessagingTemplate; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.core.MessageCreator; import org.springframework.stereotype.Service; @Service public class SenderService { //JmsMessagingTemplate是对JmsTemplate的进一步封装,使用更加方便,但功能较少 @Autowired private JmsMessagingTemplate jmsMessagingTemplate; @Autowired private JmsTemplate jmsTemplate; public void send(String destination, String msg) { //方案一:使用JmsMessagingTemplate获取ConnectionFactory,从而使用原生API ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory(); try { Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); } catch (JMSException e) { e.printStackTrace(); } //方案二:传入MessageCreator对象,通过重写其createMessage方法来指定消息的一些属性 jmsTemplate.send(destination, new MessageCreator() { @Override public Message createMessage(Session session) throws JMSException { TextMessage textMessage = session.createTextMessage("xxoo"); textMessage.setStringProperty("hehe", "enen"); return textMessage; } }); send2("springboot",null); send3("ooo",null); } public void send2(String destination, String msg) { ArrayList<String> list = new ArrayList<>(); list.add("malaoshi"); list.add("lain"); list.add("zhou"); //方案三:调用convertAndSend方法,该方法可以直接将Object对象以默认转换方式转为一个消息,我们不必关注是Object对象具体如何转为消息,默认发送到名为destination的topic中 jmsMessagingTemplate.convertAndSend(destination, list); } public void send3(String destination, String msg) { ArrayList<String> list = new ArrayList<>(); list.add("malaoshi"); list.add("lain"); list.add("zhou"); //如果想发送到queue中,第一个参数不能是String类型,而应该传入一个ActiveMQQueue对象 jmsMessagingTemplate.convertAndSend(new ActiveMQQueue(destination), list); } }
20 ActiveMQ Artemis
- 新版本的ActiveMQ
- 后台服务器改为使用Netty
- 实现了自己的存储,不再默认使用KahaDB
- 对内部实现逻辑进行了优化,性能更快
- 原来版本的ActiveMQ对外使用的是tcp协议,但进入到broker内部后,会转为ActiveMQ自己实现的OpenWire协议,Artemis中不会再进行协议转换,可以提升多客户端连接时性能
- 优化传输流程:使用异步IO
- 官方文档:http://activemq.apache.org/components/artemis/migration
21 JMSReplyTo
- Reply To为消息头中的一项
- 用于sender确认receiver是否成功消费消息
21.1 应用场景
- sender调用
producer.send
发送消息给mq,mq收到消息后会发送ack确认包给sender - receiver调用
consumer.receive
从mq接收消息,收到消息后,发送ack确认包给mq - sender只知道自己是否正确发送给mq,而并不知道receiver是否已经正确消费了消息
- 因此sender可以在发送消息给mq时,指定消息头中的reply to值为一个特定的目的地,同时监听这个目的地
- 这样receiver消费完消息后,发送"自己已经处理完毕"消息给reply to对应目的地,sender就能收到该条确认消息
21.2 版本特殊性
- ActiveMQ5.10.x 以上版本必须使用JDK1.8才能正常使用
- ActiveMQ5.9.x 及以下版本使用JDK1.7即可正常使用
21.3 API使用
-
sender
public void send(String destination, String msg) throws JMSException { ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory(); Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); //发送到xxoo队列 Queue queue = session.createQueue("xxoo"); MessageProducer messageProducer = session.createProducer(queue); Message message = session.createTextMessage("请接收消息,收到消息后回复"); //设置reply to信息,消息头中存在一下记录Reply To queue://reply message.setJMSReplyTo(new ActiveMQQueue("reply")); messageProducer.send(message); //receiver处理消息后,会向reply to对应的地址发送消息,因此sender去监听reply to对应地址,就能知道receiver是否已经处理完了消息 MessageConsumer messageConsumer = session.createConsumer(new ActiveMQQueue("reply")); messageConsumer.setMessageListener(message1 -> { try { System.out.println("sender 收到消息:" + ((TextMessage) message1).getText()); } catch (JMSException e) { e.printStackTrace(); } }); }
-
receiver
public void receive(String destination, String msg) throws JMSException { ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory(); Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageConsumer messageConsumer = session.createConsumer(new ActiveMQQueue("xxoo")); messageConsumer.setMessageListener(new MessageListener() { @Override public void onMessage(Message message) { try { //获取reply to的地址 System.out.println("receiver 收到消息:" + ((TextMessage) message).getText()); TextMessage textMessage = session.createTextMessage("我已接收到消息"); //处理消息后,通知sender session.createProducer(message.getJMSReplyTo()).send(textMessage); } catch (JMSException e) { e.printStackTrace(); } } }); }
22 JMSCorrelationID
- Correlation ID为消息头中一项
- 用于多条消息间关联,给人一种会话的感觉
22.1 应用场景
- sender发送给broker的消息和receiver发送给reply to的消息不是同一条消息,因此它们的Message ID不同
- 因此sender无法知道reply to中哪条消息与原本自身发送的消息对应
- sender可以在发送消息时,指定唯一的Correlation ID,receiver向reply to发送的消息中,使用该消息原来的Correlation ID,这样sender再收到消息时候,可以指定selector,只接收指定Correlation ID的消息
23 JMSTimestamp
- Timestamp为消息头中一项
- 记录了sender发送消息的时间
23.1 API
-
receiver
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.command.ActiveMQMessage; import javax.jms.*; import java.text.SimpleDateFormat; import java.util.Date; public class Receiver { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory( ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://127.0.0.1:61616" ); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); final Queue queue = session.createQueue("user"); MessageConsumer consumer = session.createConsumer(queue); consumer.setMessageListener(new MessageListener() { public void onMessage(Message message) { try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss SSSS"); //sender发送消息时间 System.out.println(simpleDateFormat.format(new Date(message.getJMSTimestamp()))); //broker接收到消息的时间 System.out.println(simpleDateFormat.format(new Date(((ActiveMQMessage) message).getBrokerInTime()))); //broker推送给receiver的时间 System.out.println(simpleDateFormat.format(new Date(((ActiveMQMessage) message).getBrokerOutTime()))); } catch (JMSException e) { e.printStackTrace(); } } }); } }
23 Request/Response模型实现
- 可以简单理解为点对点的消息传递,sender发送request后,必须收到receiver的response才结束
- 官方推荐的实现:http://activemq.apache.org/how-should-i-implement-request-response-with-jms.html
23.1 QueueRequestor实现
23.1.1 原理
- sender中可以通过QueueRequestor对象的request方法发送消息,该方法会一直阻塞到receiver收到消息并返回确认消息给sender
- request方法内部创建了一个TemporaryQueue类型的temp-queue队列,放入了reply to中,然后发送消息给MQ,最后通过receive方法,阻塞监听temp-queue队列,因此直到receiver收到了这个消息并向temp-queue队列中发送回执,sender才解除阻塞
- request方法违背了mq的异步通讯的本质,使效率降低,但保留了解偶、异构系统的特性
23.1.2 API
-
sender
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Sender { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "nio://192.168.246.128:5671" ); Connection connection = connectionFactory.createConnection(); //注意,由于request方法中,涉及到接收请求,因此此处必须将connection start connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); TextMessage message = session.createTextMessage(); Queue queue = session.createQueue("user"); QueueRequestor queueRequestor = new QueueRequestor((QueueSession) session, queue); System.out.println("===准备发送请求"); TextMessage responseMessage = (TextMessage) queueRequestor.request(message); System.out.println("===请求发送完毕"); System.out.println("responseMessage:=" + responseMessage.getText()); System.out.println("System exit...."); } }
-
Receiver
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Receiver { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory( ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "nio://192.168.246.128:5671" ); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue("user"); MessageConsumer consumer = session.createConsumer(queue); consumer.setMessageListener(new MessageListener() { public void onMessage(Message message) { System.out.println("receiver接收到请求"); try { MessageProducer producer = session.createProducer(message.getJMSReplyTo()); producer.send(session.createTextMessage("==============")); } catch (JMSException e) { e.printStackTrace(); } } }); } }
23.2 TemporaryQueue实现
- QueueRequestor方式是同步的,效率较低,考虑使用异步监听方式实现request/response编程模型,QueueRequestor中其实就是使用TemporaryQueue实现的,只不过是同步的,此处相当于是对QueueRequestor进行改良,将同步改为异步
- sender和receiver第一次通过user队列知道TemporaryQueue地址后,之后就可以一直在这个临时队列上,进行消息传输,类似会话的概念
- Connection消失,TemporaryQueue就跟着消失
23.2.1 API
-
sender
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Sender { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "nio://192.168.246.128:5671" ); Connection connection = connectionFactory.createConnection(); //注意,由于request方法中,涉及到接收请求,因此此处必须将connection start connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue("user"); TextMessage textMessage = session.createTextMessage(); TemporaryQueue temporaryQueue = session.createTemporaryQueue(); textMessage.setJMSReplyTo(temporaryQueue); MessageProducer producer = session.createProducer(queue); System.out.println("====开始发送消息"); producer.send(textMessage); session.createConsumer(temporaryQueue).setMessageListener(new MessageListener() { public void onMessage(Message message) { System.out.println("收到消息"); } }); } }
-
Receiver:同上
23.3 JMSCorrelationID实现
- 使用TemporaryQueue+JMSReplyTo实现response/request编程模型时,每一组response/request就会创建一个TemporaryQueue,而每个TemporaryQueue又会导致新启动一个线程,大量的TemporaryQueue创建会导致内存增加,线程增多,broker性能降低
- 因此可以考虑使用JMSCorrelationID实现,解决大量创建TemporaryQueue问题
23.3.1 API
-
sender
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.util.UUID; public class Sender { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://127.0.0.1:61616" ); Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue("user"); TextMessage textMessage = session.createTextMessage(); String jMSCorrelationID = UUID.randomUUID().toString(); textMessage.setJMSCorrelationID(jMSCorrelationID); //表示这条消息是发送给receiver的,防止Sender内的consumer接收到该消息 textMessage.setStringProperty("type", "R"); MessageProducer producer = session.createProducer(queue); System.out.println("====开始发送消息"); producer.send(textMessage); session.createConsumer(queue, "JMSCorrelationID='" + jMSCorrelationID + "' and type='S'").setMessageListener(new MessageListener() { public void onMessage(Message message) { System.out.println("收到消息"); } }); } }
-
receiver
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Receiver { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory( ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://127.0.0.1:61616" ); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); final Queue queue = session.createQueue("user"); MessageConsumer consumer = session.createConsumer(queue,"type='R'"); consumer.setMessageListener(new MessageListener() { public void onMessage(Message message) { System.out.println("receiver接收到请求"); try { MessageProducer producer = session.createProducer(queue); TextMessage textMessage = session.createTextMessage("=============="); textMessage.setJMSCorrelationID(message.getJMSCorrelationID()); textMessage.setStringProperty("type","S"); producer.send(textMessage); } catch (JMSException e) { e.printStackTrace(); } } }); } }
24 如何防止消息丢失
- 集群(高性能、高可用):可以解决由于单点故障导致的消息丢失
- 死信队列:解决由于消息过期导致的消息丢失
- 持久化:未持久化的消息,broker宕机后,自动丢失
- 消息重投:解决由于网络问题导致的消息丢失
- 记录日志:自己记录日志,用于消息丢失后方便排查原因
- 检查selector、独占消费者:可能由于设置了selector和独占消费者,导致无法正确接收消息
- 检查限流:broker可能设置了限流,导致超过一定限度后不再接收消息
- 使用QueueRequestor同步阻塞消费:损失太大,效率降低太多
25 如何防止重复消费
- 因为网络传输、broker宕机等故障,消息发送给receiver后,receiver的确认信息没有传送到消息队列,导致消息队列不知道该消息已经被消费过,因此再次将该消息分发给其他的消费者
- 幂等方法:指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变
- 其实解决的不是消息重复发送,而是保证客户端程序的幂等性,从而保证多次消费重复消息时,不会引起系统状态错误(例如将消息插入数据库,重复消息导致数据库中插入两条记录)
- 其中一种常见做法,可以将消息唯一的Message ID,存放在Map对象中,如果为并发环境,保存在ConcurrentHashMap中,每次消费消息前,使用Map的putIfAbsent方法判断,如果Map对象中原本有该Message ID,put失败,返回原Map中的值,如果原Map对象中没有该消息,put成功,返回当前Message ID,然后真正开始消费消息
- 但这个Map不会被清理,因此可能会越来越大,最后甚至导致内存溢出,因此可以考虑使用google提供的Guava Cache,设置指定时间后消息过期,这样可以限制缓存无法无限增大
26 Linux下安装
-
官网下载Unix/Linux/Cygwin版本
-
解压
-
修改jetty.xml中ip地址,否则只能通过localhost或127.0.0.1访问
-
在init.d下建立软连接,保证可以以service activemq start方式启动
cd /etc/init.d ln -s /root/apache-activemq-5.16.0/bin/activemq ./
-
设置开机启动
chkconfig activemq on
-
人工为activemq脚本添加JAVA_HOME,否则启动报错
vi /usr/local/activemq/bin/activemq #在最前面两行添加 JAVA_HOME="/opt/mashibing/jdk1.8.0_261" export JAVA_HOME
-
服务管理
service activemq start service activemq status service activemq stop
27 传输协议
-
官方文档http://activemq.apache.org/configuring-version-5-transports
-
客户端使用不同协议连接broker时,broker处理的效率不同。sender和receiver使用不同协议连接broker时,不影响消息的接收
-
在conf/activemq.xml中transportConnectors下进行配置
<transportConnectors> <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB --> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> </transportConnectors>
-
一般都使用openwire协议,该协议是基于tcp协议实现的,默认使用61616端口
-
可以使用NIO进行传输,使用的仍然是tcp协议,只不过底层使用NIO包,提供了异步网络通讯,从而提供了更好的性能。NIO适用于高并发访问MQ的场景
<transportConnector name="nio" uri="nio://0.0.0.0:61616"/>
//客户端使用连接时也应使用nio ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "nio://localhost:61616" );
-
ActiveMQ5.13.0后开始支持AUTO的传输方式,配置为AUTO后,会根据客户端连接方式,自动选择使用OpenWire、STOMP、 AMQP、MQTT四种协议中的一种来进行连接
<transportConnector name="auto" uri="auto://localhost:5671"/>
-
使用AUTO+NIO
<transportConnector name="auto+nio" uri="auto+nio://localhost:5671"/>
//客户端连接配置使用两种之一都可以 "nio://localhost:5671" "tcp://localhost:5671"
28 QueueBrowser
-
可以查看队列中的消息而不消费,类似activemq控制台中的显示功能,无法查看topic中消息,不支持使用listener
-
API使用
QueueBrowser browser = session.createBrowser(new ActiveMQQueue("user")); Enumeration enumeration = browser.getEnumeration(); while(enumeration.hasMoreElements()){ TextMessage message = (TextMessage) enumeration.nextElement(); System.out.println(message.getText()); }
29 使用Hawtio监控ActiveMQ
- Hawtio是用于管理Java应用程序的轻量级、模块化Web控制台
- 官方网站https://hawt.io/
29.1 jar包启动
- 启动hawtio:java -jar hawtio-app-2.10.1.jar
- 登陆:http://localhost:8080/hawtio
- Add connection
- Host:activemq的host–Port:activemq的port–Path:/api/jolokia–Test Connection–Add
- 输入localhos:8080登陆
29.2 war包内嵌在ActiveMQ中
-
将war包放在webapps目录下
-
jetty.xml的bean id="secHandlerCollection"内的list标签下添加如下内容
<bean class="org.eclipse.jetty.webapp.WebAppContext"> <property name="contextPath" value="/hawtio" /> <property name="war" value="${activemq.home}/webapps/hawtio.war" /> <property name="logUrlOnStart" value="true" /> </bean>
-
配置hawtio权限、使用activemq账号密码登陆,windows修改bin/activemq,mac修改bin/env
#windows下 if "%ACTIVEMQ_OPTS%" == "" set ACTIVEMQ_OPTS=-Xms1G -Xmx1G -Dhawtio.realm=activemq -Dhawtio.role=admins -Dhawtio.rolePrincipalClasses=org.apache.activemq.jaas.GroupPrincipal -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config="%ACTIVEMQ_CONF%\login.config" #mac下 ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS_MEMORY -Dhawtio.realm=activemq -Dhawtio.role=admins -Dhawtio.rolePrincipalClasses=org.apache.activemq.jaas.GroupPrincipal -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=$ACTIVEMQ_CONF/login.config"
30 消息发送方案优化
- 异步发送消息时,容易造成消息丢失,因为sender发送了消息后,不再关注broker是否返回ack
- 但如果使用同步发送消息,又会导致效率降低
- 可以考虑将所有场景都配置为异步发送消息,然后在编程时,sender使用ActiveMQMessageProducer的带回调函数的send方法发送消息,既能保证消息不丢失,又能防止同步导致的阻塞,虽然会导致sender效率降低,但通常sender端效率不会称为瓶颈,receiver端消费效率才是瓶颈
30.1 API
-
sender
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQMessageProducer; import org.apache.activemq.AsyncCallback; import javax.jms.Connection; import javax.jms.JMSException; import javax.jms.Queue; import javax.jms.Session; import java.util.concurrent.CountDownLatch; public class Sender { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://127.0.0.1:61616" ); Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue("user"); //需要使用ActiveMQMessageProducer的send方法,才有回调功能 ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(queue); final CountDownLatch countDownLatch = new CountDownLatch(1000); for (int i = 0; i < 1000; i++) { producer.send(session.createTextMessage(i + ""), new AsyncCallback() { public void onException(JMSException exception) { exception.printStackTrace(); } public void onSuccess() { //每次收到broker的ack,都countDown countDownLatch.countDown(); } }); } //当所有消息都被broker确认发送完成,才继续执行 countDownLatch.await(); System.out.println("1000条消息全部发送完成"); } }
31 prefetchSize
-
sender发送消息时,由sender将消息推送给broker
-
receiver获取消息时,如果采用receiver主动从broker获取的方式,效率较低,而如果由broker主动推送给receiver,那么如果一次发送大量消息,却都不确认,这会造成事务上下文变大,broker端这种“半消费状态”的数据变多,因此ActiveMQ提供一个prefetchSize参数以限制broker可以立即分发给单个consumer的最大消息条数,receiver会在与broker建立连接时,告诉broker自身的prefetchSize
- 如果prefetchSize为0,broker不会主动推送消息给receiver,而是由receiver从broker拉取消息
- 如果prefetchSize不为0,broker批量向receiver内存中推送prefetchSize条消息,broker中记录count=prefetchSize,当receiver接收消息时,会从内存中读取之前broker批量推送来的其中一条消息,当receiver返回ack给broker,broker中count=count-1,直到count<=prefetchSize/2,broker重新向consumer批量推送消息
-
当count=prefetchSize,broker处理方式和prefetchSize为0时相同
-
prefetchSize过大会导致消费倾斜:例如receiver消费消息的速度较慢,但prefetchSize设置较大,那么即使配置了多个receiver,broker也会一次性将prefetchSize条消息发送给某个receiver,从而导致消费倾斜。如果为慢速消费的情况,可以考虑将prefetchSize设置为1从而避免消费倾斜
-
prefetchSize造成receiver内存溢出:如果单条消息过大,虽然prefetchSize值较小,但总量较大,这些消息会批量发送给receiver,占用大量receiver内存,最终可能导致receiver内存溢出
-
prefetch默认值
consumer type default value queue 1000 queue browser 500 topic 32766 durable topic 1000 -
设置prefetchSize
//方案一:创建连接时整体设置 ActiveMQConnectionFactory connectio nFactory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://localhost:5671?jms.prefetchPolicy.all=50" ); //方案二:创建连接时对topic和queue单独设置 ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://localhost:5671?jms.prefetchPolicy.queuePrefetch=1&jms.prefetchPolicy.topicPrefetch=1" ); //方案三:针对destination单独设置,会覆盖连接时的设置值 Destination topic = session.createTopic("user?consumer.prefetchSize=10");
32 批量确认
-
ActiveMQ默认开启批量确认消息,批量确认可以提高系统性能,也可以关闭批量确认,有助于人为控制消息确认。配合prefetch一起使用,达到批量获取,批量确认的效果
-
关闭批量确认
//方案一 new ActiveMQConnectionFactory("tcp://locahost:61616?jms.optimizeAcknowledge=false"); //方案二 ((ActiveMQConnectionFactory)connectionFactory).setOptimizeAcknowledge(fase); //方案三 ((ActiveMQConnection)connection).setOptimizeAcknowledge(true);
33 可追溯的Topic
-
默认情况下, sender向topic发送消息后,broker会马上将消息发送给订阅了该topic的receiver,并从broker内删除,如果某个receiver此时不在线,那么就无法收到该条消息。配置可追溯的topic后,broker会保留Topic内一定数量、时间内的消息,即使发送消息时receiver不在线,等receiver上线后仍然能收到消息,从而避免订阅了了topic的receiver错过消息
-
activemq.xml:配置Topic内消息的保留方案
<!-->表示通配符,也就是所有topic都进行如下配置--> <policyEntry topic=">"> <subscriptionRecoveryPolicy> <!--以下配置任选其一--> <!--1. 只在内存中保留1024字节,因为是在内存中保留,所以重启后无法再次推送--> <fixedSizedSubscriptionRecoveryPolicy maximumSize="1024"/> <!--2. 保留固定数量的消息--> <fixedCountSubscriptionRecoveryPolicy maximumSize="100"/> <!--3. 保留60000ms内的消息--> <timedSubscriptionRecoveryPolicy recoverDuration="60000"/> <!--4. 保留最后一条消息--> <lastImageSubscriptionRecoveryPolicy/> </subscriptionRecoveryPolicy> </policyEntry>
-
receiver中设置:配置允许追溯(接收)broker内保存的topic内的消息
topic = session.createTopic("tpk?consumer.retroactive=true"); consumer = session.createConsumer(topic);
34 持久订阅者:Durable Subscriber
-
对于topic,有两种订阅类型,Durable Subscribers和NonDurable Subscribers
-
当broker发送消息给订阅者时,如果持久化订阅者不在线,等上线后依然能收到该消息。但如果非持久化订阅者不在线,那么该消息就再也无法收到
-
sender
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class Sender { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory( ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://192.168.246.128:5671" ); Connection connection = activeMQConnectionFactory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer messageProducer = session.createProducer(session.createTopic("user")); messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); messageProducer.send(session.createTextMessage("11111")); } }
-
receiver
package com.mashibing.mq; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.command.ActiveMQMessage; import javax.jms.*; import java.text.SimpleDateFormat; import java.util.Date; public class Receiver { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory( ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://192.168.246.128:5671" ); Connection connection = activeMQConnectionFactory.createConnection(); //Activemq通过clientID+订阅者名,一同来区分不同订阅者,两个连接如果设置同一个clientID和订阅者名,第二个启动时会报错 connection.setClientID("aaa"); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Topic topic = session.createTopic("user"); //创建一个持久化订阅者,bbb为订阅者名 MessageConsumer consumer = session.createDurableSubscriber(topic,"bbb"); consumer.setMessageListener(new MessageListener() { public void onMessage(Message message) { try { System.out.println(((TextMessage)message).getText()); } catch (JMSException e) { e.printStackTrace(); } } }); } }
35 常见问题
35.1 AUTO_ACKNOWLEDGE造成消息丢失、乱序
- broker推送给receiver消息后,如果receiver返回ack给broker,那么roker会将该条消息彻底删除
- 如果消息的签收模式设置为AUTO_ACKNOWLEDGE,那么当客户端执行
consumer.receive
,或MessageListener的onMessage
方法结束后,会自动发送ack给broker - 对于
consumer.receive
,此时客户端还没有消费该消息(对该消息做处理),broker中就将该消息删除,一旦在真正消费该消息过程中出现问题,就会导致消息丢失。而对于onMessage
方法,我们通常将消费消息的逻辑放入其中,但无论onMessage
成功,或抛出异常,receiver都会发送ack给broker,这也会导致消息丢失 - 因此应尽量配置成CLIENT_ACKNOWLEDGE,人为进行ack
- 但如果配置为CLIENT_ACKNOWLEDGE,如果receiver宕机,会导致receiver一直无法发送ack给broker,直到broker与receiver之间的connection断开,broker才会重新将这些消息发送给其他receiver,在这过程中,可能出现其他后发送的消息都先于这批消息被处理完毕,造成消息消费乱序
35.2 exclusive和selector导致消息堆积
- 如果配置receiver为独占消费者(exclusive)或selector时,会出现某些消息只能由某一个receiver接收,如果receiver消费速度过慢,而sender创建消息较快,那么会导致broker中堆积消息,堆积到一定程度后,sender就无法再发送消息给broker
36 AbortSlowConsumerStrategy:关闭慢速消费者策略
-
当前版本下,有一个策略类AbortSlowConsumerStrategy,允许你配置在一定时间间隔后关闭慢速消费者。AbortSlowConsumerStrategy类通过检查consumer的prefetch buffer满了多久来判断consumer是否为一个慢速消费者。当终止一个慢速consumer后,其内部prefetch buffer中的那些消息会被发送给对应目的地上的其他消费者
-
该策略对prefetch配置为0或1的consumer不生效
-
通过配置对慢速消费者的处理策略,broker可以启动一个后台线程用来检测所有的慢速消费者,并在一定时间间隔后,关闭这些慢消费者
<policyEntry queue=">" producerFlowControl="true" memoryLimit="512mb"> <slowConsumerStrategy> <!--1. 配置了abortSlowConsumerStrategy就表示会中断慢速消费者--> <!--2. 相关属性解释 1. maxTimeSinceLastAck:consumer最近一次ack后多久,才能被标记为慢速消费者,单位ms 2. abortConnection:false表示broker会发送一条消息请求慢速consumer关闭连接,true表示broker自动关闭与慢速consumer的连接 --> <!--abortConnection:中断慢速消费者,但不关闭底层连接--> <abortSlowConsumerStrategy abortConnection="false"/> </slowConsumerStrategy> </policyEntry>
37 PendingMessageLimitStrategy:待发送消息限制策略
-
官网中提到的non-durable topics,经资料查找,并没有这个概念,只有topic的订阅者有持久化和非持久化的概念,此处应该理解为,被持久化订阅者订阅的topic
-
如果某个topic的非持久化订阅者,消费速度过慢,会导致broker内存中消息不被删除(对于Topic而言,一条消息只有所有的订阅者都消费才会被删除),而一旦broker内存被填满,就会导致producer生产速度降低,从而导致原本快速的consumer因为没有新的消息可以消费,导致速度变慢
-
配置
<policyEntry topic="ORDERS.>"> <!--此策略只对topic的非持久订阅者有效--> <pendingMessageLimitStrategy> <!--以下两条不能同时配置--> <!--broker会为consumer保留50条消息,当超过50条没被消费,那么老消息会被清理--> <constantPendingMessageLimitStrategy limit="50"/> <!--保留2.5 * prefetchSize条消息--> <prefetchRatePendingMessageLimitStrategy multiplier="2.5"/> </pendingMessageLimitStrategy> </policyEntry>
38 EIP:Enterprise Integration Patterns
- EIP用于将不同消息中间件进行整合,从而作为消息总线使用,对不同消息中间件,提供了统一的API,让他们可以互相交互
- 常用实现为camel
39 集群配置
39.1 Master Slave集群:解决高可用
-
官方文档:<http://activemq.apache.org/masterslave.html
-
不同场景下集群状态
- 启动时
-
Master宕机
-
Master重启
-
三种类型
- Shared File System Master Slave:使用同一个KahaDB数据文件
- JDBC Master Slave:访问同一个数据库、同一个表
- Replicated LevelDB Store:利用zookeeper实现
-
基本原理:几台broker不会同时启动,他们通过访问同一处文件系统、同一数据库、或zookeeper,来争抢文件系统、数据库、zookeeper中的锁,抢到锁的broker才能启动,其他没抢到锁的broker一直等待,不断尝试获取锁。一旦Master对应的broker宕机,就会释放锁,此时其他broker中,就会有新broker抢到锁,从而启动,变为Master
-
配置步骤
-
复制一份activemq服务端
-
activemq.xml
<!--1. 修改brokerName,一个改为activemq01,一个改为activemq02--> <broker xmlns="http://activemq.apache.org/schema/core" brokerName="activemq01" dataDirectory="${activemq.data}"></broker> <broker xmlns="http://activemq.apache.org/schema/core" brokerName="activemq02" dataDirectory="${activemq.data}"></broker> <!--2. 如果为kahadb,将两个文件中路径配置成一样的--> <!--a. activemq01上配置--> <persistenceAdapter> <kahaDB directory="${activemq.data}/kahadb"/> </persistenceAdapter> <!--a. activemq02上配置--> <persistenceAdapter> <kahaDB directory="/usr/local/activemq/data/kahadb"/> </persistenceAdapter> <!--3. 也可以配置为jdbc,两个broker中的jdbc需要配置成同一个数据源-->
-
39.2 failover故障转移协议
-
官方文档:http://activemq.apache.org/failover-transport-reference.html
-
配置failover后,如果broker与客户端连接断开,client会重新启动一个线程,不断从failover的url参数中获取一个url来重新连接。当配置了Master Slave集群后,如果某个Master宕机,之前的Slave变为了Master,而Slave所在的地址和端口和Master不同,所以可能需要配置failover实现客户端连接自动切换
-
配置语法
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory( "admin", "admin", "failover:(nio://localhost:5671,nio://localhost:5672)" );
39.3 Networks of brokers集群:解决高性能
- 官方文档:http://activemq.apache.org/networks-of-brokers.html
- 基本原理
- Networks of brokers集群中的broker会同时启动
- 所有broker建立连接后,会互相交付订阅信息,包括自身持有的destinations、consumers、持久订阅者等。此后一个broker会把其他broker当做一个消息"订阅者"
- broker上的消息会优先发送给直连自身的consumer
- 如果receiver连接的是broker2,但其订阅的queue或topic在broker1上,那么当接收消息时,broker2会先从broker1将消息拉取到自身,并发送broker1确认的ack,此时broker1将自身该条消息删除。最后broker2再将该消息推送给receiver
- 这种集群对于queue的性能不会有提升,因为对于broker1来说,由broker2拉取消息,和某个receiver拉取消息,其性能开销是相同的。因此真正生产环境如果queue中消息过多,消费客户端过多,就将一个queue拆成两个queue处理,而不是使用Networks of brokers集群
- 但对于topic的情况,例如原本broker1需要与100个客户端建立连接,启动100个线程,并发送消息,此时只需要与其中50个客户端+1个broker2建立连接即可,broker2再与剩下50个客户端建立连接,这样就可以大大提升整体性能
39.3.1 静态网络配置
-
已知所有broker的ip和端口
-
activemq.xml
<!--1. broker下配置networkConnectors,networkConnectors用于配置服务端与服务端之间通讯,之前提到的transportConnectors用于配置服务端与客户端间通讯--> <networkConnectors> <!--name:加入到哪个集群中networkConnector的唯一标识,同一集群中两个broker该值必须相同--> <networkConnector duplex="true" name="amq-cluster" uri="static:failover://(nio://192.168.246.128:5671,nio://192.168.246.128:5672)"/> </networkConnectors> <!--2. 应该将Master Slave集群中kahaDB修改的内容,改回原来的样子--> <kahaDB directory="${activemq.data}/kahadb"/> <!--3. 由于Networks of brokers集群中所有broker都要启动,所以如果两个broker在同一台机器,需要修改客户端连接的端口和broker控制台端口,防止冲突--> <transportConnectors> <transportConnector name="auto+nio" uri="auto+nio://192.168.246.128:5672"/> </transportConnectors>
-
jetty.xml
<!--防止端口冲突,修改broker2配置--> <property name="port" value="8162"/>
-
启动成功后,Connections、Network中都有集群中其他节点信息
-
测试向broker1发送消息,从broker2中能接收到该消息,集群确实搭建成功
39.3.2 动态网络配置
-
可以随时动态调整broker数量,通过udp动态发现集群中有哪些broker。broker启动后会使用udp协议向组播地址发送数据包,以便让其他在这个组播地址中的节点感知自己的存在。udp数据包中主要包括自身ActiveMQ的版本信息、连接到自身所需要使用的主机名、协议名、端口号等
-
官方文档:http://activemq.apache.org/multicast-transport-reference
-
只需在静态网络配置基础上,修改activemq.xml中如下内容
<networkConnectors> <!--使用multicast协议,可以指定组播地址或使用multicast://default,表示使用239.255.2.3--> <networkConnector uri="multicast://239.0.0.5" duplex="false"/> </networkConnectors> <!--指明,udp数据包中,将哪条连接向其他ActiveMQ节点公布--> <transportConnector name="auto+nio" uri="auto+nio://192.168.246.128:5672" discoveryUri="multicast://239.0.0.5"/>
39.4 配置消息回流
-
Networks of brokers集群中,receiver连接broker2,但订阅的是broker1上的queue,那么receiver接收消息时,broker1会将消息转发给broker2后,broker2一旦回复ack,broker1中消息就被清空
-
如果broker2收到了broker1的消息,并ack后,但未发送给receiver之前,broker2宕机,此时由于客户端配置了failover协议,receiver连接到了broker1上,但此时broker1中已经没有了这些消息,如果broker2开启了持久化,那么这些消息在broker2上,即使现在broker2重新启动,由于客户端已经连在了broker1上,也无法收到这些消息
-
因此可以在activemq.xml中进行如下配置
<destinationPolicy> <policyMap> <policyEntries> <policyEntry queue=">" enableAudit="false"> <networkBridgeFilterFactory> <!--1. replayWhenNoConsumers为true,当broker2上有需要转发的消息但是没有被消费时,把消息回流到它原始的broker1上 2. 同时把enableAudit设置为false,防止消息回流后被当作重复消息而不被分发--> <conditionalNetworkBridgeFilterFactory replayWhenNoConsumers="true"/> </networkBridgeFilterFactory> </policyEntry> </policyEntries> </policyMap> </destinationPolicy>
39.5 消息副本
- Master Slave集群没实现数据的高可用,数据还是只有一份,丢失后就找不回来了
- Activemq自身并没提供数据高可用,但提供了一些方案
- 官方文档:http://activemq.apache.org/replicated-message-store