双主双从集群搭建和消息的发送消费示例
1、总体架构介绍
下面我们搭建一个双主双从的集群,并且采用同步的方式来同步主从之间的信息,总体架构如下:
2、集群工作流程
集群工作流程如下:
- 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
- Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
- 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
- Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
- Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
通常情况下,broker 的从服务器很少承担请求处理任务,更多是处于备份和待命状态。从服务器的主要职责是与主服务器进行数据同步,实时复制主服务器上的消息数据,以保证数据的冗余和高可用性。当主服务器出现故障时,从服务器会通过选举机制升级为主服务器,承担起处理所有读写请求的任务。
3、搭建集群
3.1、服务器准备
先准备两台服务器,比如两台虚拟机,可以分别在这两台服务器上放不同的主节点和从节点,但注意同一组的 broker 不要放在同一台服务器上,避免服务器宕机后主从节点一起崩溃。信息如下:
序号 | IP | 角色 | 架构模式 |
---|---|---|---|
1 | 192.168.32.130 | nameserver、brokerserver | Master1、Slave2 |
2 | 192.168.32.131 | nameserver、brokerserver | Master2、Slave1 |
3.2、修改域名映射
修改 hosts 文件:
- vim /etc/hosts
往 hosts 文件中添加以下信息:
- # nameserver
- 192.168.32.130 rocketmq-nameserver1
- 192.168.32.131 rocketmq-nameserver2
- # broker
- 192.168.32.130 rocketmq-master1
- 192.168.32.130 rocketmq-slave2
- 192.168.32.131 rocketmq-master2
- 192.168.32.131 rocketmq-slave1
3.3、关闭防火墙或者开放端口
宿主机需要远程访问虚拟机的rocketmq服务和web服务,需要开放相关的端口号,简单粗暴的方式是直接关闭防火墙
- # 关闭防火墙
- systemctl stop firewalld.service
- # 查看防火墙的状态
- firewall-cmd --state
- # 禁止firewall开机启动
- systemctl disable firewalld.service
或者为了安全,只开放特定的端口号,RocketMQ默认使用3个端口:9876 、10911 、11011 。如果防火墙没有关闭的话,那么防火墙就必须开放这些端口:
nameserver
默认使用 9876 端口master
默认使用 10911 端口slave
默认使用11011 端口
执行以下命令:
- # 开放name server默认端口
- firewall-cmd --remove-port=9876/tcp --permanent
- # 开放master默认端口
- firewall-cmd --remove-port=10911/tcp --permanent
- # 开放slave默认端口 (当前集群模式可不开启)
- firewall-cmd --remove-port=11011/tcp --permanent
- # 重启防火墙
- firewall-cmd --reload
3.4、配置环境变量
修改 profile 文件:
- vim /etc/profile
直接在该文件最后添加以下配置即可:
- # set rocketmq 注意:下面的ROCKETMQ_HOME指定的路径应该是你服务器上rocketmq实际安装的路径
- ROCKETMQ_HOME=/usr/local/rocketmq/rocketmq-all-4.4.0-bin-release
- PATH=$PATH:$ROCKETMQ_HOME/bin
- export ROCKETMQ_HOME PATH
修改后执行命令重新加载配置,使得配置立刻生效:
- source /etc/profile
3.5、创建消息存储路径
- mkdir /usr/local/rocketmq/store
- mkdir /usr/local/rocketmq/store/commitlog
- mkdir /usr/local/rocketmq/store/consumequeue
- mkdir /usr/local/rocketmq/store/index
因为同一主机上启动多个broker时,store路径要不同,所以需要额外另外一个 store1 目录给从 broker 使用:
- mkdir /usr/local/rocketmq/store1
- mkdir /usr/local/rocketmq/store1/commitlog
- mkdir /usr/local/rocketmq/store1/consumequeue
- mkdir /usr/local/rocketmq/store1/index
3.6、修改broker配置文件
在安装目录下的 conf 目录下,我们可以看到以下目录结构:
因为我们是采用双主双从,同步更新的集群,所以修改 2m-2s-sync 下的配置文件。
- master1
服务器:192.168.32.130 下:
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a.properties
修改配置如下:
- # 所属集群名字
- brokerClusterName=rocketmq-cluster
- # broker名字,注意此处不同的配置文件填写的不一样
- brokerName=broker-a
- # 0 表示 Master,>0 表示 Slave
- brokerId=0
- # nameServer地址,分号分割
- namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
- # 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
- defaultTopicQueueNums=4
- # 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
- autoCreateTopicEnable=true
- # 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
- autoCreateSubscriptionGroup=true
- # Broker 对外服务的监听端口
- listenPort=10911
- # 删除文件时间点,默认凌晨 4点
- deleteWhen=04
- # 文件保留时间,默认 48 小时
- fileReservedTime=120
- # commitLog每个文件的大小默认1G
- mapedFileSizeCommitLog=1073741824
- # ConsumeQueue每个文件默认存30W条,根据业务情况调整
- mapedFileSizeConsumeQueue=300000
- # destroyMapedFileIntervalForcibly=120000
- # redeleteHangedFileInterval=120000
- # 检测物理文件磁盘空间
- diskMaxUsedSpaceRatio=88
- # 存储路径
- storePathRootDir=/usr/local/rocketmq/store
- # commitLog 存储路径
- storePathCommitLog=/usr/local/rocketmq/store/commitlog
- # 消费队列存储路径存储路径
- storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
- # 消息索引存储路径
- storePathIndex=/usr/local/rocketmq/store/index
- # checkpoint 文件存储路径
- storeCheckpoint=/usr/local/rocketmq/store/checkpoint
- # abort 文件存储路径
- abortFile=/usr/local/rocketmq/store/abort
- # 限制的消息大小
- maxMessageSize=65536
- # flushCommitLogLeastPages=4
- # flushConsumeQueueLeastPages=2
- # flushCommitLogThoroughInterval=10000
- # flushConsumeQueueThoroughInterval=60000
- # Broker 的角色
- # - ASYNC_MASTER 异步复制Master
- # - SYNC_MASTER 同步双写Master
- # - SLAVE
- brokerRole=SYNC_MASTER
- # 刷盘方式
- # - ASYNC_FLUSH 异步刷盘
- # - SYNC_FLUSH 同步刷盘
- flushDiskType=SYNC_FLUSH
- # checkTransactionMessageEnable=false
- # 发消息线程池数量
- # sendMessageThreadPoolNums=128
- # 拉消息线程池数量
- # pullMessageThreadPoolNums=128
- slave2
服务器:192.168.32.130 下:
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b-s.properties
修改配置如下:
- # 所属集群名字
- brokerClusterName=rocketmq-cluster
- # broker名字,注意此处不同的配置文件填写的不一样
- brokerName=broker-b
- # 0 表示 Master,>0 表示 Slave
- brokerId=1
- # nameServer地址,分号分割
- namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
- # 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
- defaultTopicQueueNums=4
- # 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
- autoCreateTopicEnable=true
- # 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
- autoCreateSubscriptionGroup=true
- # Broker 对外服务的监听端口
- listenPort=11011
- # 删除文件时间点,默认凌晨 4点
- deleteWhen=04
- # 文件保留时间,默认 48 小时
- fileReservedTime=120
- # commitLog每个文件的大小默认1G
- mapedFileSizeCommitLog=1073741824
- # ConsumeQueue每个文件默认存30W条,根据业务情况调整
- mapedFileSizeConsumeQueue=300000
- # destroyMapedFileIntervalForcibly=120000
- # redeleteHangedFileInterval=120000
- # 检测物理文件磁盘空间
- diskMaxUsedSpaceRatio=88
- # 存储路径
- storePathRootDir=/usr/local/rocketmq/store1
- # commitLog 存储路径
- storePathCommitLog=/usr/local/rocketmq/store1/commitlog
- # 消费队列存储路径存储路径
- storePathConsumeQueue=/usr/local/rocketmq/store1/consumequeue
- # 消息索引存储路径
- storePathIndex=/usr/local/rocketmq/store1/index
- # checkpoint 文件存储路径
- storeCheckpoint=/usr/local/rocketmq/store1/checkpoint
- # abort 文件存储路径
- abortFile=/usr/local/rocketmq/store1/abort
- # 限制的消息大小
- maxMessageSize=65536
- # flushCommitLogLeastPages=4
- # flushConsumeQueueLeastPages=2
- # flushCommitLogThoroughInterval=10000
- # flushConsumeQueueThoroughInterval=60000
- # Broker 的角色
- # - ASYNC_MASTER 异步复制Master
- # - SYNC_MASTER 同步双写Master
- # - SLAVE
- brokerRole=SLAVE
- # 刷盘方式
- # - ASYNC_FLUSH 异步刷盘
- # - SYNC_FLUSH 同步刷盘
- flushDiskType=ASYNC_FLUSH
- # checkTransactionMessageEnable=false
- # 发消息线程池数量
- # sendMessageThreadPoolNums=128
- # 拉消息线程池数量
- # pullMessageThreadPoolNums=128
- master2
服务器:192.168.32.131
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b.properties
修改配置如下:
- # 所属集群名字
- brokerClusterName=rocketmq-cluster
- # broker名字,注意此处不同的配置文件填写的不一样
- brokerName=broker-b
- # 0 表示 Master,>0 表示 Slave
- brokerId=0
- # nameServer地址,分号分割
- namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
- # 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
- defaultTopicQueueNums=4
- # 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
- autoCreateTopicEnable=true
- # 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
- autoCreateSubscriptionGroup=true
- # Broker 对外服务的监听端口
- listenPort=10911
- # 删除文件时间点,默认凌晨 4点
- deleteWhen=04
- # 文件保留时间,默认 48 小时
- fileReservedTime=120
- # commitLog每个文件的大小默认1G
- mapedFileSizeCommitLog=1073741824
- # ConsumeQueue每个文件默认存30W条,根据业务情况调整
- mapedFileSizeConsumeQueue=300000
- # destroyMapedFileIntervalForcibly=120000
- # redeleteHangedFileInterval=120000
- # 检测物理文件磁盘空间
- diskMaxUsedSpaceRatio=88
- # 存储路径
- storePathRootDir=/usr/local/rocketmq/store
- # commitLog 存储路径
- storePathCommitLog=/usr/local/rocketmq/store/commitlog
- # 消费队列存储路径存储路径
- storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
- # 消息索引存储路径
- storePathIndex=/usr/local/rocketmq/store/index
- # checkpoint 文件存储路径
- storeCheckpoint=/usr/local/rocketmq/store/checkpoint
- # abort 文件存储路径
- abortFile=/usr/local/rocketmq/store/abort
- # 限制的消息大小
- maxMessageSize=65536
- # flushCommitLogLeastPages=4
- # flushConsumeQueueLeastPages=2
- # flushCommitLogThoroughInterval=10000
- # flushConsumeQueueThoroughInterval=60000
- # Broker 的角色
- # - ASYNC_MASTER 异步复制Master
- # - SYNC_MASTER 同步双写Master
- # - SLAVE
- brokerRole=SYNC_MASTER
- # 刷盘方式
- # - ASYNC_FLUSH 异步刷盘
- # - SYNC_FLUSH 同步刷盘
- flushDiskType=SYNC_FLUSH
- # checkTransactionMessageEnable=false
- # 发消息线程池数量
- # sendMessageThreadPoolNums=128
- # 拉消息线程池数量
- # pullMessageThreadPoolNums=128
-
slave1
服务器:192.168.32.131:
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a-s.properties
修改配置如下:
- # 所属集群名字
- brokerClusterName=rocketmq-cluster
- # broker名字,注意此处不同的配置文件填写的不一样
- brokerName=broker-a
- # 0 表示 Master,>0 表示 Slave
- brokerId=1
- # nameServer地址,分号分割
- namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
- # 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
- defaultTopicQueueNums=4
- # 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
- autoCreateTopicEnable=true
- # 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
- autoCreateSubscriptionGroup=true
- # Broker 对外服务的监听端口
- listenPort=11011
- # 删除文件时间点,默认凌晨 4点
- deleteWhen=04
- # 文件保留时间,默认 48 小时
- fileReservedTime=120
- # commitLog每个文件的大小默认1G
- mapedFileSizeCommitLog=1073741824
- # ConsumeQueue每个文件默认存30W条,根据业务情况调整
- mapedFileSizeConsumeQueue=300000
- # destroyMapedFileIntervalForcibly=120000
- # redeleteHangedFileInterval=120000
- # 检测物理文件磁盘空间
- diskMaxUsedSpaceRatio=88
- # 存储路径
- storePathRootDir=/usr/local/rocketmq/store1
- # commitLog 存储路径
- storePathCommitLog=/usr/local/rocketmq/store1/commitlog
- # 消费队列存储路径存储路径
- storePathConsumeQueue=/usr/local/rocketmq/store1/consumequeue
- # 消息索引存储路径
- storePathIndex=/usr/local/rocketmq/store1/index
- # checkpoint 文件存储路径
- storeCheckpoint=/usr/local/rocketmq/store1/checkpoint
- # abort 文件存储路径
- abortFile=/usr/local/rocketmq/store1/abort
- # 限制的消息大小
- maxMessageSize=65536
- # flushCommitLogLeastPages=4
- # flushConsumeQueueLeastPages=2
- # flushCommitLogThoroughInterval=10000
- # flushConsumeQueueThoroughInterval=60000
- # Broker 的角色
- # - ASYNC_MASTER 异步复制Master
- # - SYNC_MASTER 同步双写Master
- # - SLAVE
- brokerRole=SLAVE
- # 刷盘方式
- # - ASYNC_FLUSH 异步刷盘
- # - SYNC_FLUSH 同步刷盘
- flushDiskType=ASYNC_FLUSH
- # checkTransactionMessageEnable=false
- # 发消息线程池数量
- # sendMessageThreadPoolNums=128
- # 拉消息线程池数量
- # pullMessageThreadPoolNums=128
3.7、修改启动脚本文件
根据内存大小对JVM参数进行适当的调整:
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin/runbroker.sh
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin/runserver.sh
参考修改如下:
- JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
3.8、启动服务
先关闭 rocketmq,然后再重新启动集群:
- # 关闭NameServer
- sh mqshutdown namesrv
- # 关闭Broker
- sh mqshutdown broker
- 启动nameserve集群
分别在192.168.32.130和192.168.32.131启动NameServer:
- # 由于已经配置了环境变量,所以可以直接执行以下命令,不然需要在 bin 目录下执行
- nohup sh mqnamesrv &
- 启动broker集群
在192.168.32.130上启动master1和slave2:
- # 启动master1:
- nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a.properties &
- # 启动slave2:
- nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b-s.properties &
在192.168.32.131上启动master2和slave1:
- # 启动master2:
- nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b.properties &
- # 启动slave1:
- nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a-s.properties &
启动后可通过JPS查看启动进程
4、发送消息
新建一个 maven 工程,导入MQ客户端依赖(MQ 集群相当于是服务器端,往集群发送消息也就相当于是客户端):
- <dependency>
- <groupId>org.apache.rocketmq</groupId>
- <artifactId>rocketmq-client</artifactId>
- <version>4.4.0</version>
- </dependency>
消息发送者步骤分析:
- 创建消息生产者producer,并制定生产者组名
- 指定Nameserver地址
- 启动producer
- 创建消息对象,指定主题Topic、Tag(消息标签)和消息体
- 发送消息
- 关闭生产者producer
消息消费者步骤分析:
- 创建消费者Consumer,制定消费者组名
- 指定Nameserver地址
- 订阅主题Topic和Tag
- 设置回调函数,处理消息
- 启动消费者consumer
在发送消息时,需指定主题 Topic、Tag 和消息体,消费者就可以根据主题和 Tag 来订阅消息,以此来接收到指定主题和 Tag 的消息。
(Tag 标签的作用:消费者订阅了某个Topic后,消息队列RocketMQ版会将该Topic中的所有消息投递给消费端进行消费。若消费者只需要关注部分消息,可通过设置过滤条件在消息队列RocketMQ版服务端完成消息过滤,只消费需要关注的消息。)
4.1、发送同步消息
同步消息指的是向 broker 发送消息后会一直等待 broker 服务器返回发送结果才会执行下一步的程序,否则程序进程一直会等待结果返回。这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
- /**
- * 发送同步消息
- */
- public class SyncProducer {
- public static void main(String[] args) throws Exception {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- for (int i = 0; i < 10; i++) {
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg = new Message("base", "Tag1", ("同步消息Hello World" + i).getBytes());
- //5.发送消息
- SendResult result = producer.send(msg);
- //发送状态
- SendStatus status = result.getSendStatus();
- System.out.println("发送结果:" + result);
- //线程睡1秒
- TimeUnit.SECONDS.sleep(1);
- }
- //6.关闭生产者producer
- producer.shutdown();
- }
- }
4.2、发送异步消息
异步消息指的是向 broker 发送消息后,producer发送消息线程不阻塞。发送异步消息时可以指定消息发送成功和发送异常的回调方法,这些回调任务在消息发送返回后会在一个新的线程中执行 。
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
- /**
- * 发送异步消息
- */
- public class AsyncProducer {
- public static void main(String[] args) throws Exception {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- for (int i = 0; i < 10; i++) {
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg = new Message("base", "Tag2", ("异步消息Hello World" + i).getBytes());
- //5.发送异步消息
- producer.send(msg, new SendCallback() {
- /**
- * 发送成功回调函数
- * @param sendResult
- */
- public void onSuccess(SendResult sendResult) {
- System.out.println("发送结果:" + sendResult);
- }
- /**
- * 发送失败回调函数
- * @param e
- */
- public void onException(Throwable e) {
- System.out.println("发送异常:" + e);
- }
- });
- //线程睡1秒
- TimeUnit.SECONDS.sleep(1);
- }
- //6.关闭生产者producer
- producer.shutdown();
- }
- }
4.3、发送单向消息
发送单向消息时,broker 服务器不会有返回结果,producer 客户端也无需等待broker 服务器的结果 。
这种方式主要用在不特别关心发送结果的场景,例如日志发送。
- /**
- * 发送单向消息
- */
- public class OneWayProducer {
- public static void main(String[] args) throws Exception, MQBrokerException {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- for (int i = 0; i < 3; i++) {
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg = new Message("base", "Tag3", ("单向消息Hello World" + i).getBytes());
- //5.发送单向消息。没有任何返回结果
- producer.sendOneway(msg);
- //线程睡1秒
- TimeUnit.SECONDS.sleep(1);
- }
- //6.关闭生产者producer
- producer.shutdown();
- }
- }
5、接收消息
可以通过 DefaultMQPushConsumer 类来创建消费者,以此接收消息:
- /**
- * 消息的接受者
- */
- public class Consumer {
- public static void main(String[] args) throws Exception {
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag。下面的*表示监听base的所有Tag
- consumer.subscribe("base", "*");
- //可指定消费模式:负载均衡|广播模式。默认为负载均衡模式
- //MessageModel.CLUSTERING-负载均衡模式 MessageModel.BROADCASTING-广播模式
- //consumer.setMessageModel(MessageModel.BROADCASTING);
- //4.设置回调函数,处理消息
- consumer.registerMessageListener(new MessageListenerConcurrently() {
- //接受消息内容
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
- for (MessageExt msg : msgs) {
- System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody()));
- }
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- });
- //5.启动消费者consumer
- consumer.start();
- }
- }
消费者启动后不会自动退出,由此一旦生产者有发送消息,消费者即可接收到。即使在消费者开启之前已经有消息发送,消费者也可接收到这些消息。
5.1、负载均衡消费模式(集群模式)
消费者消费消息有负载均衡和广播模式,默认为负载均衡消费模式。
Consumer 在拉取消息之前需要对 Topic 的 Message 进行负载操作,简单来说就是将Topic下的MessageQueue分配给这些Consumer,至于怎么分,就是通过这些负载策略定义的算法规则来划分。
RocketMQ 默认使用的负载均衡策略为平均负载策略。如果某个Consumer集群,订阅了相同的某个Topic,Topic下面的这些MessageQueue会被平均分配给集群中的所有Consumer中。
如下图:
示例:
比如上面的消费者方法我们手动设置为负载均衡模式:
- //指定消费模式为负载均衡(可以不用指定也行,默认就是负载均衡模式)
- consumer.setMessageModel(MessageModel.CLUSTERING);
然后同时启动两个相同的消费者 main 进程,然后发送消息,结果如下:
可以看到,两个进程消费了相同数量的消息,并且平均分配。
5.2、广播模式
广播消费,类似于ActiveMQ中的发布订阅模式,每条消息都将会对一个Consumer Group下的各个Consumer实例都消费一遍,每个消费者消费的消息都是一样多的。
示例:
将消费者方法手动设置为广播模式:
- //指定消费模式为广播模式
- consumer.setMessageModel(MessageModel.BROADCASTING);
然后同时启动两个相同的消费者 main 进程,然后发送消息,结果如下:
可以看到,两个进程对每条消息都进行了消费。
6、顺序消息
默认情况下,消费者接收消息时,并不能保证接收到的消息顺序的。通过发送顺序消息,可以保证消费者接收的消息有序。消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。
顺序消费的原理解析:在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列),而消费消息时会从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果发送到多个queue,但是同一系列的消息发送到同一个 queue,则能保证分区有序,即相对每个queue,消息都是有序的。
下面我们用订单进行分区有序的示例,即保证在隶属于同一个订单时,消费者接收到的消息是有序的。假设一个订单的顺序流程是:创建、付款、推送、完成。则我们控制订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
先创建一个订单实体类:
- /**
- * 订单构建者
- */
- public class OrderStep {
- private long orderId;
- private String desc;
- public long getOrderId() {
- return orderId;
- }
- public void setOrderId(long orderId) {
- this.orderId = orderId;
- }
- public String getDesc() {
- return desc;
- }
- public void setDesc(String desc) {
- this.desc = desc;
- }
- @Override
- public String toString() {
- return "OrderStep{" +
- "orderId=" + orderId +
- ", desc='" + desc + '\'' +
- '}';
- }
- //该方法可用于生成订单列表
- public static List<OrderStep> buildOrders() {
- // 1039L : 创建 付款 推送 完成
- // 1065L : 创建 付款
- // 7235L :创建 付款
- List<OrderStep> orderList = new ArrayList<OrderStep>();
- OrderStep orderDemo = new OrderStep();
- orderDemo.setOrderId(1039L);
- orderDemo.setDesc("创建");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(1065L);
- orderDemo.setDesc("创建");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(1039L);
- orderDemo.setDesc("付款");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(7235L);
- orderDemo.setDesc("创建");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(1065L);
- orderDemo.setDesc("付款");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(7235L);
- orderDemo.setDesc("付款");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(1065L);
- orderDemo.setDesc("完成");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(1039L);
- orderDemo.setDesc("推送");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(7235L);
- orderDemo.setDesc("完成");
- orderList.add(orderDemo);
- orderDemo = new OrderStep();
- orderDemo.setOrderId(1039L);
- orderDemo.setDesc("完成");
- orderList.add(orderDemo);
- return orderList;
- }
- }
生产者发送消息:
- public class Producer {
- public static void main(String[] args) throws Exception {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- //构建消息集合
- List<OrderStep> orderSteps = OrderStep.buildOrders();
- //发送消息
- for (int i = 0; i < orderSteps.size(); i++) {
- String body = orderSteps.get(i) + ""; //消息体
- Message message = new Message("OrderTopic", "Order", "i" + i, body.getBytes());
- /**
- * 发送消息
- * 参数一:消息对象
- * 参数二:消息队列的选择器
- * 参数三:选择队列的业务标识(订单ID)
- */
- SendResult sendResult = producer.send(message, new MessageQueueSelector() {
- /**
- * 该方法用于控制哪些消息被放在同一队列中
- * @param mqs:队列集合
- * @param msg:消息对象
- * @param arg:业务标识的参数
- * @return
- */
- @Override
- public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
- long orderId = (long) arg;
- long index = orderId % mqs.size();
- return mqs.get((int) index); //最终返回一个消息队列,消息队列一致的消息会放在同一队列中
- }
- }, orderSteps.get(i).getOrderId());
- System.out.println("发送结果:" + sendResult);
- }
- producer.shutdown();
- }
- }
消费者消费消息:
- public class Consumer {
- public static void main(String[] args) throws MQClientException {
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag
- consumer.subscribe("OrderTopic", "*");
- //4.注册消息监听器。接收顺序消息时,参数为MessageListenerOrderly实例
- consumer.registerMessageListener(new MessageListenerOrderly() {
- @Override
- public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
- for (MessageExt msg : msgs) {
- System.out.println("线程名称:【" + Thread.currentThread().getName() + "】:" + new String(msg.getBody()));
- }
- return ConsumeOrderlyStatus.SUCCESS;
- }
- });
- //5.启动消费者
- consumer.start();
- System.out.println("消费者启动");
- }
- }
先执行生产者发送消息,然后执行消费者接收消息,运行结果如下:
可以看到,虽然不是全局有序,但是是分区有序的,即同一订单的内容是有序的。
7、延时消息
rocketmq提供一种延时消息的解决方案,就是在特定的时间到了,消息才会被投递出去供consumer
消费。broker 在接收到延迟消息的时候会把对应延迟级别的消息先存储到对应的延迟队列中,等延迟消息时间到达时,会把消息重新存储到对应的 topic 的 queue 里面,以供消费者使用。
生产者:
- public class Producer {
- public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- for (int i = 0; i < 10; i++) {
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg = new Message("DelayTopic", "Tag1", ("Hello World" + i).getBytes());
- //设定延迟时间
- msg.setDelayTimeLevel(2);
- //5.发送消息
- SendResult result = producer.send(msg);
- //发送状态
- SendStatus status = result.getSendStatus();
- System.out.println("发送结果:" + result);
- //线程睡1秒
- TimeUnit.SECONDS.sleep(1);
- }
- //6.关闭生产者producer
- producer.shutdown();
- }
- }
消费者:
- public class Consumer {
- public static void main(String[] args) throws Exception {
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag
- consumer.subscribe("DelayTopic", "*");
- //4.设置回调函数,处理消息
- consumer.registerMessageListener(new MessageListenerConcurrently() {
- //接受消息内容
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
- for (MessageExt msg : msgs) {
- System.out.println("消息ID:【" + msg.getMsgId() + "】,延迟时间:" + (System.currentTimeMillis() - msg.getStoreTimestamp()));
- }
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- });
- //5.启动消费者consumer
- consumer.start();
- System.out.println("消费者启动");
- }
- }
先启动消费者,然后启动生产者发送消息,你会发现,消息发送完成后,消费者并不能立即收到消息,而是需等待延迟时间后才能接收到消息。
执行结果如下,可以看到,延迟时间可能并不一定是设定的 2 秒,这是因为可能一些网络延迟等原因导致,属正常现象。
但是现在RocketMq并不支持任意时间的延时,只支持设置一些固定的延时等级,从1s到2h分别对应着等级1到18。如下:
- // org/apache/rocketmq/store/config/MessageStoreConfig.java
- private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
8、批量消息
上面我们发送消息实际上都是循环一条一条地去发送的,通过批量发送消息可以显著提高传递消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB。
批量消息实际上就是一次性发送多条消息,代码跟一条条地发送差别不大,只不过是发送的是集合而已。
生产者:
- public class Producer {
- public static void main(String[] args) throws Exception {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- List<Message> msgs = new ArrayList<Message>();
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg1 = new Message("BatchTopic", "Tag1", ("Hello World" + 1).getBytes());
- Message msg2 = new Message("BatchTopic", "Tag1", ("Hello World" + 2).getBytes());
- Message msg3 = new Message("BatchTopic", "Tag1", ("Hello World" + 3).getBytes());
- msgs.add(msg1);
- msgs.add(msg2);
- msgs.add(msg3);
- //5.发送消息
- SendResult result = producer.send(msgs);
- //发送状态
- SendStatus status = result.getSendStatus();
- System.out.println("发送结果:" + result);
- //线程睡1秒
- TimeUnit.SECONDS.sleep(1);
- //6.关闭生产者producer
- producer.shutdown();
- }
- }
消费者:
- public class Consumer {
- public static void main(String[] args) throws Exception {
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag
- consumer.subscribe("BatchTopic", "*");
- //4.设置回调函数,处理消息
- consumer.registerMessageListener(new MessageListenerConcurrently() {
- //接受消息内容
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
- for (MessageExt msg : msgs) {
- System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody()));
- }
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- });
- //5.启动消费者consumer
- consumer.start();
- System.out.println("消费者启动");
- }
- }
执行结果:
使用批量发送消息时,一批消息的总大小不应超过4MB,如果消息的总长度可能大于4MB时,这时候最好把消息进行分割。
可使用下面提供的类来实现消息分割:
- public class ListSplitter implements Iterator<List<Message>> {
- private final int SIZE_LIMIT = 1024 * 1024 * 4;
- private final List<Message> messages;
- private int currIndex;
- public ListSplitter(List<Message> messages) {
- this.messages = messages;
- }
- @Override
- public boolean hasNext() {
- return currIndex < messages.size();
- }
- @Override
- public List<Message> next() {
- int nextIndex = currIndex;
- int totalSize = 0;
- for (; nextIndex < messages.size(); nextIndex++) {
- Message message = messages.get(nextIndex);
- int tmpSize = message.getTopic().length() + message.getBody().length;
- Map<String, String> properties = message.getProperties();
- for (Map.Entry<String, String> entry : properties.entrySet()) {
- tmpSize += entry.getKey().length() + entry.getValue().length();
- }
- tmpSize = tmpSize + 20; // 增加日志的开销20字节
- if (tmpSize > SIZE_LIMIT) {
- //单个消息超过了最大的限制
- //忽略,否则会阻塞分裂的进程
- if (nextIndex - currIndex == 0) {
- //假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
- nextIndex++;
- }
- break;
- }
- if (tmpSize + totalSize > SIZE_LIMIT) {
- break;
- } else {
- totalSize += tmpSize;
- }
- }
- List<Message> subList = messages.subList(currIndex, nextIndex);
- currIndex = nextIndex;
- return subList;
- }
- }
通过实现上面的 ListSplitter 类,每次遍历获取一个容量大小不超过 4M 的集合,通过发送该集合来实现消息分割:
- //把大的消息分裂成若干个小的消息
- ListSplitter splitter = new ListSplitter(messages);
- while (splitter.hasNext()) {
- try {
- List<Message> listItem = splitter.next();
- producer.send(listItem);
- } catch (Exception e) {
- e.printStackTrace();
- //处理error
- }
- }
9、消息过滤
在消费者消费消息时,可以指定一些条件来过滤消息,rocketmq 支持通过 tag 和 SQL 来过滤消息。
9.1、通过 TAG 过滤消息
每一条消息在发送前都可以指定一个主题 topic 和标签 tag,在消费者订阅了指定的主题消费消息时,还可以通过指定 TAG 来指定只消费该 topic 下的某一些 TAG 的消息。
Tag 的作用:消费者订阅了某个Topic后,消息队列RocketMQ版会将该Topic中的所有消息投递给消费端进行消费。若消费者只需要关注部分消息,可通过设置过滤条件在消息队列RocketMQ版服务端完成消息过滤,只消费需要关注的消息。
使用示例:
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag。多个 tag 可以用 || 符号分割开,也可以通过 * 表示该主题下是所有 tag
- consumer.subscribe("FilterTagTopic", "Tag1 || Tag2 ");
9.2、通过 SQL 过滤消息
通过 TAG 可以实现过滤消息,但是一个消息只能有一个标签,这可能难以满足一些复杂的场景。此时,我们可以使用SQL表达式来筛选消息。在发送消息时,我们可以通过 putUserProperty 方法来给消息添加属性并设置,然后在消费时就可以根据这些属性来写一些 SQL 来过滤符合条件的消息。
使用示例:
生产者:
- public class Producer {
- public static void main(String[] args) throws Exception {
- //1.创建消息生产者producer,并制定生产者组名
- DefaultMQProducer producer = new DefaultMQProducer("group1");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.启动producer
- producer.start();
- for (int i = 0; i < 10; i++) {
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg = new Message("FilterSQLTopic", "Tag1", ("Hello World" + i).getBytes());
- msg.putUserProperty("i", String.valueOf(i));
- //5.发送消息
- SendResult result = producer.send(msg);
- //发送状态
- SendStatus status = result.getSendStatus();
- System.out.println("发送结果:" + result);
- //线程睡1秒
- TimeUnit.SECONDS.sleep(2);
- }
- //6.关闭生产者producer
- producer.shutdown();
- }
- }
消费者:
- public class Consumer {
- public static void main(String[] args) throws Exception {
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag
- consumer.subscribe("FilterSQLTopic", MessageSelector.bySql("i>5"));
- //4.设置回调函数,处理消息
- consumer.registerMessageListener(new MessageListenerConcurrently() {
- //接受消息内容
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
- for (MessageExt msg : msgs) {
- System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody()));
- }
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- });
- //5.启动消费者consumer
- consumer.start();
- System.out.println("消费者启动");
- }
- }
执行结果:
可以看到,虽然发送了 10 条数据,但是消费者只消费了符合 SQL 条件的其中 4 条数据。
9.2.1、rocketmq支持的SQL语法
RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
- 数值比较,比如:>,>=,<,<=,BETWEEN,=;
- 字符比较,比如:=,<>,IN;
- IS NULL 或者 IS NOT NULL;
- 逻辑符号 AND,OR,NOT;
常量支持类型为:
- 数值,比如:123,3.1415;
- 字符,比如:'abc',必须用单引号包裹起来;
- NULL,特殊的常量
- 布尔值,TRUE 或 FALSE
只有使用push模式的消费者才能用使用SQL92标准的sql语句,接口如下:
- public void subscribe(finalString topic, final MessageSelector messageSelector)
9.2.2、解决使用SQL过滤报错
在使用 SQL 过滤启动消费者时,可能会提示:The broker does not support consumer to filter message by SQL92 的错误,此时需修改 rocketmq 安装目录下的 conf/broker.conf 文件,添加 enablePropertyFilter=true 配置。
在两台服务器上分别修改该配置文件并重启 nameserver 和 broker 集群,在 rocketmq-console 上可以看到集群中 enablePropertyFilter 为 true 即说明配置成功,或者直接启动消费者不报错即可。
如果仍然不行,则分别需要修改 192.168.32.130 下的 conf/2m-2s-sync/broker-a.properties 和 conf/2m-2s-sync/broker-b-s.properties 配置文件,还有 192.168.32.131 下的 conf/2m-2s-sync/broker-b.properties 和 conf/2m-2s-sync/broker-a-s.properties 配置文件,分别添加 enablePropertyFilter=true 配置,然后重启 namerserver 和 broker 集群即可。
192.168.32.130 下:
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a.properties
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b-s.properties
192.168.32.131 下:
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b.properties
- vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a-s.properties
10、事务消息
10.1、事务消息背景
MQ组件是系统架构里必不可少的一门利器,设计层面可以降低系统耦合度,高并发场景又可以起到削峰填谷的作用,从单体应用到集群部署方案,再到现在的微服务架构,MQ凭借其优秀的性能和高可靠性,得到了广泛的认可。
随着数据量增多,系统压力变大,开始出现这种现象:数据库已经更新了,但消息没发出来,或者消息先发了,但后来数据库更新失败了,结果研发童鞋各种数据修复,这种生产问题出现的概率不大,但让人很郁闷。这个其实就是数据库事务与MQ消息的一致性问题,简单来讲,数据库的事务跟普通MQ消息发送无法直接绑定与数据库事务绑定在一起,例如上面提及的两种问题场景:
- 数据库事务提交后发送MQ消息;
- MQ消息先发,然后再提交数据库事务。
场景1的问题是数据库事务可能刚刚提交,服务器就宕机了,MQ消息没发出去,场景2的问题就是MQ消息发送出去了,但数据库事务提交失败,又没办法追加已经发出去的MQ消息,结果导致数据没更新,下游已经收到消息,最终事务出现不一致的情况。
10.2、事务消息的引出
我们以微服务架构的购物场景为例,参照一下RocketMQ官方的例子,用户A发起订单,支付100块钱操作完成后,能得到100积分,账户服务和会员服务是两个独立的微服务模块,有各自的数据库,按照上文提及的问题可能性,将会出现这些情况:
- 如果先扣款,再发消息,可能钱刚扣完,宕机了,消息没发出去,结果积分没增加。
- 如果先发消息,再扣款,可能积分增加了,但钱没扣掉,白送了100积分给人家。
- 钱正常扣了,消息也发送成功了,但会员服务实例消费消息出现问题,结果积分没增加。
由此引出的是数据库事务与MQ消息的事务一致性问题,rocketmq事务消息解决的问题:解决本地事务执行与消息发送的原子性问题。这里界限一定要明白,是确保MQ生产端正确无误地将消息发送出来,没有多发,也不会漏发。但至于发送后消费端有没有正常的消费掉(如上面提及的第三种情况,钱正常扣了,消息也发了,但下游消费出问题导致积分不对),这种异常场景将由MQ消息消费失败重试机制来保证,不在此次的讨论范围内。
常用的MQ组件针对此场景都有自己的实现方案,如ActiveMQ使用AMQP协议(二阶提交方式)保证消息正确发送,这里我们以RocketMQ为重点进行学习。
10.3、RocketMQ事务消息设计思路
根据CAP理论,RocketMQ事务消息通过异步确保方式,保证事务的最终一致性。设计流程上借鉴两阶段提交理论,流程图如下:
流程:
- 应用模块遇到要发送事务消息的场景时,先发送消息给MQ(half 消息)
- 服务端返回消息的写入结果
- 根据服务器端返回的写入结果执行数据库事务(本地事务)(如果写入失败,此时 half 消息对业务不可见,本地逻辑也不会执行)
- 根据本地事务执行的结果,再返回 Commit 或 Rollback 给 MQ 服务器
- 如果是Commit,MQ 会把消息下发给 Consumer 端;如果是Rollback,MQ会直接删掉该消息
- 图中第3步即本地事务执行后如果没响应给 MQ 服务器端,或是超时的,启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息),处理结果同图中第4步。
MQ消费的成功机制由 MQ 自己保证。
可参考:https://www.cnblogs.com/huangying2124/p/11702761.html#top
事务补偿:对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起“回查, Producer收到回查消息,检查回查消息对应的本地事务的状态,根据本地事务状态,重新Commit或者Rollback。补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
10.4、事务消息状态
事务消息共有三种状态,提交状态、回滚状态、中间状态:
- TransactionStatus.CommitTransaction:提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown:中间状态,它代表需要检查消息队列来确定状态。
10.5、代码实现
生产者:
- /**
- * 发送同步消息
- */
- public class Producer {
- public static void main(String[] args) throws Exception {
- //1.创建消息生产者producer,并制定生产者组名。注意,这里使用的是TransactionMQProducer来创建
- TransactionMQProducer producer = new TransactionMQProducer("group5");
- //2.指定Nameserver地址
- producer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //添加事务监听器。在事务监听器里面执行本地事务,执行完后再确认是否要发送commit或者rollback给MQ服务器
- producer.setTransactionListener(new TransactionListener() {
- /**
- * 在该方法中执行本地事务
- * @param msg
- * @param arg
- * @return
- */
- @Override
- public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
- if (StringUtils.equals("TAGA", msg.getTags())) {
- return LocalTransactionState.COMMIT_MESSAGE; //commit
- } else if (StringUtils.equals("TAGB", msg.getTags())) {
- return LocalTransactionState.ROLLBACK_MESSAGE; //rollback
- } else if (StringUtils.equals("TAGC", msg.getTags())) {
- return LocalTransactionState.UNKNOW; //什么都不做
- }
- return LocalTransactionState.UNKNOW;
- }
- /**
- * 该方法是MQ回查事务状态时的处理
- * (上面在TAGC消息的处理中我们都没做,所以这里实际上会匹配到TAGC的消息)
- * @param msg
- * @return
- */
- @Override
- public LocalTransactionState checkLocalTransaction(MessageExt msg) {
- System.out.println("消息的Tag:" + msg.getTags());
- return LocalTransactionState.COMMIT_MESSAGE; //最后commit
- }
- });
- //3.启动producer
- producer.start();
- String[] tags = {"TAGA", "TAGB", "TAGC"};
- for (int i = 0; i < 3; i++) {
- //4.创建消息对象,指定主题Topic、Tag和消息体
- /**
- * 参数一:消息主题Topic
- * 参数二:消息Tag
- * 参数三:消息内容
- */
- Message msg = new Message("TransactionTopic", tags[i], ("Hello World" + i).getBytes());
- //5.发送消息。这里会先发送,发送完成后才会执行事务监听器
- SendResult result = producer.sendMessageInTransaction(msg, null);
- //发送状态
- SendStatus status = result.getSendStatus();
- System.out.println("发送结果:" + result);
- //线程睡1秒
- TimeUnit.SECONDS.sleep(1);
- }
- //6.关闭生产者producer。由于MQ服务器在未收到生产者确认消息后会回查,所以启动生产者后先不要关闭
- //producer.shutdown();
- }
- }
消费者:
- /**
- * 消息的接受者
- */
- public class Consumer {
- public static void main(String[] args) throws Exception {
- //1.创建消费者Consumer,制定消费者组名
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
- //2.指定Nameserver地址
- consumer.setNamesrvAddr("192.168.32.130:9876;192.168.32.131:9876");
- //3.订阅主题Topic和Tag
- consumer.subscribe("TransactionTopic", "*");
- //4.设置回调函数,处理消息
- consumer.registerMessageListener(new MessageListenerConcurrently() {
- //接受消息内容
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
- for (MessageExt msg : msgs) {
- System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody()));
- }
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- });
- //5.启动消费者consumer
- consumer.start();
- System.out.println("消费者启动");
- }
- }
最终执行结果可以看到消费者消费了 TAGA 和 TAGC 的消息,但是没有消费 TAGB 的消息,因为 TAGB 被回滚了。
10.6、使用限制
事务消息有以下使用限制:
- 事务消息不支持延时消息和批量消息。
- 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的
transactionCheckMax
参数来修改此限制。如果已经检查某条消息超过 N 次的话( N =transactionCheckMax
) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写AbstractTransactionCheckListener
类来修改这个行为。 - 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后回查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于
transactionMsgTimeout
参数。 - 事务性消息可能不止一次被检查或消费。
- 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
- 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!