文章目录

  • 一 MQ的安装
  • 1.1 官方地址下载
  • 1.2 环境需要
  • 1.3 注意事项
  • 1.3.1 需要关闭虚拟机linux防火墙
  • 1.3.2 主机防火墙
  • 1.4 配置项
  • 1.4.1 broker 配置
  • 1.4.2 nameServer 配置
  • 1.5 启动 NameServer
  • 1.5.1 NameServer 日志
  • 1.6 启动 Broker
  • 1.6.1 Broker 日志
  • 1.7 安装可视化 rocketmq-console
  • 1.7.1 下载 rocketmq-console
  • 1.7.2 编辑 rocketmq-console 配置文件
  • 1.7.3 启动 rocketmq-console
  • 1.7.4 rocketmq-console 日志
  • 1.7.3 访问可视化
  • 二 基础入门
  • 2.1 MQ的定义
  • 2.2 为什么要用消息中间件
  • 2.2.1 应用解耦
  • 2.2.2 流量削峰
  • 2.2.3 数据分发
  • 2.3 RocketMQ 产品发展
  • 2.3.1 RocketMQ 版本发展
  • 2.3.2 阿里内部项目的使用
  • 2.3.3 展望未来
  • 三 RocketMQ 的物理架构
  • 3.1 核心概念
  • 3.1.1 NameServer
  • 3.1.2 生产者(Producer)
  • 3.1.3 消费者(Consumer)
  • 3.1.4 消息(Message)
  • 3.1.5 主机(Broker)
  • 3.2 物理架构中的整体运转
  • 四 RocketMQ 的概念模型
  • 4.1 核心概念
  • 4.1.1 分组(Group)
  • 4.1.2 主题(Topic)
  • 4.1.3 标签(Tag)
  • 4.1.4 消息队列(Message Queue)
  • 4.1.5 偏移量(Offset)
  • 五 玩转各种消息
  • 5.1 普通消息
  • 5.1.1 消息发送
  • 5.1.1.1 发送同步消息
  • 5.1.1.2 发送异步消息
  • 5.1.1.3 单向发送
  • 5.1.1.4 消息发送的权衡
  • 5.1.2 消息消费
  • 5.1.2.1 集群消费
  • 5.1.2.2 广播消费
  • 5.1.2.3 消息消费时的权衡
  • 5.2 顺序消息
  • 5.2.1 顺序消息生产
  • 5.2.2 顺序消息消费
  • 5.3 消息发送时的重要方法/属性(工作参考使用)
  • 5.3.1 属性
  • 5.3.2 方法
  • 5.3.2.1 单向发送
  • 5.3.3.3 同步发送
  • 5.3.3.2 异步发送
  • 5.4 消息消费时的重要方法/属性(工作参考使用)
  • 5.4.1 属性
  • 5.4.2 方法
  • 5.4.3 消费确认(ACK)
  • 5.5 延时消息
  • 5.5.1 概念介绍
  • 5.5.2 适用场景
  • 5.5.3 使用方式
  • 5.6 批量消息
  • 5.6.1 批量切分
  • 5.7 过滤消息
  • 5.7.1 Tag 过滤
  • 5.7.2 Sql 过滤
  • 5.7.2.1 SQL 基本语法
  • 5.8 事务消息
  • 5.8.1 正常事务流程
  • 5.8.2 事务补偿流程
  • 5.8.3 事务消息状态
  • 5.8.4 创建事务性生产者
  • 5.8.5 实现事务的监听接口
  • 5.8.6 消息消费者
  • 5.8.7 使用场景
  • 5.8.8 使用限制
  • 一 MQ的安装
  • 1.1 官方地址下载
  • 1.2 环境需要
  • 1.3 注意事项
  • 1.3.1 需要关闭虚拟机linux防火墙
  • 1.3.2 主机防火墙
  • 1.4 配置项
  • 1.4.1 broker 配置
  • 1.4.2 nameServer 配置
  • 1.5 启动 NameServer
  • 1.5.1 NameServer 日志
  • 1.6 启动 Broker
  • 1.6.1 Broker 日志
  • 1.7 安装可视化 rocketmq-console
  • 1.7.1 下载 rocketmq-console
  • 1.7.2 编辑 rocketmq-console 配置文件
  • 1.7.3 启动 rocketmq-console
  • 1.7.4 rocketmq-console 日志
  • 1.7.3 访问可视化
  • 二 基础入门
  • 2.1 MQ的定义
  • 2.2 为什么要用消息中间件
  • 2.2.1 应用解耦
  • 2.2.2 流量削峰
  • 2.2.3 数据分发
  • 2.3 RocketMQ 产品发展
  • 2.3.1 RocketMQ 版本发展
  • 2.3.2 阿里内部项目的使用
  • 2.3.3 展望未来
  • 三 RocketMQ 的物理架构
  • 3.1 核心概念
  • 3.1.1 NameServer
  • 3.1.2 生产者(Producer)
  • 3.1.3 消费者(Consumer)
  • 3.1.4 消息(Message)
  • 3.1.5 主机(Broker)
  • 3.2 物理架构中的整体运转
  • 四 RocketMQ 的概念模型
  • 4.1 核心概念
  • 4.1.1 分组(Group)
  • 4.1.2 主题(Topic)
  • 4.1.3 标签(Tag)
  • 4.1.4 消息队列(Message Queue)
  • 4.1.5 偏移量(Offset)
  • 五 玩转各种消息
  • 5.1 普通消息
  • 5.1.1 消息发送
  • 5.1.1.1 发送同步消息
  • 5.1.1.2 发送异步消息
  • 5.1.1.3 单向发送
  • 5.1.1.4 消息发送的权衡
  • 5.1.2 消息消费
  • 5.1.2.1 集群消费
  • 5.1.2.2 广播消费
  • 5.1.2.3 消息消费时的权衡
  • 5.2 顺序消息
  • 5.2.1 顺序消息生产
  • 5.2.2 顺序消息消费
  • 5.3 消息发送时的重要方法/属性(工作参考使用)
  • 5.3.1 属性
  • 5.3.2 方法
  • 5.3.2.1 单向发送
  • 5.3.3.3 同步发送
  • 5.3.3.2 异步发送
  • 5.4 消息消费时的重要方法/属性(工作参考使用)
  • 5.4.1 属性
  • 5.4.2 方法
  • 5.4.3 消费确认(ACK)
  • 5.5 延时消息
  • 5.5.1 概念介绍
  • 5.5.2 适用场景
  • 5.5.3 使用方式
  • 5.6 批量消息
  • 5.6.1 批量切分
  • 5.7 过滤消息
  • 5.7.1 Tag 过滤
  • 5.7.2 Sql 过滤
  • 5.7.2.1 SQL 基本语法
  • 5.8 事务消息
  • 5.8.1 正常事务流程
  • 5.8.2 事务补偿流程
  • 5.8.3 事务消息状态
  • 5.8.4 创建事务性生产者
  • 5.8.5 实现事务的监听接口
  • 5.8.6 消息消费者
  • 5.8.7 使用场景
  • 5.8.8 使用限制


一 MQ的安装

1.1 官方地址下载

作者微信:
Xri2117
欢迎大家加入JAVA交流群,添加作者微信拉群
写博客不易,请尊重原创

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送

使用最新的4.8的版本。

http://rocketmq.apache.org/dowloading/releases/

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送_02

1.2 环境需要

  • Linux 64位系统
  • Jdk 1.8
  • 建议安装目录在/opt/建立rocketMq文件夹
  • 注:源码安装需要Maven。运行版本不需要

1.3 注意事项

假设服务的外网IP地址:192.168.56.101 虚拟机的话就是本机IP 主机和虚拟机之间需要ping通

1.3.1 需要关闭虚拟机linux防火墙

互相ping不同 未关闭防火墙 可能导致连接失败,无法生产消息

systemctl stop firewalld
1.3.2 主机防火墙

rocketmq 用docker进行部署 rocketmq搭建教程_json_03

1.4 配置项

RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小。

但是这个也仅仅是在测试环境中,RocketMQ在生产上最低要求至少8G内存(官方推荐)才能确保RocketMQ的效果

编辑 runbroker.sh 和 runserver.sh修改默认JVM大小(windows上对应cmd文件)

1.4.1 broker 配置

/bin目录下

vim runbroker.sh

JAVA_OPT="${JAVA_OPT} -server -Xms1024m -Xmx1024m -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
1.4.2 nameServer 配置

/bin目录下

vim runserver.sh

JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
choose_gc_options

1.5 启动 NameServer

# 进入mq bin目录
cd /opt/bin

# 启动nameserver
nohup sh mqnamesrv &

# 查看日志 会在~目录下有logs 日志路径可修改
tail -200f ~/logs/rocketmqlogs/namesrv.log
1.5.1 NameServer 日志
2021-07-03 03:16:42 INFO main - rocketmqHome=/opt/rocketmq
2021-07-03 03:16:42 INFO main - kvConfigPath=/root/namesrv/kvConfig.json
2021-07-03 03:16:42 INFO main - configStorePath=/root/namesrv/namesrv.properties
2021-07-03 03:16:42 INFO main - productEnvName=center
2021-07-03 03:16:42 INFO main - clusterTest=false
2021-07-03 03:16:42 INFO main - orderMessageEnable=false
2021-07-03 03:16:42 INFO main - listenPort=9876
2021-07-03 03:16:42 INFO main - serverWorkerThreads=8
2021-07-03 03:16:42 INFO main - serverCallbackExecutorThreads=0
2021-07-03 03:16:42 INFO main - serverSelectorThreads=3
2021-07-03 03:16:42 INFO main - serverOnewaySemaphoreValue=256
2021-07-03 03:16:42 INFO main - serverAsyncSemaphoreValue=64
2021-07-03 03:16:42 INFO main - serverChannelMaxIdleTimeSeconds=120
2021-07-03 03:16:42 INFO main - serverSocketSndBufSize=65535
2021-07-03 03:16:42 INFO main - serverSocketRcvBufSize=65535
2021-07-03 03:16:42 INFO main - serverPooledByteBufAllocatorEnable=true
2021-07-03 03:16:42 INFO main - useEpollNativeSelector=false
2021-07-03 03:16:42 INFO main - Server is running in TLS permissive mode
2021-07-03 03:16:42 INFO main - Tls config file doesn't exist, skip it
2021-07-03 03:16:42 INFO main - Log the final used tls related configuration
2021-07-03 03:16:42 INFO main - tls.test.mode.enable = true
2021-07-03 03:16:42 INFO main - tls.server.need.client.auth = none
2021-07-03 03:16:42 INFO main - tls.server.keyPath = null
2021-07-03 03:16:42 INFO main - tls.server.keyPassword = null
2021-07-03 03:16:42 INFO main - tls.server.certPath = null
2021-07-03 03:16:42 INFO main - tls.server.authClient = false
2021-07-03 03:16:42 INFO main - tls.server.trustCertPath = null
2021-07-03 03:16:42 INFO main - tls.client.keyPath = null
2021-07-03 03:16:42 INFO main - tls.client.keyPassword = null
2021-07-03 03:16:42 INFO main - tls.client.certPath = null
2021-07-03 03:16:42 INFO main - tls.client.authServer = false
2021-07-03 03:16:42 INFO main - tls.client.trustCertPath = null
2021-07-03 03:16:42 INFO main - Using OpenSSL provider
2021-07-03 03:16:43 INFO main - SSLContext created for server
2021-07-03 03:16:43 INFO main - Try to start service thread:FileWatchService started:false lastThread:null
2021-07-03 03:16:43 INFO NettyEventExecutor - NettyEventExecutor service started

# 容器已经启动
2021-07-03 03:16:43 INFO main - The Name Server boot success. serializeType=JSON
2021-07-03 03:16:43 INFO FileWatchService - FileWatchService service started
2021-07-03 03:17:43 INFO NSScheduledThread1 - --------------------------------------------------------
2021-07-03 03:17:43 INFO NSScheduledThread1 - configTable SIZE: 0

1.6 启动 Broker

修改配置文件增加外网地址(你启动加载哪个配置文件就修改哪个,这里修改broker.conf)

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 公网ip 虚拟机的话就是虚拟机机ip 本机可以ping通 
brokerIP1=192.168.3.200
# nameserver地址
namesrvAddr=192.168.3.200:9876
# 进入mq bin目录
cd /opt/bin

# 启动broker -n 指定namever地址 conf文件配置后可以不指定,通过配置文件配置
# autoCreateTopicEnable=true 这样启动的服务器可以自动创建主题(客户端),不过生产一般不推荐。
nohup sh mqbroker -c ../conf/broker.conf -n 192.168.3.200:9876 autoCreateTopicEnable=true &

# 查看日志 会在~目录下有logs 日志路径可修改
tail -200f ~/logs/rocketmqlogs/broker.log
  • 可能发生的问题
# /root/store/ 没有commitlog文件夹 创建即可
2021-07-03 03:27:38 ERROR DiskCheckScheduledThread1 - Error when measuring disk space usage, file doesn't exist on this path: /root/store/commitlog

# /root/store/ 没有consumequeue文件夹 创建即可
Error when measuring disk space usage, file doesn't exist on this path: /root/store/consumequeue
1.6.1 Broker 日志
[topicName=SCHEDULE_TOPIC_XXXX, readQueueNums=18, writeQueueNums=18, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=RMQ_SYS_TRANS_HALF_TOPIC, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=DefaultCluster_REPLY_TOPIC, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=BenchmarkTest, readQueueNums=1024, writeQueueNums=1024, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=OFFSET_MOVED_EVENT, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=broker-a, readQueueNums=1, writeQueueNums=1, perm=RWX, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=TBW102, readQueueNums=8, writeQueueNums=8, perm=RWX, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=SELF_TEST_TOPIC, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=DefaultCluster, readQueueNums=16, writeQueueNums=16, perm=RWX, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load /root/store/config/topics.json OK
2021-07-03 03:29:51 INFO main - load /root/store/config/consumerOffset.json OK
2021-07-03 03:29:51 INFO main - load /root/store/config/consumerFilter.json OK
2021-07-03 03:29:51 INFO main - Try to start service thread:AllocateMappedFileService started:false lastThread:null
2021-07-03 03:29:51 INFO main - load /root/store/config/delayOffset.json OK
2021-07-03 03:29:52 INFO main - Set user specified name server address: 192.168.3.200:9876
2021-07-03 03:29:52 WARN main - Load default transaction message hook service: TransactionalMessageServiceImpl
2021-07-03 03:29:52 WARN main - Load default discard message hook service: DefaultTransactionalMessageCheckListener
2021-07-03 03:29:52 INFO main - The broker dose not enable acl
2021-07-03 03:29:52 INFO main - Try to start service thread:ReputMessageService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:AcceptSocketService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:GroupTransferService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:HAClient started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:FlushConsumeQueueService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:FlushRealTimeService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:StoreStatsService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:FileWatchService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:PullRequestHoldService started:false lastThread:null
2021-07-03 03:29:52 INFO FileWatchService - FileWatchService service started
2021-07-03 03:29:52 INFO PullRequestHoldService - PullRequestHoldService service started
2021-07-03 03:29:52 INFO main - Try to start service thread:TransactionalMessageCheckService started:false lastThread:null

# 注册成功
2021-07-03 03:29:52 INFO brokerOutApi_thread_1 - register broker[0]to name server 192.168.3.200:9876 OK

# 启动成功
2021-07-03 03:29:52 INFO main - The broker[broker-a, 192.168.3.200:10911] boot success. serializeType=JSON and name server is 192.168.3.200:9876
2021-07-03 03:30:02 INFO BrokerControllerScheduledThread1 - dispatch behind commit log 0 bytes
2021-07-03 03:30:02 INFO BrokerControllerScheduledThread1 - Slave fall behind master: 0 bytes
# nameserver 日志会出现 新broker 注册
2021-07-03 03:29:52 INFO RemotingExecutorThread_4 - new broker registered, 192.168.3.200:10911 HAServer: 172.17.0.1:10912

1.7 安装可视化 rocketmq-console

1.7.1 下载 rocketmq-console

运行前确保:已经有jdk1.8,Maven(打包需要安装Maven 3.2.x)

下载:https://codeload.github.com/apache/rocketmq-externals/zip/master

rocketmq 用docker进行部署 rocketmq搭建教程_json_04

这个包主要包含的是Message Connector,具体详情见 https://rocketmq-1.gitbook.io/rocketmq-connector/

里面后端管理界面是:rocketmq-console

1.7.2 编辑 rocketmq-console 配置文件

下载完成之后,进入‘\rocketmq-console\src\main\resources’文件夹,打开‘application.properties’进行配置。

# 服务器地址
server.address=192.168.3.200
# 控制台端口
server.port=8089

### SSL setting
#server.ssl.key-store=classpath:rmqcngkeystore.jks
#server.ssl.key-store-password=rocketmq
#server.ssl.keyStoreType=PKCS12
#server.ssl.keyAlias=rmqcngkey

#spring.application.index=true
spring.application.name=rocketmq-console
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
logging.level.root=INFO
logging.config=classpath:logback.xml
#if this value is empty,use env value rocketmq.config.namesrvAddr  NAMESRV_ADDR | now, you can set it in ops page.default localhost:9876

# nameserver地址 默认端口9876
rocketmq.config.namesrvAddr=192.168.3.200:9876
#if you use rocketmq version < 3.5.8, rocketmq.config.isVIPChannel should be false.default true
rocketmq.config.isVIPChannel=
#rocketmq-console's data path:dashboard/monitor
rocketmq.config.dataPath=/tmp/rocketmq-console/data
#set it false if you don't want use dashboard.default true
rocketmq.config.enableDashBoardCollect=true
#set the message track trace topic if you don't want use the default one
rocketmq.config.msgTrackTopicName=
rocketmq.config.ticketKey=ticket

#Must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required
rocketmq.config.loginRequired=false

#set the accessKey and secretKey if you used acl
#rocketmq.config.accessKey=
#rocketmq.config.secretKey=

进入\rocketmq-externals\rocketmq-console文件夹

执行mvn clean package -Dmaven.test.skip=true 编译生成可执行jar包

建议放入/opt/建立文件夹

1.7.3 启动 rocketmq-console
# 启动可视化 可指定内存大小
nohup java -jar -Xms128m -Xmx256m rocketmq-console-ng-2.0.0.jar &
1.7.4 rocketmq-console 日志
.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.2.RELEASE)

[2021-07-03 03:41:10.868]  INFO Starting App v2.0.0 on localhost with PID 29113 (/opt/rocketmq-console/rocketmq-console-ng-2.0.0.jar started by root in /opt/rocketmq-console)
[2021-07-03 03:41:10.869]  INFO No active profile set, falling back to default profiles: default
[2021-07-03 03:41:13.198]  INFO setNameSrvAddrByProperty nameSrvAddr=192.168.3.200:9876
[2021-07-03 03:41:13.694]  INFO Tomcat initialized with port(s): 8089 (http)
[2021-07-03 03:41:13.708]  INFO Initializing ProtocolHandler ["http-nio-192.168.3.200-8089"]
[2021-07-03 03:41:13.709]  INFO Starting service [Tomcat]
[2021-07-03 03:41:13.709]  INFO Starting Servlet engine: [Apache Tomcat/9.0.29]
[2021-07-03 03:41:13.908]  INFO Initializing Spring embedded WebApplicationContext
[2021-07-03 03:41:13.909]  INFO Root WebApplicationContext: initialization completed in 2811 ms
[2021-07-03 03:41:15.166]  INFO Initializing ExecutorService 'applicationTaskExecutor'
[2021-07-03 03:41:15.350]  INFO Adding welcome page: class path resource [static/index.html]
[2021-07-03 03:41:15.563]  INFO Initializing ExecutorService 'taskScheduler'
[2021-07-03 03:41:15.584]  INFO Exposing 2 endpoint(s) beneath base path '/actuator'
[2021-07-03 03:41:15.656]  INFO Starting ProtocolHandler ["http-nio-192.168.3.200-8089"]
[2021-07-03 03:41:15.692]  INFO Tomcat started on port(s): 8089 (http) with context path ''
[2021-07-03 03:41:15.697]  INFO Started App in 5.735 seconds (JVM running for 6.688)
1.7.3 访问可视化

浏览器输入 192.168.3.200:8089 查看

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_05

二 基础入门

有需要其他介绍请参阅:
rocketMQ官方文档

2.1 MQ的定义

其实并没有标准定义。一般认为,消息中间件属于分布式系统中一个子系统,关注于数据的发送和接收,利用高效可靠的异步消息传递机制对分布
式系统中的其余各个子系统进行集成。

  1. 高效:对于消息的处理处理速度快。
  2. 可靠:一般消息中间件都会有消息持久化机制和其他的机制确保消息不丢失。
  3. 异步:指发送完一个请求,不需要等待返回,随时可以再发送下一个请求,既不需要等待
  4. 总结我们消息中间件不生产消息,只是消息的搬运工。

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送_06

2.2 为什么要用消息中间件

2.2.1 应用解耦

系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者
因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验

使用消息中间件,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队
列中,用户的下单操作正常完成。当物流系统恢复后,继续处理存放在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。

rocketmq 用docker进行部署 rocketmq搭建教程_json_07

2.2.2 流量削峰

应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大
提到系统的稳定性和用户体验。

互联网公司的大促场景(双十一、店庆活动、秒杀活动)都会使用到 MQ

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送_08

2.2.3 数据分发

通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消
息队列中直接获取数据即可。

接口调用的弊端,无论是新增系统,还是移除系统,代码改造工作量都很大。

使用 MQ 做数据分发好处,无论是新增系统,还是移除系统,代码改造工作量较小。

所以使用 MQ 做数据的分发,可以提高团队开发的效率

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_09

2.3 RocketMQ 产品发展

2.3.1 RocketMQ 版本发展

Metaq1.x 是 RocketMQ 前身的第一个版本,本质上把 Kafka 做了一次 java 版本的重写(Kafka 是 sacla)

Meta2.x,主要是对存储部分进行了优化,因为 kafka 的数据存储,它的 paration 是一个全量的复制,在阿里、在淘宝的这种海量交易。Kafka 这种
机制的横向拓展是非常不好的。2012 年阿里同时把 Meta2.0 从阿里内部开源出来,取名 RocketMQ,同时为了命名上的规范(版本上延续),所以这个就
是 RocketMQ3.0

现在 RocketMQ 主要维护的是 4.x 的版本,也是大家使用得最多的版本,2017 年从 Apache 顶级项目毕

2.3.2 阿里内部项目的使用

那么在阿里公司内部,原则上遵守开源共建原则。RocketMQ 项目只维护核心功能,且去除了所有其他运行时依赖,核心功能最简化。每个 BU
( Business Unit 业务单元)的个性化需求都在 RocketMQ 项目之上进行深度定制。RocketMQ 向其他 BU 提供的仅仅是 Jar 包,例如要定制一个 Broker,
那么只需要依赖 rocketmq-broker 这 jar 包即可,可通过 API 进行交互, 如果定制 client,则依赖 rocketmq-client 这个 jar 包,对其提供的 api 进行
再封装

在 RocketMQ 项目基础上几个常用的项目如

  • com.taobao.metaq v3.0 = RocketMQ + 淘宝个性化需求

为淘宝应用提供消息服务

  • com.alipay.zpullmsg v1.0 = RocketMQ + 支付宝个性化需求

为支付宝应用提供消息服务

  • com.alibaba.commonmq v1.0 = Notify + RocketMQ + B2B 个性化需求

为 B2B 应用提供消息服务

2.3.3 展望未来

从阿里负责 RocketMQ 的架构核心人员的信息来看,阿里内部一直全力拓展 RocketMQ

2017 年 10 月份,OpenMessaging 项目由阿里巴巴发起,与雅虎、滴滴出行、Streamlio 公司共同参与创立, 项目意在创立厂商无关、平台无关的分布
式消息及流处理领域的应用开发标准。同时 OpenMessaging 入驻 Linux 基金会

OpenMessaging 项目已经开始在 Apache RocketMQ 中率先落地,并推广至整个阿里云平台.

另外 RocketMQ5 的版本也在内部推进,主要的方向是 Cloud Native(云原生)

另外 Apache RocketMQ 的商业版本,Aliware MQ 在微服务、流计算、IoT、异步解耦、数据同步等场景有非常广泛的运用

rocketmq 用docker进行部署 rocketmq搭建教程_java_10

三 RocketMQ 的物理架构

消息队列 RocketMQ 是阿里巴巴集团基于高可用分布式集群技术,自主研发的云正式商用的专业消息中间件,既可为分布式应用系统提供异步解耦
和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性,是阿里巴巴双 11 使用的核心产品。

RocketMQ 的设计基于主题的发布与订阅模式,其核心功能包括消息发送、消息存储(Broker)、消息消费,整体设计追求简单与性能第一。

3.1 核心概念

rocketmq 用docker进行部署 rocketmq搭建教程_java_11

3.1.1 NameServer

NameServer 是整个 RocketMQ 的“大脑”,它是 RocketMQ 的服务注册中心,所以 RocketMQ 需要先启动 NameServer 再启动 Rocket 中的 Broker。

Broker 在启动时向所有 NameServer 注册(主要是服务器地址等),生产者在发送消息之前先从 NameServer 获取 Broker 服务器地址列表(消费者一
样),然后根据负载均衡算法从列表中选择一台服务器进行消息发送。

NameServer 与每台 Broker 服务保持长连接,并间隔 30s 检查 Broker 是否存活,如果检测到 Broker 宕机,则从路由注册表中将其移除。这样就可以实
现 RocketMQ 的高可用。具体细节后续讲解

3.1.2 生产者(Producer)

生产者:也称为消息发布者,负责生产并发送消息至 RocketMQ。

3.1.3 消费者(Consumer)

消费者:也称为消息订阅者,负责从 RocketMQ 接收并消费消息。

3.1.4 消息(Message)

消息:生产或消费的数据,对于 RocketMQ 来说,消息就是字节数组。

3.1.5 主机(Broker)

RocketMQ 的核心,用于暂存和传输消息。

3.2 物理架构中的整体运转

  1. NameServer 先启动
  2. Broker 启动时向 NameServer 注册
  3. 生产者在发送某个主题的消息之前先从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),然后根据负载均衡算法从列表中选择一台
    Broker 进行消息发送。
  4. NameServer 与每台 Broker 服务器保持长连接,并间隔 30S 检测 Broker 是否存活,如果检测到 Broker 宕机(使用心跳机制,如果检测超过120S),则从路由注册表中将其移除。
  5. 消费者在订阅某个主题的消息之前从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),但是消费者选择从 Broker 中订阅消息,订阅
    规则由 Broker 配置决定

四 RocketMQ 的概念模型

4.1 核心概念

4.1.1 分组(Group)

生产者:标识发送同一类消息的Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息
(事务消息中如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其它producer,
确认这条消息应该 commit 还是 rollback)

消费者:标识一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致。同一个 Consumer Group 下的各个实例将共同消费 topic
的消息,起到负载均衡的作用。

消费进度以 Consumer Group 为粒度管理,不同 Consumer Group 之间消费进度彼此不受影响,即消息 A 被 Consumer Group1 消费过,也会再给 Consumer
Group2 消费。

4.1.2 主题(Topic)

标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定 Topic。

区分消息的种类;一个发送者可以发送消息给一个或者多个 Topic
一个消息的接收者可以订阅一个或者多个 Topic 消息

4.1.3 标签(Tag)

RocketMQ 支持给在发送的时候给 topic 打 tag,同一个 topic 的消息虽然逻辑管理是一样的。但是消费 topic1 的时候,如果你消费订阅的时候指定的
是 tagA,那么 tagB 的消息将不会投递。

4.1.4 消息队列(Message Queue)

简称 Queue 或 Q。消息物理管理单位。一个 Topic 将有若干个 Q。若一个 Topic 创建在不同的 Broker,则不同的 broker 上都有若干 Q,消息将物理地
存储落在不同 Broker 结点上,具有水平扩展的能力。
无论生产者还是消费者,实际的生产和消费都是针对 Q 级别。例如 Producer 发送消息的时候,会预先选择(默认轮询)好该 Topic 下面的某一条 Q
发送;Consumer 消费的时候也会负载均衡地分配若干个 Q,只拉取对应 Q 的消息。
每一条 message queue 均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log)
恢复回来。

4.1.5 偏移量(Offset)

RocketMQ 中,有很多 offset 的概念。一般我们只关心暴露到客户端的 offset。不指定的话,就是指 Message Queue 下面的 offset。
Message queue 是无限长的数组。一条消息进来下标就会涨 1,而这个数组的下标就是 offset,Message queue 中的 max offset 表示消息的最大 offset
Consumer offset 可以理解为标记 Consumer Group 在一条逻辑 Message Queue 上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的
最新消费的消息 offset+1,即实际上表示的是下次拉取的 offset

五 玩转各种消息

5.1 普通消息

  • 导入MQ客户端依赖
<dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.8.0</version>
    </dependency>
  • 消息发送步骤
  1. 创建消息生产者 producer,并指定生产者组名
  2. 指定 Nameserver 地址
  3. 启动 producer
  4. 创建消息对象,指定 Topic、Tag 和消息体
  5. 发送消息
  6. 关闭生产者 producer
  • 消息消费者步骤
  1. 创建消费者 Consumer,指定消费者组名
  2. 指定 Nameserver 地址
  3. 订阅主题 Topic 和 Tag
  4. 设置回调函数,处理消息
  5. 启动消费者 consumer
5.1.1 消息发送
5.1.1.1 发送同步消息

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送_12

这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

同步发送是指消息发送方发出数据后,同步等待,直到收到接收方发回响应之后才发下一个请求。

/**
 * @author Crazy.X
 * 同步发送
 */
public class SyncProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        for (int i = 0; i < 100; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            // 发送消息到一个Broker
            SendResult sendResult = producer.send(msg);
            // 通过sendResult返回消息是否成功送达
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_13

  • Message ID

消息的全局唯一标识(内部机制的 ID 生成是使用机器 IP 和消息偏移量的组成,所以有可能重复,如果是幂等性还是最好考虑 Key),由消息队列 MQ
系统自动生成,唯一标识某条消息。

  • SendStatus

发送的标识。成功,失败等

  • Queue

相当于是 Topic 的分区;用于并行发送和接收消息

5.1.1.2 发送异步消息

rocketmq 用docker进行部署 rocketmq搭建教程_json_14

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 Broker的响应

消息发送方在发送了一条消息后,不等接收方发回响应,接着进行第二条消息发送。发送方通过回调接口的方式接收服务器响应,并对响应结果进行处理

/**
 * @author Crazy.X
 * 异步发送
 */
public class AsyncProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        // 设置发送异步失败时的重试次数
        producer.setRetryTimesWhenSendAsyncFailed(0);
        //启用Broker故障延迟机制
        producer.setSendLatencyFaultEnable(true);

        for (int i = 0; i < 100; i++) {
            final int index = i;
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest",
                    "TagA",
                    "OrderID888",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
            // SendCallback接收异步返回结果的回调
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
        }
        Thread.sleep(10000);
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_java_15

5.1.1.3 单向发送

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_16

这种方式主要用在不特别关心发送结果的场景,例如日志发送

单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

/**
 * @author Crazy.X
 * 单向发送
 */
public class OneWayProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        for (int i = 0; i < 100; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            // 发送单向消息,没有任何返回结果
            producer.sendOneway(msg);

        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
5.1.1.4 消息发送的权衡

发送方式

发送TPS

发送结果反馈

可靠性

适用场景

同步可靠发送



不丢失

重要通知邮件、报名短信通知、营销短信系统等

异步可靠发送



不丢失

用户视频上传后通知启动转码服务、转码完成后通知推送转码结果等

单向发送



可能丢失

适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集

5.1.2 消息消费
5.1.2.1 集群消费

rocketmq 用docker进行部署 rocketmq搭建教程_java_17

消费者的一种消费模式。一个 Consumer Group 中的各个 Consumer 实例分摊去消费消息,即一条消息只会投递到一个 Consumer Group 下面的一个实例。

实际上,每个 Consumer 是平均分摊 Message Queue 的去做拉取消费。例如某个 Topic 有 3 条 Q,其中一个 Consumer Group有3个实例(可能是3个进程,或者3台机器),那么每个实例只消费其中的 1 条 Q。

而由 Producer 发送消息的时候是轮询所有的 Q,所以消息会平均散落在不同的 Q 上,可以认为 Q 上的消息是平均的。那么实例也就平均地消费消息了。
这种模式下,消费进度(Consumer Offset)的存储会持久化到 Broker

/**
 * @author Crazy.X
 * 推模式集群消费
 */
public class BalanceConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息消费者,指定组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 指定NameServer地址信息
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 设置最大重新消费次数
        consumer.setMaxReconsumeTimes(1);
        // 订阅Topic subExpression 对tag进行指定进行过滤 也可以正则表达式 TagA|TagB
        consumer.subscribe("TopicTest", "*");
        // 负载均衡模式集群消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                try {
                    for (MessageExt msg : msgs) {
                        String topic = msg.getTopic();
                        String msgBody = new String(msg.getBody());
                        String tags = msg.getTags();
                        System.out.println("收到消息: " + "topic:" + topic + ",tags:" + tags + ",msgBody:" + msgBody);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    // 消费失败,认为没有消费,可能会重复发送
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                // 未出现异常进行提交 成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_json_18

5.1.2.2 广播消费

rocketmq 用docker进行部署 rocketmq搭建教程_json_19

消费者的一种消费模式。消息将对一个 Consumer Group 下的各个 Consumer 实例都投递一遍。即即使这些 Consumer 属于同一个 Consumer Group,
消息也会被 Consumer Group 中的每个 Consumer 都消费一次。

实际上,是一个消费组下的每个消费者实例都获取到了 topic 下面的每个 Message Queue 去拉取消费。所以消息会投递到每个消费者实例。
这种模式下,消费进度(Consumer Offset)会存储持久化到实例本地

/**
 * @author Crazy.X
 * 广播模式消费
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        /// 实例化消息消费者,指定组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 指定NameServer地址信息
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 设置最大重新消费次数
        consumer.setMaxReconsumeTimes(1);
        // 订阅Topic subExpression 对tag进行指定进行过滤 也可以正则表达式 TagA|TagB
        consumer.subscribe("TopicTest", "*");
        //广播模式消费
        consumer.setMessageModel(MessageModel.BROADCASTING);

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            // 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,
            // 但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
5.1.2.3 消息消费时的权衡

集群模式:适用场景&注意事项

  1. 消费端集群化部署,每条消息只需要被处理一次。
  2. 由于消费进度在服务端维护,可靠性更高。
  3. 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
  4. 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。

广播模式:适用场景&注意事项

  1. 广播消费模式下不支持顺序消息。
  2. 广播消费模式下不支持重置消费位点。
  3. 每条消息都需要被相同逻辑的多台机器处理。
  4. 消费进度在客户端维护,出现重复的概率稍大于集群模式。
  5. 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
  6. 广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
  7. 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
  8. 目前仅 Java 客户端支持广播模式。
  9. 广播模式下服务端不维护消费进度,所以消息队列RocketMQ控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

5.2 顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ 可以严格的保证消息有序,可以分为分区有序或者全局有序。

顺序消费的原理解析,在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列);而消费消息的时候从多个 queue
上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉
取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个 queue 参与,则为分区有序,即相对每个 queue,消息都是有序的。

  • 全局顺序消息

全局顺序只创建一个queqe 即可全局顺序 不做记录

  • 部分顺序消息
5.2.1 顺序消息生产

一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,下面是订单进行分区有序的示例代码。

/**
 * @author Crazy.X
 * 部分顺序消息生产
 */
public class ProducerInOrder {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("OrderProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();

        // tags数组 使用tag区分
        String[] tags = new String[]{"TagA", "TagC", "TagD"};
        // 订单列表 模拟订单数据
        List<Order> orderList = new ProducerInOrder().buildOrders();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(date);

        // 创建消息,并指定Topic,Tag和消息体
        for (int i = 0; i < orderList.size(); i++) {
            // 加个时间前缀
            String body = dateStr + " Order:" + orderList.get(i);
            // 指定tags组
            Message msg = new Message("PartOrder", tags[i % tags.length], "KEY" + i, body.getBytes());

            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    //根据订单id选择发送queue
                    Long id = (Long) arg;
                    long index = id % mqs.size();
                    // 返回
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//订单id

            System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
                    sendResult.getSendStatus(),
                    sendResult.getMessageQueue().getQueueId(),
                    body));
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }

    /**
     * 订单
     */
    private static class Order {
        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 "Order{" +
                    "orderId=" + orderId +
                    ", desc='" + desc + '\'' +
                    '}';
        }
    }

    /**
     * 生成模拟订单数据
     */
    private List<Order> buildOrders() {
        List<Order> orderList = new ArrayList<Order>();

        Order orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        return orderList;
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BMWlGpo-1625329014522)(494F4EF9B5884E1C911324A04E9C0140)]

5.2.2 顺序消息消费

消费时,同一个 OrderId 获取到的肯定是同一个队列。从而确保一个订单中处理的顺序。

/**
 * @author Crazy.X
 * 部分顺序消息消费
 */
public class ConsumerInOrder {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderConsumer");
        consumer.setNamesrvAddr("192.168.3.200:9876");

        // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
        // 如果非第一次启动,那么按照上次消费的位置继续消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("PartOrder", "TagA || TagC || TagD");
        consumer.registerMessageListener(new MessageListenerOrderly() {
            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() +
                            "queueId=" + msg.getQueueId() +
                            ", content:" + new String(msg.getBody()));
                }
                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.out.println("Consumer Started.");
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_20

5.3 消息发送时的重要方法/属性(工作参考使用)

属性与方法是整个类,可以放到一个类下观看

5.3.1 属性
//  producerGroup:生产者所属组(针对 事务消息 高可用)
        DefaultMQProducer producer = new DefaultMQProducer("produce_details");
        //  默认主题在每一个Broker队列数量(对于新创建主题有效)
        producer.setDefaultTopicQueueNums(8);
        //  发送消息默认超时时间,默认3s (3000ms)
        producer.setSendMsgTimeout(1000 * 3);
        //  消息体超过该值则启用压缩,默认4k
        producer.setCompressMsgBodyOverHowmuch(1024 * 4);
        //  同步方式发送消息重试次数,默认为2,总共执行3次
        producer.setRetryTimesWhenSendFailed(2);
        //  异步方式发送消息重试次数,默认为2,总共执行3次
        producer.setRetryTimesWhenSendAsyncFailed(2);
        //  消息重试时选择另外一个Broker时(消息没有存储成功是否发送到另外一个broker),默认为false
        producer.setRetryAnotherBrokerWhenNotStoreOK(false);
        //  允许发送的最大消息长度,默认为4M
        producer.setMaxMessageSize(1024 * 1024 * 4);

        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
5.3.2 方法
// 启动Producer实例
        producer.start();
        
        // 如果不再发送消息,关闭Producer实例。(这行代码要放在最后)
        producer.shutdown();
        
        // 查找该主题下所有消息队列
        List<MessageQueue> MessageQueue = producer.fetchPublishMessageQueues("TopicTest");
        for (MessageQueue queue : MessageQueue) {
            System.out.println(queue.getQueueId());
        }
5.3.2.1 单向发送
for (int i = 0; i < 10; i++) {
            final int index = i;
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest",
                    "TagA",
                    "OrderID888",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)
            );

            //  ------------  单向发送  ------------
            //  1.1发送单向消息
            producer.sendOneway(msg);
            //  1.2指定队列单向发送消息(使用select方法)
            producer.sendOneway(msg, (mqs, msg1, arg) -> mqs.get(0), null);
            //  1.3指定队列单向发送消息(根据之前查找出来的主题)
            producer.sendOneway(msg, MessageQueue.get(0));
5.3.3.3 同步发送
//  2.1同步发送消息
            SendResult sendResult0 = producer.send(msg);
            //  2.1同步超时发送消息(属性设置:sendMsgTimeout 发送消息默认超时时间,默认3s (3000ms) )
            SendResult sendResult1 = producer.send(msg, 1000 * 3);
            //  2.2指定队列同步发送消息(使用select方法)
            SendResult sendResult2 = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    return mqs.get(0);
                }
            }, null);
            //  2.3指定队列同步发送消息(根据之前查找出来的主题队列信息)
            SendResult sendResult3 = producer.send(msg, MessageQueue.get(0));
5.3.3.2 异步发送
//  3.1异步发送消息
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
            //  3.1异步超时发送消息
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            }, 1000 * 3);
            //  3.2选择指定队列异步发送消息(根据之前查找出来的主题队列信息)
            producer.send(msg, MessageQueue.get(0),
                    new SendCallback() {
                        @Override
                        public void onSuccess(SendResult sendResult) {
                            System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                        }

                        @Override
                        public void onException(Throwable e) {
                            System.out.printf("%-10d Exception %s %n", index, e);
                            e.printStackTrace();
                        }
                    });
            //  3.3选择指定队列异步发送消息(使用select方法)
            producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            return mqs.get(0);
                        }
                    },
                    new SendCallback() {
                        @Override
                        public void onSuccess(SendResult sendResult) {
                            System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                        }

                        @Override
                        public void onException(Throwable e) {
                            System.out.printf("%-10d Exception %s %n", index, e);
                            e.printStackTrace();
                        }
                    });
        }

5.4 消息消费时的重要方法/属性(工作参考使用)

属性与方法是整个类,可以放到一个类下观看

5.4.1 属性
// consumerGroup:消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("Faker");
        // 指定NameServer地址信息.
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 消息消费模式(默认集群消费)
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 指定消费开始偏移量(上次消费偏移量、最大偏移量、最小偏移量、启动时间戳)开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        // 消费者最小线程数量(默认20)
        consumer.setConsumeThreadMin(20);
        // 消费者最大线程数量(默认20)
        consumer.setConsumeThreadMax(20);
        // 推模式下任务间隔时间(推模式也是基于不断的轮训拉取的封装)
        consumer.setPullInterval(0);
        // 推模式下任务拉取的条数,默认32条(一批批拉)
        consumer.setPullBatchSize(32);
        // 消息重试次数,-1代表16次 (超过 次数成为死信消息)
        consumer.setMaxReconsumeTimes(-1);
        // 消息消费超时时间(消息可能阻塞正在使用的线程的最大时间:以分钟为单位)
        consumer.setConsumeTimeout(15);
5.4.2 方法
// 方法-订阅
        // 基于主题订阅消息,消息过滤使用表达式
        consumer.subscribe("TopicTest", "*"); //tag  tagA||TagB||TagC
        // 基于主题订阅消息,消息过滤使用表达式
        consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3"));
        // 基于主题订阅消息,消息过滤使用表达式
        consumer.subscribe("TopicTest", MessageSelector.byTag("tagA||TagB"));
        // 取消消息订阅
        consumer.unsubscribe("TopicTest");

        // 注册监听器
        // 注册并发事件监听器
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                //没有成功  -- 到重试队列中来
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            //  // 未出现异常进行提交 成功
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            //todo
        });

        // 注册顺序消息事件监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
                } catch (Exception e) {
                    e.printStackTrace();
                    // 这个点要注意:意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
5.4.3 消费确认(ACK)
  1. 业务实现消费回调的时候,当且仅当此回调函数返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ 才会认为这批消息(默认是 1 条)
    是消费完成的。中途断电,抛出异常等都不会认为成功——即都会重新投递。
  2. 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ 就会认为这批消息消费失败了。
  3. 如果业务的回调没有处理好而抛出异常,会认为是消费失败 ConsumeConcurrentlyStatus.RECONSUME_LATER 处理。
  4. 为了保证消息是肯定被至少消费成功一次,RocketMQ 会把这批消息重发回 Broker(topic 不是原 topic 而是这个消费组的 RETRY topic),在延迟的某
    个时间点(默认是 10 秒,业务可设置)后,再次投递到这个 ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认 16 次),就会投
    递到 DLQ 死信队列。应用可以监控死信队列来做人工干预。
  5. 另外如果使用顺序消费的回调 MessageListenerOrderly 时,由于顺序消费是要前者消费成功才能继续消费,所以没有 RECONSUME_LATER 的这个状态,
    只有 SUSPEND_CURRENT_QUEUE_A_MOMENT 来暂停队列的其余消费,直到原消息不断重试成功为止才能继续消

5.5 延时消息

5.5.1 概念介绍

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_21

延时消息:Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

5.5.2 适用场景

消息生产和消费有时间窗口要求:比如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在 30 分钟以
后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略。

5.5.3 使用方式

Apache RocketMQ 目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在 Broker 层面,必须要做消息排序,如果再涉及到持久化,
那么消息排序要不可避免的产生巨大性能开销。(阿里云 RocketMQ 提供了任意时刻的定时消息功能,Apache 的 RocketMQ 并没有,阿里并没有开源)

发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递

延迟消息是根据延迟队列的 level 来的,延迟队列默认

msg.setDelayTimeLevel(3)代表延迟 10 秒
“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”

在源码org/apache/rocketmq/store/config/MessageStoreConfig.java

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_22

是这 18 个等级(秒(s)、分(m)、小时(h)),level 为 1,表示延迟 1 秒后消费,level 为 5 表示延迟 1 分钟后消费,level 为 18 表示延迟 2 个
小时消费。生产消息跟普通的生产消息类似,只需要在消息上设置延迟队列的 level 即可。消费消息跟普通的消费消息一致。

  • 生产者
/**
 * @author Crazy.X
 * 延时消息-生产者
 */
public class ScheduledMessageProducer {
    public static void main(String[] args) throws Exception {
        // 实例化一个生产者来产生延时消息
        DefaultMQProducer producer = new DefaultMQProducer("ScheduledProducer");
        // 指定NameServer地址信息
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        int totalMessagesToSend = 10;
        for (int i = 0; i < totalMessagesToSend; i++) {
            Message message = new Message("ScheduledTopic", ("Hello scheduled message " + i).getBytes());
            // 设置延时等级3,这个消息将在10s之后投递给消费者(详看delayTimeLevel)
            // delayTimeLevel:(1~18个等级)"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
            // 4就是第四个等级 30s
            message.setDelayTimeLevel(4);
            // 发送消息
            producer.send(message);
        }
        // 关闭生产者
        producer.shutdown();
    }
}
  • 消费者
/**
 * @author Crazy.X
 * 延时消息-消费者
 */
public class ScheduledMessageConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ScheduledConsumer");
        // 指定NameServer地址信息
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 订阅Topics
        consumer.subscribe("ScheduledTopic", "*");
        // 注册消息监听者
        consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {
            for (MessageExt message : messages) {
                // msgId 消息ID
                // getStoreTimestamp() 存储时间戳
                // getBornTimestamp() 诞生时间戳
                System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
                        + (message.getStoreTimestamp() - message.getBornTimestamp()) + "ms later");
            }
            // 提交消费
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 启动消费者
        consumer.start();
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送_23

5.6 批量消息

批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK(集群时会细讲),而且不能是延
时消息。此外,这一批消息的总大小不应超过 4MB

  • 生产者
/**
 * @author Crazy.X
 * 批量消息-生产者  list不要超过4m
 */
/**
 * @author Crazy.X
 * 批量消息-生产者  list不要超过4m
 */
public class BatchProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();

        String topic = "BatchTest";
        // list不要超过4m
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
        try {
            producer.send(messages);
        } catch (Exception e) {
            producer.shutdown();
            e.printStackTrace();
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
  • 消费者
/**
 * @author Crazy.X
 * 批量消息-消费者 批量消费延迟较高
 */
public class BatchConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息消费者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BatchConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 订阅Topic
        consumer.subscribe("BatchTest", "*");
        //负载均衡模式消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            System.out.printf("%s Receive New Messages: %s %n",
                    Thread.currentThread().getName(), msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_消息发送_24

5.6.1 批量切分

如果消息的总长度可能大于 4MB 时,这时候最好把消息进行分割

/**
 * @author Crazy.X
 * 批量消息-超过4m-生产者
 */
public class SplitBatchProducer {

    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();

        //large batch
        String topic = "BatchTest";
        List<Message> messages = new ArrayList<>(100 * 1000);
        //10万元素的数组
        for (int i = 0; i < 100 * 1000; i++) {
            messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes()));
        }

        //把大的消息分裂成若干个小的消息(1M左右)
        ListSplitter splitter = new ListSplitter(messages);
        while (splitter.hasNext()) {
            List<Message> listItem = splitter.next();
            producer.send(listItem);
            Thread.sleep(100);
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
        System.out.printf("Consumer Started.%n");
    }

}

class ListSplitter implements Iterator<List<Message>> {
    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();
            }
            // 增加日志的开销20字节
            tmpSize = tmpSize + 20;
            //1M
            int sizeLimit = 1000 * 1000;
            if (tmpSize > sizeLimit) {
                //单个消息超过了最大的限制(1M)
                //忽略,否则会阻塞分裂的进程
                if (nextIndex - currIndex == 0) {
                    //假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
                    nextIndex++;
                }
                break;
            }
            if (tmpSize + totalSize > sizeLimit) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Not allowed to remove");
    }
}

5.7 过滤消息

注意事项: 区分tag group一定要不同的组,否则变成负载均衡状态出现数据错误

5.7.1 Tag 过滤

在大多数情况下,TAG 是一个简单而有用的设计,其可以来选择您想要的消息。举例说明:JD电商电脑商品的只消费电脑,书籍商品的只消费书籍

  • 生产者
/**
 * @author Crazy.X
 * tag过滤-生产者
 */
public class TagFilterProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("TagFilterProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        producer.start();
        // 创建两个tags
        String[] tags = new String[]{"computer", "book"};

        for (int i = 0; i < 10; i++) {
            Message msg = new Message("TagFilterTest",
                    tags[i % tags.length],
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例
        producer.shutdown();
    }
}
  • 消费者-tag computer
/**
 * @author Crazy.X
 * tag过滤computer-消费者
 */
public class TagComputerFilterConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
        // 实例化消息消费者,指定组名-tag区分的话组一定不要设置重复
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TagComputerFilterConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // tag来区分,举例说明:JD电商电脑商品的只消费电脑,书籍商品的只消费书籍
        consumer.subscribe("TagFilterTest", "computer");
        // 指定消费开始偏移量 ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET 也是默认值,可见源码
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_java_25

  • 消费者-tag book
/**
 * @author Crazy.X
 * tag过滤book-消费者
 */
public class TagBookFilterConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
        // 实例化消息消费者,指定组名-tag区分的话组一定不要设置重复
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TagBookFilterConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // tag来区分,举例说明:JD电商电脑商品的只消费电脑,书籍商品的只消费书籍
        consumer.subscribe("TagFilterTest", "book");
        // 指定消费开始偏移量 ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET 也是默认值,可见源码
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_java_26

5.7.2 Sql 过滤

注意: 开启sql过滤会影响性能(不建议使用)

sql过滤需要broker.conf配置文件添加配置

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
brokerIP1=192.168.3.200
namesrvAddr=192.168.3.200:9876
# 加入开启过滤
enablePropertyFilter=true
5.7.2.1 SQL 基本语法

RocketMQ 定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。

  1. 数值比较:比如:>>=<<=BETWEEN=
  2. 字符比较:比如:=<>IN
  3. IS NULL 或者 IS NOT NULL
  • 逻辑符号:ANDORNOT

常量支持类型为:

  1. 数值,比如:123,3.1415;
  2. 字符,比如:‘abc’,必须用单引号包裹起来;
  3. NULL,特殊的常量
  4. 布尔值,TRUEFALSE

只有推送消费者才能通过 SQL92 选择消息。界面是:

public void subscribe(final String topic, final MessageSelector messageSelector)
  • 生产者
/**
 * @author Crazy.X
 * sql过滤 -消息生产者(加入消息属性)
 */
public class SqlFilterProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("SqlFilterProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        producer.start();

        String[] tags = new String[]{"TagA", "TagB", "TagC"};

        for (int i = 0; i < 10; i++) {
            Message msg = new Message("SqlFilterTest",
                    tags[i % tags.length],
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            // 设置一些属性
            msg.putUserProperty("a", String.valueOf(i));

            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例
        producer.shutdown();
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_27

  • 消费者
/**
 * @author Crazy.X
 * sql过滤-消费者
 */
public class SqlFilterConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息消费者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("SqlFilterConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // Don't forget to set enablePropertyFilter=true in broker
        consumer.subscribe("SqlFilterTest",
                MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
                        "and (a is not null and a between 0 and 3)"));

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

rocketmq 用docker进行部署 rocketmq搭建教程_json_28

5.8 事务消息

rocketmq 用docker进行部署 rocketmq搭建教程_配置文件_29

其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

5.8.1 正常事务流程

(1) 发送消息(half 消息):图中步骤 1。

(2) 服务端响应消息写入结果:图中步骤 2。

(3) 根据发送结果执行本地事务(如果写入失败,此时 half 消息对业务不可见,本地逻辑不执行):图中步骤 3。

(4) 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见):图中步骤 4

5.8.2 事务补偿流程

(1) 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次“回查”:图中步骤 5。

(2) Producer 收到回查消息,检查回查消息对应的本地事务的状态:图中步骤 6。

(3) 根据本地事务状态,重新 Commit 或者 Rollback::图中步骤 6。

其中,补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况。

5.8.3 事务消息状态

事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction: 提交状态,它允许消费者消费此消息(完成图中了 1,2,3,4 步,第 4 步是 Commit)。
  • TransactionStatus.RollbackTransaction: 回滚状态,它代表该消息将被删除,不允许被消费(完成图中了 1,2,3,4 步, 第 4 步是 Rollback)。
  • ransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态(完成图中了 1,2,3 步, 但是没有 4 或者没有 7,无法 Commit
5.8.4 创建事务性生产者

使用 TransactionMQProducer 类创建生产者,并指定唯一的 ProducerGroup,就可以设置自定义线程池来处理这些检查请求。执行本地事务后、需
要根据执行结果对消息队列进行回复

/**
 * @author Crazy.X
 * 事务消息-消息发送方
 */
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        // 创建消息生产者
        TransactionMQProducer producer = new TransactionMQProducer("TransactionProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 创建线程池
        ExecutorService executorService = new ThreadPoolExecutor(
                2,
                5,
                100,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2000), r -> {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        });
        // 设置生产者回查线程池
        producer.setExecutorService(executorService);
        // 生产者设置监听器
        producer.setTransactionListener(transactionListener);
        // 启动消息生产者
        producer.start();
        String[] tags = new String[]{"TagA", "TagB", "TagC"};
        for (int i = 0; i < 3; i++) {
            try {
                Message msg = new Message(
                        "TransactionTopic",
                        tags[i % tags.length],
                        "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                //1,2步  半事务的发送,确认。
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);
                Thread.sleep(1000);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}
5.8.5 实现事务的监听接口

当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务(步骤 3)。它返回前一节中提到的三个事务状态之一。
checkLocalTranscation 方法用于检查本地事务状态(步骤 5),并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。

public class TransactionListenerImpl implements TransactionListener {
    // 事务状态记录
    private AtomicInteger transactionIndex = new AtomicInteger(0);
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    //执行本地事务 3
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("执行本地事务");
        int value = transactionIndex.getAndIncrement();

        //0,1,2
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);

        //这里模拟的不进行步骤4  A系统不知道的--UNKNOW
        return LocalTransactionState.UNKNOW;
    }

    //检查本地事务状态  默认是60s,一分钟检查一次
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        //打印每次回查的时间
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
        System.out.println("checkLocalTransaction:" + df.format(new Date()));// new Date()为获取当前系统时间
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【中间状态】");
                    return LocalTransactionState.UNKNOW;
                case 1:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【提交状态】");
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【回滚状态】");
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                default:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【提交状态】");
                    return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
        //  System.out.println("MQ检查消息【"+msg.getTransactionId()+"】事务状态【提交状态】");
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}
5.8.6 消息消费者
/**
 * @author Crazy.X
 * 事务消息-消费者
 */
public class TransactionalConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TransactionalConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 订阅Topic
        consumer.subscribe("TransactionTopic", "*");
        //负载均衡模式消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
5.8.7 使用场景

用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。如何保证数据
的完整性?

可以使用 RocketMQ 的分布式事务保证在下单失败后系统数据的完整性

5.8.8 使用限制
  1. 事务消息不支持延时消息和批量消息。
  2. 事务回查的间隔时间:BrokerConfig. transactionCheckInterval 通过 Broker 的配置文件设置好。3. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的
    transactionCheckMax 参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认
    情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  3. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用
    户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
  4. 事务性消息可能不止一次被检查或消费。
  5. 事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。
  6. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事
    务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
  7. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ 服务器能通过它们的生产者
    ID 查询到消费者。[toc]

一 MQ的安装

1.1 官方地址下载

使用最新的4.8的版本。

http://rocketmq.apache.org/dowloading/releases/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VPRKrBLA-1625329015934)(845B800DD1ED4CBD85B5F4A99AFB47FA)]

1.2 环境需要

  • Linux 64位系统
  • Jdk 1.8
  • 建议安装目录在/opt/建立rocketMq文件夹
  • 注:源码安装需要Maven。运行版本不需要

1.3 注意事项

假设服务的外网IP地址:192.168.56.101 虚拟机的话就是本机IP 主机和虚拟机之间需要ping通

1.3.1 需要关闭虚拟机linux防火墙

互相ping不同 未关闭防火墙 可能导致连接失败,无法生产消息

systemctl stop firewalld
1.3.2 主机防火墙

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QEziWjBL-1625329015935)(EAD5477012344AFEA89AFC7C2D22E2E2)]

1.4 配置项

RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小。

但是这个也仅仅是在测试环境中,RocketMQ在生产上最低要求至少8G内存(官方推荐)才能确保RocketMQ的效果

编辑 runbroker.sh 和 runserver.sh修改默认JVM大小(windows上对应cmd文件)

1.4.1 broker 配置

/bin目录下

vim runbroker.sh

JAVA_OPT="${JAVA_OPT} -server -Xms1024m -Xmx1024m -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
1.4.2 nameServer 配置

/bin目录下

vim runserver.sh

JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
choose_gc_options

1.5 启动 NameServer

# 进入mq bin目录
cd /opt/bin

# 启动nameserver
nohup sh mqnamesrv &

# 查看日志 会在~目录下有logs 日志路径可修改
tail -200f ~/logs/rocketmqlogs/namesrv.log
1.5.1 NameServer 日志
2021-07-03 03:16:42 INFO main - rocketmqHome=/opt/rocketmq
2021-07-03 03:16:42 INFO main - kvConfigPath=/root/namesrv/kvConfig.json
2021-07-03 03:16:42 INFO main - configStorePath=/root/namesrv/namesrv.properties
2021-07-03 03:16:42 INFO main - productEnvName=center
2021-07-03 03:16:42 INFO main - clusterTest=false
2021-07-03 03:16:42 INFO main - orderMessageEnable=false
2021-07-03 03:16:42 INFO main - listenPort=9876
2021-07-03 03:16:42 INFO main - serverWorkerThreads=8
2021-07-03 03:16:42 INFO main - serverCallbackExecutorThreads=0
2021-07-03 03:16:42 INFO main - serverSelectorThreads=3
2021-07-03 03:16:42 INFO main - serverOnewaySemaphoreValue=256
2021-07-03 03:16:42 INFO main - serverAsyncSemaphoreValue=64
2021-07-03 03:16:42 INFO main - serverChannelMaxIdleTimeSeconds=120
2021-07-03 03:16:42 INFO main - serverSocketSndBufSize=65535
2021-07-03 03:16:42 INFO main - serverSocketRcvBufSize=65535
2021-07-03 03:16:42 INFO main - serverPooledByteBufAllocatorEnable=true
2021-07-03 03:16:42 INFO main - useEpollNativeSelector=false
2021-07-03 03:16:42 INFO main - Server is running in TLS permissive mode
2021-07-03 03:16:42 INFO main - Tls config file doesn't exist, skip it
2021-07-03 03:16:42 INFO main - Log the final used tls related configuration
2021-07-03 03:16:42 INFO main - tls.test.mode.enable = true
2021-07-03 03:16:42 INFO main - tls.server.need.client.auth = none
2021-07-03 03:16:42 INFO main - tls.server.keyPath = null
2021-07-03 03:16:42 INFO main - tls.server.keyPassword = null
2021-07-03 03:16:42 INFO main - tls.server.certPath = null
2021-07-03 03:16:42 INFO main - tls.server.authClient = false
2021-07-03 03:16:42 INFO main - tls.server.trustCertPath = null
2021-07-03 03:16:42 INFO main - tls.client.keyPath = null
2021-07-03 03:16:42 INFO main - tls.client.keyPassword = null
2021-07-03 03:16:42 INFO main - tls.client.certPath = null
2021-07-03 03:16:42 INFO main - tls.client.authServer = false
2021-07-03 03:16:42 INFO main - tls.client.trustCertPath = null
2021-07-03 03:16:42 INFO main - Using OpenSSL provider
2021-07-03 03:16:43 INFO main - SSLContext created for server
2021-07-03 03:16:43 INFO main - Try to start service thread:FileWatchService started:false lastThread:null
2021-07-03 03:16:43 INFO NettyEventExecutor - NettyEventExecutor service started

# 容器已经启动
2021-07-03 03:16:43 INFO main - The Name Server boot success. serializeType=JSON
2021-07-03 03:16:43 INFO FileWatchService - FileWatchService service started
2021-07-03 03:17:43 INFO NSScheduledThread1 - --------------------------------------------------------
2021-07-03 03:17:43 INFO NSScheduledThread1 - configTable SIZE: 0

1.6 启动 Broker

修改配置文件增加外网地址(你启动加载哪个配置文件就修改哪个,这里修改broker.conf)

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 公网ip 虚拟机的话就是虚拟机机ip 本机可以ping通 
brokerIP1=192.168.3.200
# nameserver地址
namesrvAddr=192.168.3.200:9876
# 进入mq bin目录
cd /opt/bin

# 启动broker -n 指定namever地址 conf文件配置后可以不指定,通过配置文件配置
# autoCreateTopicEnable=true 这样启动的服务器可以自动创建主题(客户端),不过生产一般不推荐。
nohup sh mqbroker -c ../conf/broker.conf -n 192.168.3.200:9876 autoCreateTopicEnable=true &

# 查看日志 会在~目录下有logs 日志路径可修改
tail -200f ~/logs/rocketmqlogs/broker.log
  • 可能发生的问题
# /root/store/ 没有commitlog文件夹 创建即可
2021-07-03 03:27:38 ERROR DiskCheckScheduledThread1 - Error when measuring disk space usage, file doesn't exist on this path: /root/store/commitlog

# /root/store/ 没有consumequeue文件夹 创建即可
Error when measuring disk space usage, file doesn't exist on this path: /root/store/consumequeue
1.6.1 Broker 日志
[topicName=SCHEDULE_TOPIC_XXXX, readQueueNums=18, writeQueueNums=18, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=RMQ_SYS_TRANS_HALF_TOPIC, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=DefaultCluster_REPLY_TOPIC, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=BenchmarkTest, readQueueNums=1024, writeQueueNums=1024, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=OFFSET_MOVED_EVENT, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=broker-a, readQueueNums=1, writeQueueNums=1, perm=RWX, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=TBW102, readQueueNums=8, writeQueueNums=8, perm=RWX, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=SELF_TEST_TOPIC, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load exist local topic, TopicConfig [topicName=DefaultCluster, readQueueNums=16, writeQueueNums=16, perm=RWX, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
2021-07-03 03:29:51 INFO main - load /root/store/config/topics.json OK
2021-07-03 03:29:51 INFO main - load /root/store/config/consumerOffset.json OK
2021-07-03 03:29:51 INFO main - load /root/store/config/consumerFilter.json OK
2021-07-03 03:29:51 INFO main - Try to start service thread:AllocateMappedFileService started:false lastThread:null
2021-07-03 03:29:51 INFO main - load /root/store/config/delayOffset.json OK
2021-07-03 03:29:52 INFO main - Set user specified name server address: 192.168.3.200:9876
2021-07-03 03:29:52 WARN main - Load default transaction message hook service: TransactionalMessageServiceImpl
2021-07-03 03:29:52 WARN main - Load default discard message hook service: DefaultTransactionalMessageCheckListener
2021-07-03 03:29:52 INFO main - The broker dose not enable acl
2021-07-03 03:29:52 INFO main - Try to start service thread:ReputMessageService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:AcceptSocketService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:GroupTransferService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:HAClient started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:FlushConsumeQueueService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:FlushRealTimeService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:StoreStatsService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:FileWatchService started:false lastThread:null
2021-07-03 03:29:52 INFO main - Try to start service thread:PullRequestHoldService started:false lastThread:null
2021-07-03 03:29:52 INFO FileWatchService - FileWatchService service started
2021-07-03 03:29:52 INFO PullRequestHoldService - PullRequestHoldService service started
2021-07-03 03:29:52 INFO main - Try to start service thread:TransactionalMessageCheckService started:false lastThread:null

# 注册成功
2021-07-03 03:29:52 INFO brokerOutApi_thread_1 - register broker[0]to name server 192.168.3.200:9876 OK

# 启动成功
2021-07-03 03:29:52 INFO main - The broker[broker-a, 192.168.3.200:10911] boot success. serializeType=JSON and name server is 192.168.3.200:9876
2021-07-03 03:30:02 INFO BrokerControllerScheduledThread1 - dispatch behind commit log 0 bytes
2021-07-03 03:30:02 INFO BrokerControllerScheduledThread1 - Slave fall behind master: 0 bytes
# nameserver 日志会出现 新broker 注册
2021-07-03 03:29:52 INFO RemotingExecutorThread_4 - new broker registered, 192.168.3.200:10911 HAServer: 172.17.0.1:10912

1.7 安装可视化 rocketmq-console

1.7.1 下载 rocketmq-console

运行前确保:已经有jdk1.8,Maven(打包需要安装Maven 3.2.x)

下载:https://codeload.github.com/apache/rocketmq-externals/zip/master

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9sbOoIc6-1625329015936)(18778333602946D18F3A534BD7A3375D)]

这个包主要包含的是Message Connector,具体详情见 https://rocketmq-1.gitbook.io/rocketmq-connector/

里面后端管理界面是:rocketmq-console

1.7.2 编辑 rocketmq-console 配置文件

下载完成之后,进入‘\rocketmq-console\src\main\resources’文件夹,打开‘application.properties’进行配置。

# 服务器地址
server.address=192.168.3.200
# 控制台端口
server.port=8089

### SSL setting
#server.ssl.key-store=classpath:rmqcngkeystore.jks
#server.ssl.key-store-password=rocketmq
#server.ssl.keyStoreType=PKCS12
#server.ssl.keyAlias=rmqcngkey

#spring.application.index=true
spring.application.name=rocketmq-console
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
logging.level.root=INFO
logging.config=classpath:logback.xml
#if this value is empty,use env value rocketmq.config.namesrvAddr  NAMESRV_ADDR | now, you can set it in ops page.default localhost:9876

# nameserver地址 默认端口9876
rocketmq.config.namesrvAddr=192.168.3.200:9876
#if you use rocketmq version < 3.5.8, rocketmq.config.isVIPChannel should be false.default true
rocketmq.config.isVIPChannel=
#rocketmq-console's data path:dashboard/monitor
rocketmq.config.dataPath=/tmp/rocketmq-console/data
#set it false if you don't want use dashboard.default true
rocketmq.config.enableDashBoardCollect=true
#set the message track trace topic if you don't want use the default one
rocketmq.config.msgTrackTopicName=
rocketmq.config.ticketKey=ticket

#Must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required
rocketmq.config.loginRequired=false

#set the accessKey and secretKey if you used acl
#rocketmq.config.accessKey=
#rocketmq.config.secretKey=

进入\rocketmq-externals\rocketmq-console文件夹

执行mvn clean package -Dmaven.test.skip=true 编译生成可执行jar包

建议放入/opt/建立文件夹

1.7.3 启动 rocketmq-console
# 启动可视化 可指定内存大小
nohup java -jar -Xms128m -Xmx256m rocketmq-console-ng-2.0.0.jar &
1.7.4 rocketmq-console 日志
.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.2.RELEASE)

[2021-07-03 03:41:10.868]  INFO Starting App v2.0.0 on localhost with PID 29113 (/opt/rocketmq-console/rocketmq-console-ng-2.0.0.jar started by root in /opt/rocketmq-console)
[2021-07-03 03:41:10.869]  INFO No active profile set, falling back to default profiles: default
[2021-07-03 03:41:13.198]  INFO setNameSrvAddrByProperty nameSrvAddr=192.168.3.200:9876
[2021-07-03 03:41:13.694]  INFO Tomcat initialized with port(s): 8089 (http)
[2021-07-03 03:41:13.708]  INFO Initializing ProtocolHandler ["http-nio-192.168.3.200-8089"]
[2021-07-03 03:41:13.709]  INFO Starting service [Tomcat]
[2021-07-03 03:41:13.709]  INFO Starting Servlet engine: [Apache Tomcat/9.0.29]
[2021-07-03 03:41:13.908]  INFO Initializing Spring embedded WebApplicationContext
[2021-07-03 03:41:13.909]  INFO Root WebApplicationContext: initialization completed in 2811 ms
[2021-07-03 03:41:15.166]  INFO Initializing ExecutorService 'applicationTaskExecutor'
[2021-07-03 03:41:15.350]  INFO Adding welcome page: class path resource [static/index.html]
[2021-07-03 03:41:15.563]  INFO Initializing ExecutorService 'taskScheduler'
[2021-07-03 03:41:15.584]  INFO Exposing 2 endpoint(s) beneath base path '/actuator'
[2021-07-03 03:41:15.656]  INFO Starting ProtocolHandler ["http-nio-192.168.3.200-8089"]
[2021-07-03 03:41:15.692]  INFO Tomcat started on port(s): 8089 (http) with context path ''
[2021-07-03 03:41:15.697]  INFO Started App in 5.735 seconds (JVM running for 6.688)
1.7.3 访问可视化

浏览器输入 192.168.3.200:8089 查看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-beesUikZ-1625329015937)(32DC3C51C2384D34AA41FCAA1AF08F19)]

二 基础入门

有需要其他介绍请参阅:
rocketMQ官方文档

2.1 MQ的定义

其实并没有标准定义。一般认为,消息中间件属于分布式系统中一个子系统,关注于数据的发送和接收,利用高效可靠的异步消息传递机制对分布
式系统中的其余各个子系统进行集成。

  1. 高效:对于消息的处理处理速度快。
  2. 可靠:一般消息中间件都会有消息持久化机制和其他的机制确保消息不丢失。
  3. 异步:指发送完一个请求,不需要等待返回,随时可以再发送下一个请求,既不需要等待
  4. 总结我们消息中间件不生产消息,只是消息的搬运工。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3r97nWFx-1625329015937)(E2C045BFC5BF41E2B679BFD63D740350)]

2.2 为什么要用消息中间件

2.2.1 应用解耦

系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者
因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验

使用消息中间件,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队
列中,用户的下单操作正常完成。当物流系统恢复后,继续处理存放在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BR6aCFHZ-1625329015938)(F473FF79760A44748D7B2D334005CB38)]

2.2.2 流量削峰

应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大
提到系统的稳定性和用户体验。

互联网公司的大促场景(双十一、店庆活动、秒杀活动)都会使用到 MQ

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Wv8EOCX-1625329015939)(D1D8FA759EC447E690620A81F51D47C5)]

2.2.3 数据分发

通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消
息队列中直接获取数据即可。

接口调用的弊端,无论是新增系统,还是移除系统,代码改造工作量都很大。

使用 MQ 做数据分发好处,无论是新增系统,还是移除系统,代码改造工作量较小。

所以使用 MQ 做数据的分发,可以提高团队开发的效率

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90s6dGmd-1625329015940)(D9C3CFC2718D41449DEB5E6ACB48D3BE)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLiAK4q2-1625329015940)(0B8C59349BFE4307BFB1C0E9FDD95C22)]

2.3 RocketMQ 产品发展

2.3.1 RocketMQ 版本发展

Metaq1.x 是 RocketMQ 前身的第一个版本,本质上把 Kafka 做了一次 java 版本的重写(Kafka 是 sacla)

Meta2.x,主要是对存储部分进行了优化,因为 kafka 的数据存储,它的 paration 是一个全量的复制,在阿里、在淘宝的这种海量交易。Kafka 这种
机制的横向拓展是非常不好的。2012 年阿里同时把 Meta2.0 从阿里内部开源出来,取名 RocketMQ,同时为了命名上的规范(版本上延续),所以这个就
是 RocketMQ3.0

现在 RocketMQ 主要维护的是 4.x 的版本,也是大家使用得最多的版本,2017 年从 Apache 顶级项目毕

2.3.2 阿里内部项目的使用

那么在阿里公司内部,原则上遵守开源共建原则。RocketMQ 项目只维护核心功能,且去除了所有其他运行时依赖,核心功能最简化。每个 BU
( Business Unit 业务单元)的个性化需求都在 RocketMQ 项目之上进行深度定制。RocketMQ 向其他 BU 提供的仅仅是 Jar 包,例如要定制一个 Broker,
那么只需要依赖 rocketmq-broker 这 jar 包即可,可通过 API 进行交互, 如果定制 client,则依赖 rocketmq-client 这个 jar 包,对其提供的 api 进行
再封装

在 RocketMQ 项目基础上几个常用的项目如

  • com.taobao.metaq v3.0 = RocketMQ + 淘宝个性化需求

为淘宝应用提供消息服务

  • com.alipay.zpullmsg v1.0 = RocketMQ + 支付宝个性化需求

为支付宝应用提供消息服务

  • com.alibaba.commonmq v1.0 = Notify + RocketMQ + B2B 个性化需求

为 B2B 应用提供消息服务

2.3.3 展望未来

从阿里负责 RocketMQ 的架构核心人员的信息来看,阿里内部一直全力拓展 RocketMQ

2017 年 10 月份,OpenMessaging 项目由阿里巴巴发起,与雅虎、滴滴出行、Streamlio 公司共同参与创立, 项目意在创立厂商无关、平台无关的分布
式消息及流处理领域的应用开发标准。同时 OpenMessaging 入驻 Linux 基金会

OpenMessaging 项目已经开始在 Apache RocketMQ 中率先落地,并推广至整个阿里云平台.

另外 RocketMQ5 的版本也在内部推进,主要的方向是 Cloud Native(云原生)

另外 Apache RocketMQ 的商业版本,Aliware MQ 在微服务、流计算、IoT、异步解耦、数据同步等场景有非常广泛的运用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Gm54gNR-1625329015940)(9433EF6FA9124FA192DDB36C3543CC34)]

三 RocketMQ 的物理架构

消息队列 RocketMQ 是阿里巴巴集团基于高可用分布式集群技术,自主研发的云正式商用的专业消息中间件,既可为分布式应用系统提供异步解耦
和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性,是阿里巴巴双 11 使用的核心产品。

RocketMQ 的设计基于主题的发布与订阅模式,其核心功能包括消息发送、消息存储(Broker)、消息消费,整体设计追求简单与性能第一。

3.1 核心概念

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cR21OKf-1625329015941)(1A4F5708E8CA42DEB55FC369366EF867)]

3.1.1 NameServer

NameServer 是整个 RocketMQ 的“大脑”,它是 RocketMQ 的服务注册中心,所以 RocketMQ 需要先启动 NameServer 再启动 Rocket 中的 Broker。

Broker 在启动时向所有 NameServer 注册(主要是服务器地址等),生产者在发送消息之前先从 NameServer 获取 Broker 服务器地址列表(消费者一
样),然后根据负载均衡算法从列表中选择一台服务器进行消息发送。

NameServer 与每台 Broker 服务保持长连接,并间隔 30s 检查 Broker 是否存活,如果检测到 Broker 宕机,则从路由注册表中将其移除。这样就可以实
现 RocketMQ 的高可用。具体细节后续讲解

3.1.2 生产者(Producer)

生产者:也称为消息发布者,负责生产并发送消息至 RocketMQ。

3.1.3 消费者(Consumer)

消费者:也称为消息订阅者,负责从 RocketMQ 接收并消费消息。

3.1.4 消息(Message)

消息:生产或消费的数据,对于 RocketMQ 来说,消息就是字节数组。

3.1.5 主机(Broker)

RocketMQ 的核心,用于暂存和传输消息。

3.2 物理架构中的整体运转

  1. NameServer 先启动
  2. Broker 启动时向 NameServer 注册
  3. 生产者在发送某个主题的消息之前先从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),然后根据负载均衡算法从列表中选择一台
    Broker 进行消息发送。
  4. NameServer 与每台 Broker 服务器保持长连接,并间隔 30S 检测 Broker 是否存活,如果检测到 Broker 宕机(使用心跳机制,如果检测超过120S),则从路由注册表中将其移除。
  5. 消费者在订阅某个主题的消息之前从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),但是消费者选择从 Broker 中订阅消息,订阅
    规则由 Broker 配置决定

四 RocketMQ 的概念模型

4.1 核心概念

4.1.1 分组(Group)

生产者:标识发送同一类消息的Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息
(事务消息中如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其它producer,
确认这条消息应该 commit 还是 rollback)

消费者:标识一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致。同一个 Consumer Group 下的各个实例将共同消费 topic
的消息,起到负载均衡的作用。

消费进度以 Consumer Group 为粒度管理,不同 Consumer Group 之间消费进度彼此不受影响,即消息 A 被 Consumer Group1 消费过,也会再给 Consumer
Group2 消费。

4.1.2 主题(Topic)

标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定 Topic。

区分消息的种类;一个发送者可以发送消息给一个或者多个 Topic
一个消息的接收者可以订阅一个或者多个 Topic 消息

4.1.3 标签(Tag)

RocketMQ 支持给在发送的时候给 topic 打 tag,同一个 topic 的消息虽然逻辑管理是一样的。但是消费 topic1 的时候,如果你消费订阅的时候指定的
是 tagA,那么 tagB 的消息将不会投递。

4.1.4 消息队列(Message Queue)

简称 Queue 或 Q。消息物理管理单位。一个 Topic 将有若干个 Q。若一个 Topic 创建在不同的 Broker,则不同的 broker 上都有若干 Q,消息将物理地
存储落在不同 Broker 结点上,具有水平扩展的能力。
无论生产者还是消费者,实际的生产和消费都是针对 Q 级别。例如 Producer 发送消息的时候,会预先选择(默认轮询)好该 Topic 下面的某一条 Q
发送;Consumer 消费的时候也会负载均衡地分配若干个 Q,只拉取对应 Q 的消息。
每一条 message queue 均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log)
恢复回来。

4.1.5 偏移量(Offset)

RocketMQ 中,有很多 offset 的概念。一般我们只关心暴露到客户端的 offset。不指定的话,就是指 Message Queue 下面的 offset。
Message queue 是无限长的数组。一条消息进来下标就会涨 1,而这个数组的下标就是 offset,Message queue 中的 max offset 表示消息的最大 offset
Consumer offset 可以理解为标记 Consumer Group 在一条逻辑 Message Queue 上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的
最新消费的消息 offset+1,即实际上表示的是下次拉取的 offset

五 玩转各种消息

5.1 普通消息

  • 导入MQ客户端依赖
<dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.8.0</version>
    </dependency>
  • 消息发送步骤
  1. 创建消息生产者 producer,并指定生产者组名
  2. 指定 Nameserver 地址
  3. 启动 producer
  4. 创建消息对象,指定 Topic、Tag 和消息体
  5. 发送消息
  6. 关闭生产者 producer
  • 消息消费者步骤
  1. 创建消费者 Consumer,指定消费者组名
  2. 指定 Nameserver 地址
  3. 订阅主题 Topic 和 Tag
  4. 设置回调函数,处理消息
  5. 启动消费者 consumer
5.1.1 消息发送
5.1.1.1 发送同步消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pxDwclSm-1625329015942)(2B8B547B948B43FEA8CA47F3B84DE891)]

这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

同步发送是指消息发送方发出数据后,同步等待,直到收到接收方发回响应之后才发下一个请求。

/**
 * @author Crazy.X
 * 同步发送
 */
public class SyncProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        for (int i = 0; i < 100; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            // 发送消息到一个Broker
            SendResult sendResult = producer.send(msg);
            // 通过sendResult返回消息是否成功送达
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2WiwcVd-1625329015942)(2032ED4B864C4A3789D4C1ABBDE17438)]

  • Message ID

消息的全局唯一标识(内部机制的 ID 生成是使用机器 IP 和消息偏移量的组成,所以有可能重复,如果是幂等性还是最好考虑 Key),由消息队列 MQ
系统自动生成,唯一标识某条消息。

  • SendStatus

发送的标识。成功,失败等

  • Queue

相当于是 Topic 的分区;用于并行发送和接收消息

5.1.1.2 发送异步消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyOAviN7-1625329015942)(D75A128AD2FF48B999B1342EED9FBA83)]

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 Broker的响应

消息发送方在发送了一条消息后,不等接收方发回响应,接着进行第二条消息发送。发送方通过回调接口的方式接收服务器响应,并对响应结果进行处理

/**
 * @author Crazy.X
 * 异步发送
 */
public class AsyncProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        // 设置发送异步失败时的重试次数
        producer.setRetryTimesWhenSendAsyncFailed(0);
        //启用Broker故障延迟机制
        producer.setSendLatencyFaultEnable(true);

        for (int i = 0; i < 100; i++) {
            final int index = i;
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest",
                    "TagA",
                    "OrderID888",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
            // SendCallback接收异步返回结果的回调
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
        }
        Thread.sleep(10000);
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RjvurzRo-1625329015943)(83FF9F26D9204545B40CEE188EA534D8)]

5.1.1.3 单向发送

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDbhth9g-1625329015943)(70080B7E8E7E44C9B617DF392E9208CF)]

这种方式主要用在不特别关心发送结果的场景,例如日志发送

单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

/**
 * @author Crazy.X
 * 单向发送
 */
public class OneWayProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        for (int i = 0; i < 100; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            // 发送单向消息,没有任何返回结果
            producer.sendOneway(msg);

        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
5.1.1.4 消息发送的权衡

发送方式

发送TPS

发送结果反馈

可靠性

适用场景

同步可靠发送



不丢失

重要通知邮件、报名短信通知、营销短信系统等

异步可靠发送



不丢失

用户视频上传后通知启动转码服务、转码完成后通知推送转码结果等

单向发送



可能丢失

适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集

5.1.2 消息消费
5.1.2.1 集群消费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WrMAWPp-1625329015944)(65B5C45B0AB94BCD9A9E128AD7E594E4)]

消费者的一种消费模式。一个 Consumer Group 中的各个 Consumer 实例分摊去消费消息,即一条消息只会投递到一个 Consumer Group 下面的一个实例。

实际上,每个 Consumer 是平均分摊 Message Queue 的去做拉取消费。例如某个 Topic 有 3 条 Q,其中一个 Consumer Group有3个实例(可能是3个进程,或者3台机器),那么每个实例只消费其中的 1 条 Q。

而由 Producer 发送消息的时候是轮询所有的 Q,所以消息会平均散落在不同的 Q 上,可以认为 Q 上的消息是平均的。那么实例也就平均地消费消息了。
这种模式下,消费进度(Consumer Offset)的存储会持久化到 Broker

/**
 * @author Crazy.X
 * 推模式集群消费
 */
public class BalanceConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息消费者,指定组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 指定NameServer地址信息
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 设置最大重新消费次数
        consumer.setMaxReconsumeTimes(1);
        // 订阅Topic subExpression 对tag进行指定进行过滤 也可以正则表达式 TagA|TagB
        consumer.subscribe("TopicTest", "*");
        // 负载均衡模式集群消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                try {
                    for (MessageExt msg : msgs) {
                        String topic = msg.getTopic();
                        String msgBody = new String(msg.getBody());
                        String tags = msg.getTags();
                        System.out.println("收到消息: " + "topic:" + topic + ",tags:" + tags + ",msgBody:" + msgBody);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    // 消费失败,认为没有消费,可能会重复发送
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                // 未出现异常进行提交 成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7a3qL6i-1625329015944)(7A434D20B12E4988A495B1819B796146)]

5.1.2.2 广播消费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JraRXBGk-1625329015945)(D9FA0964CAAA4CF9AC1DF851645A4BE7)]

消费者的一种消费模式。消息将对一个 Consumer Group 下的各个 Consumer 实例都投递一遍。即即使这些 Consumer 属于同一个 Consumer Group,
消息也会被 Consumer Group 中的每个 Consumer 都消费一次。

实际上,是一个消费组下的每个消费者实例都获取到了 topic 下面的每个 Message Queue 去拉取消费。所以消息会投递到每个消费者实例。
这种模式下,消费进度(Consumer Offset)会存储持久化到实例本地

/**
 * @author Crazy.X
 * 广播模式消费
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        /// 实例化消息消费者,指定组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 指定NameServer地址信息
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 设置最大重新消费次数
        consumer.setMaxReconsumeTimes(1);
        // 订阅Topic subExpression 对tag进行指定进行过滤 也可以正则表达式 TagA|TagB
        consumer.subscribe("TopicTest", "*");
        //广播模式消费
        consumer.setMessageModel(MessageModel.BROADCASTING);

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            // 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,
            // 但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
5.1.2.3 消息消费时的权衡

集群模式:适用场景&注意事项

  1. 消费端集群化部署,每条消息只需要被处理一次。
  2. 由于消费进度在服务端维护,可靠性更高。
  3. 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
  4. 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。

广播模式:适用场景&注意事项

  1. 广播消费模式下不支持顺序消息。
  2. 广播消费模式下不支持重置消费位点。
  3. 每条消息都需要被相同逻辑的多台机器处理。
  4. 消费进度在客户端维护,出现重复的概率稍大于集群模式。
  5. 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
  6. 广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
  7. 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
  8. 目前仅 Java 客户端支持广播模式。
  9. 广播模式下服务端不维护消费进度,所以消息队列RocketMQ控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

5.2 顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ 可以严格的保证消息有序,可以分为分区有序或者全局有序。

顺序消费的原理解析,在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列);而消费消息的时候从多个 queue
上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉
取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个 queue 参与,则为分区有序,即相对每个 queue,消息都是有序的。

  • 全局顺序消息
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDGwSZvq-1625329015945)(8EF91CFF098A4C4FA4AE63F5FD25E43C)]
    全局顺序只创建一个queqe 即可全局顺序 不做记录
  • 部分顺序消息
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZc11GDt-1625329015946)(81E4B776AFDB4B67AB84C236497DCC34)]
5.2.1 顺序消息生产

一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,下面是订单进行分区有序的示例代码。

/**
 * @author Crazy.X
 * 部分顺序消息生产
 */
public class ProducerInOrder {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("OrderProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();

        // tags数组 使用tag区分
        String[] tags = new String[]{"TagA", "TagC", "TagD"};
        // 订单列表 模拟订单数据
        List<Order> orderList = new ProducerInOrder().buildOrders();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(date);

        // 创建消息,并指定Topic,Tag和消息体
        for (int i = 0; i < orderList.size(); i++) {
            // 加个时间前缀
            String body = dateStr + " Order:" + orderList.get(i);
            // 指定tags组
            Message msg = new Message("PartOrder", tags[i % tags.length], "KEY" + i, body.getBytes());

            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    //根据订单id选择发送queue
                    Long id = (Long) arg;
                    long index = id % mqs.size();
                    // 返回
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//订单id

            System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
                    sendResult.getSendStatus(),
                    sendResult.getMessageQueue().getQueueId(),
                    body));
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }

    /**
     * 订单
     */
    private static class Order {
        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 "Order{" +
                    "orderId=" + orderId +
                    ", desc='" + desc + '\'' +
                    '}';
        }
    }

    /**
     * 生成模拟订单数据
     */
    private List<Order> buildOrders() {
        List<Order> orderList = new ArrayList<Order>();

        Order orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406002L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406003L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new Order();
        orderDemo.setOrderId(20210406001L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        return orderList;
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzxLjOi2-1625329015947)(494F4EF9B5884E1C911324A04E9C0140)]

5.2.2 顺序消息消费

消费时,同一个 OrderId 获取到的肯定是同一个队列。从而确保一个订单中处理的顺序。

/**
 * @author Crazy.X
 * 部分顺序消息消费
 */
public class ConsumerInOrder {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderConsumer");
        consumer.setNamesrvAddr("192.168.3.200:9876");

        // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
        // 如果非第一次启动,那么按照上次消费的位置继续消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("PartOrder", "TagA || TagC || TagD");
        consumer.registerMessageListener(new MessageListenerOrderly() {
            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() +
                            "queueId=" + msg.getQueueId() +
                            ", content:" + new String(msg.getBody()));
                }
                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.out.println("Consumer Started.");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zvejeszb-1625329015947)(0607824793684878AED71186FBE084C6)]

5.3 消息发送时的重要方法/属性(工作参考使用)

属性与方法是整个类,可以放到一个类下观看

5.3.1 属性
//  producerGroup:生产者所属组(针对 事务消息 高可用)
        DefaultMQProducer producer = new DefaultMQProducer("produce_details");
        //  默认主题在每一个Broker队列数量(对于新创建主题有效)
        producer.setDefaultTopicQueueNums(8);
        //  发送消息默认超时时间,默认3s (3000ms)
        producer.setSendMsgTimeout(1000 * 3);
        //  消息体超过该值则启用压缩,默认4k
        producer.setCompressMsgBodyOverHowmuch(1024 * 4);
        //  同步方式发送消息重试次数,默认为2,总共执行3次
        producer.setRetryTimesWhenSendFailed(2);
        //  异步方式发送消息重试次数,默认为2,总共执行3次
        producer.setRetryTimesWhenSendAsyncFailed(2);
        //  消息重试时选择另外一个Broker时(消息没有存储成功是否发送到另外一个broker),默认为false
        producer.setRetryAnotherBrokerWhenNotStoreOK(false);
        //  允许发送的最大消息长度,默认为4M
        producer.setMaxMessageSize(1024 * 1024 * 4);

        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
5.3.2 方法
// 启动Producer实例
        producer.start();
        
        // 如果不再发送消息,关闭Producer实例。(这行代码要放在最后)
        producer.shutdown();
        
        // 查找该主题下所有消息队列
        List<MessageQueue> MessageQueue = producer.fetchPublishMessageQueues("TopicTest");
        for (MessageQueue queue : MessageQueue) {
            System.out.println(queue.getQueueId());
        }
5.3.2.1 单向发送
for (int i = 0; i < 10; i++) {
            final int index = i;
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message(
                    "TopicTest",
                    "TagA",
                    "OrderID888",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)
            );

            //  ------------  单向发送  ------------
            //  1.1发送单向消息
            producer.sendOneway(msg);
            //  1.2指定队列单向发送消息(使用select方法)
            producer.sendOneway(msg, (mqs, msg1, arg) -> mqs.get(0), null);
            //  1.3指定队列单向发送消息(根据之前查找出来的主题)
            producer.sendOneway(msg, MessageQueue.get(0));
5.3.3.3 同步发送
//  2.1同步发送消息
            SendResult sendResult0 = producer.send(msg);
            //  2.1同步超时发送消息(属性设置:sendMsgTimeout 发送消息默认超时时间,默认3s (3000ms) )
            SendResult sendResult1 = producer.send(msg, 1000 * 3);
            //  2.2指定队列同步发送消息(使用select方法)
            SendResult sendResult2 = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    return mqs.get(0);
                }
            }, null);
            //  2.3指定队列同步发送消息(根据之前查找出来的主题队列信息)
            SendResult sendResult3 = producer.send(msg, MessageQueue.get(0));
5.3.3.2 异步发送
//  3.1异步发送消息
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
            //  3.1异步超时发送消息
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            }, 1000 * 3);
            //  3.2选择指定队列异步发送消息(根据之前查找出来的主题队列信息)
            producer.send(msg, MessageQueue.get(0),
                    new SendCallback() {
                        @Override
                        public void onSuccess(SendResult sendResult) {
                            System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                        }

                        @Override
                        public void onException(Throwable e) {
                            System.out.printf("%-10d Exception %s %n", index, e);
                            e.printStackTrace();
                        }
                    });
            //  3.3选择指定队列异步发送消息(使用select方法)
            producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            return mqs.get(0);
                        }
                    },
                    new SendCallback() {
                        @Override
                        public void onSuccess(SendResult sendResult) {
                            System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                        }

                        @Override
                        public void onException(Throwable e) {
                            System.out.printf("%-10d Exception %s %n", index, e);
                            e.printStackTrace();
                        }
                    });
        }

5.4 消息消费时的重要方法/属性(工作参考使用)

属性与方法是整个类,可以放到一个类下观看

5.4.1 属性
// consumerGroup:消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("Faker");
        // 指定NameServer地址信息.
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 消息消费模式(默认集群消费)
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 指定消费开始偏移量(上次消费偏移量、最大偏移量、最小偏移量、启动时间戳)开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        // 消费者最小线程数量(默认20)
        consumer.setConsumeThreadMin(20);
        // 消费者最大线程数量(默认20)
        consumer.setConsumeThreadMax(20);
        // 推模式下任务间隔时间(推模式也是基于不断的轮训拉取的封装)
        consumer.setPullInterval(0);
        // 推模式下任务拉取的条数,默认32条(一批批拉)
        consumer.setPullBatchSize(32);
        // 消息重试次数,-1代表16次 (超过 次数成为死信消息)
        consumer.setMaxReconsumeTimes(-1);
        // 消息消费超时时间(消息可能阻塞正在使用的线程的最大时间:以分钟为单位)
        consumer.setConsumeTimeout(15);
5.4.2 方法
// 方法-订阅
        // 基于主题订阅消息,消息过滤使用表达式
        consumer.subscribe("TopicTest", "*"); //tag  tagA||TagB||TagC
        // 基于主题订阅消息,消息过滤使用表达式
        consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3"));
        // 基于主题订阅消息,消息过滤使用表达式
        consumer.subscribe("TopicTest", MessageSelector.byTag("tagA||TagB"));
        // 取消消息订阅
        consumer.unsubscribe("TopicTest");

        // 注册监听器
        // 注册并发事件监听器
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                //没有成功  -- 到重试队列中来
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            //  // 未出现异常进行提交 成功
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            //todo
        });

        // 注册顺序消息事件监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
                } catch (Exception e) {
                    e.printStackTrace();
                    // 这个点要注意:意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
5.4.3 消费确认(ACK)
  1. 业务实现消费回调的时候,当且仅当此回调函数返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ 才会认为这批消息(默认是 1 条)
    是消费完成的。中途断电,抛出异常等都不会认为成功——即都会重新投递。
  2. 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ 就会认为这批消息消费失败了。
  3. 如果业务的回调没有处理好而抛出异常,会认为是消费失败 ConsumeConcurrentlyStatus.RECONSUME_LATER 处理。
  4. 为了保证消息是肯定被至少消费成功一次,RocketMQ 会把这批消息重发回 Broker(topic 不是原 topic 而是这个消费组的 RETRY topic),在延迟的某
    个时间点(默认是 10 秒,业务可设置)后,再次投递到这个 ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认 16 次),就会投
    递到 DLQ 死信队列。应用可以监控死信队列来做人工干预。
  5. 另外如果使用顺序消费的回调 MessageListenerOrderly 时,由于顺序消费是要前者消费成功才能继续消费,所以没有 RECONSUME_LATER 的这个状态,
    只有 SUSPEND_CURRENT_QUEUE_A_MOMENT 来暂停队列的其余消费,直到原消息不断重试成功为止才能继续消

5.5 延时消息

5.5.1 概念介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Pstlr4L-1625329015948)(19AF7E0B6C8645B691DFAE0DA985DEFC)]

延时消息:Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

5.5.2 适用场景

消息生产和消费有时间窗口要求:比如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在 30 分钟以
后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略。

5.5.3 使用方式

Apache RocketMQ 目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在 Broker 层面,必须要做消息排序,如果再涉及到持久化,
那么消息排序要不可避免的产生巨大性能开销。(阿里云 RocketMQ 提供了任意时刻的定时消息功能,Apache 的 RocketMQ 并没有,阿里并没有开源)

发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递

延迟消息是根据延迟队列的 level 来的,延迟队列默认

msg.setDelayTimeLevel(3)代表延迟 10 秒
“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”

在源码org/apache/rocketmq/store/config/MessageStoreConfig.java

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCSCUDC7-1625329015948)(85F5AD2143684589A41D2023C0ECAA78)]

是这 18 个等级(秒(s)、分(m)、小时(h)),level 为 1,表示延迟 1 秒后消费,level 为 5 表示延迟 1 分钟后消费,level 为 18 表示延迟 2 个
小时消费。生产消息跟普通的生产消息类似,只需要在消息上设置延迟队列的 level 即可。消费消息跟普通的消费消息一致。

  • 生产者
/**
 * @author Crazy.X
 * 延时消息-生产者
 */
public class ScheduledMessageProducer {
    public static void main(String[] args) throws Exception {
        // 实例化一个生产者来产生延时消息
        DefaultMQProducer producer = new DefaultMQProducer("ScheduledProducer");
        // 指定NameServer地址信息
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();
        int totalMessagesToSend = 10;
        for (int i = 0; i < totalMessagesToSend; i++) {
            Message message = new Message("ScheduledTopic", ("Hello scheduled message " + i).getBytes());
            // 设置延时等级3,这个消息将在10s之后投递给消费者(详看delayTimeLevel)
            // delayTimeLevel:(1~18个等级)"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
            // 4就是第四个等级 30s
            message.setDelayTimeLevel(4);
            // 发送消息
            producer.send(message);
        }
        // 关闭生产者
        producer.shutdown();
    }
}
  • 消费者
/**
 * @author Crazy.X
 * 延时消息-消费者
 */
public class ScheduledMessageConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ScheduledConsumer");
        // 指定NameServer地址信息
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 订阅Topics
        consumer.subscribe("ScheduledTopic", "*");
        // 注册消息监听者
        consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {
            for (MessageExt message : messages) {
                // msgId 消息ID
                // getStoreTimestamp() 存储时间戳
                // getBornTimestamp() 诞生时间戳
                System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
                        + (message.getStoreTimestamp() - message.getBornTimestamp()) + "ms later");
            }
            // 提交消费
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 启动消费者
        consumer.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yERfyKIF-1625329015949)(5039A098A320407EB2A42833884F7865)]

5.6 批量消息

批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK(集群时会细讲),而且不能是延
时消息。此外,这一批消息的总大小不应超过 4MB

  • 生产者
/**
 * @author Crazy.X
 * 批量消息-生产者  list不要超过4m
 */
/**
 * @author Crazy.X
 * 批量消息-生产者  list不要超过4m
 */
public class BatchProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();

        String topic = "BatchTest";
        // list不要超过4m
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
        try {
            producer.send(messages);
        } catch (Exception e) {
            producer.shutdown();
            e.printStackTrace();
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
  • 消费者
/**
 * @author Crazy.X
 * 批量消息-消费者 批量消费延迟较高
 */
public class BatchConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息消费者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BatchConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 订阅Topic
        consumer.subscribe("BatchTest", "*");
        //负载均衡模式消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            System.out.printf("%s Receive New Messages: %s %n",
                    Thread.currentThread().getName(), msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tICKnjo1-1625329015949)(532759BF7B004CAEB98D63FA972D3DF9)]

5.6.1 批量切分

如果消息的总长度可能大于 4MB 时,这时候最好把消息进行分割

/**
 * @author Crazy.X
 * 批量消息-超过4m-生产者
 */
public class SplitBatchProducer {

    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 启动Producer实例
        producer.start();

        //large batch
        String topic = "BatchTest";
        List<Message> messages = new ArrayList<>(100 * 1000);
        //10万元素的数组
        for (int i = 0; i < 100 * 1000; i++) {
            messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes()));
        }

        //把大的消息分裂成若干个小的消息(1M左右)
        ListSplitter splitter = new ListSplitter(messages);
        while (splitter.hasNext()) {
            List<Message> listItem = splitter.next();
            producer.send(listItem);
            Thread.sleep(100);
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
        System.out.printf("Consumer Started.%n");
    }

}

class ListSplitter implements Iterator<List<Message>> {
    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();
            }
            // 增加日志的开销20字节
            tmpSize = tmpSize + 20;
            //1M
            int sizeLimit = 1000 * 1000;
            if (tmpSize > sizeLimit) {
                //单个消息超过了最大的限制(1M)
                //忽略,否则会阻塞分裂的进程
                if (nextIndex - currIndex == 0) {
                    //假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
                    nextIndex++;
                }
                break;
            }
            if (tmpSize + totalSize > sizeLimit) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Not allowed to remove");
    }
}

5.7 过滤消息

注意事项: 区分tag group一定要不同的组,否则变成负载均衡状态出现数据错误

5.7.1 Tag 过滤

在大多数情况下,TAG 是一个简单而有用的设计,其可以来选择您想要的消息。举例说明:JD电商电脑商品的只消费电脑,书籍商品的只消费书籍

  • 生产者
/**
 * @author Crazy.X
 * tag过滤-生产者
 */
public class TagFilterProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("TagFilterProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        producer.start();
        // 创建两个tags
        String[] tags = new String[]{"computer", "book"};

        for (int i = 0; i < 10; i++) {
            Message msg = new Message("TagFilterTest",
                    tags[i % tags.length],
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例
        producer.shutdown();
    }
}
  • 消费者-tag computer
/**
 * @author Crazy.X
 * tag过滤computer-消费者
 */
public class TagComputerFilterConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
        // 实例化消息消费者,指定组名-tag区分的话组一定不要设置重复
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TagComputerFilterConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // tag来区分,举例说明:JD电商电脑商品的只消费电脑,书籍商品的只消费书籍
        consumer.subscribe("TagFilterTest", "computer");
        // 指定消费开始偏移量 ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET 也是默认值,可见源码
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LFCp3ooJ-1625329015949)(2332B98DD6A647888DCD3724C149E885)]

  • 消费者-tag book
/**
 * @author Crazy.X
 * tag过滤book-消费者
 */
public class TagBookFilterConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
        // 实例化消息消费者,指定组名-tag区分的话组一定不要设置重复
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TagBookFilterConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // tag来区分,举例说明:JD电商电脑商品的只消费电脑,书籍商品的只消费书籍
        consumer.subscribe("TagFilterTest", "book");
        // 指定消费开始偏移量 ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET 也是默认值,可见源码
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2CdmRsV-1625329015950)(7FDE3BE518854BD5A19E7671DE250015)]

5.7.2 Sql 过滤

注意: 开启sql过滤会影响性能(不建议使用)

sql过滤需要broker.conf配置文件添加配置

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
brokerIP1=192.168.3.200
namesrvAddr=192.168.3.200:9876
# 加入开启过滤
enablePropertyFilter=true
5.7.2.1 SQL 基本语法

RocketMQ 定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。

  1. 数值比较:比如:>>=<<=BETWEEN=
  2. 字符比较:比如:=<>IN
  3. IS NULL 或者 IS NOT NULL
  • 逻辑符号:ANDORNOT

常量支持类型为:

  1. 数值,比如:123,3.1415;
  2. 字符,比如:‘abc’,必须用单引号包裹起来;
  3. NULL,特殊的常量
  4. 布尔值,TRUEFALSE

只有推送消费者才能通过 SQL92 选择消息。界面是:

public void subscribe(final String topic, final MessageSelector messageSelector)
  • 生产者
/**
 * @author Crazy.X
 * sql过滤 -消息生产者(加入消息属性)
 */
public class SqlFilterProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("SqlFilterProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        producer.start();

        String[] tags = new String[]{"TagA", "TagB", "TagC"};

        for (int i = 0; i < 10; i++) {
            Message msg = new Message("SqlFilterTest",
                    tags[i % tags.length],
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            // 设置一些属性
            msg.putUserProperty("a", String.valueOf(i));

            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例
        producer.shutdown();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNbt2Feu-1625329015950)(F164F22ABFA84FFC8856FA67714EF23E)]

  • 消费者
/**
 * @author Crazy.X
 * sql过滤-消费者
 */
public class SqlFilterConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息消费者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("SqlFilterConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // Don't forget to set enablePropertyFilter=true in broker
        consumer.subscribe("SqlFilterTest",
                MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
                        "and (a is not null and a between 0 and 3)"));

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YnsOXScn-1625329015951)(D0F8B853E65B4966941D94625406F91D)]

5.8 事务消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6p8xsA7q-1625329015951)(FA2B1AAA9C484353B1EA14A1DFC22F75)]

其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

5.8.1 正常事务流程

(1) 发送消息(half 消息):图中步骤 1。

(2) 服务端响应消息写入结果:图中步骤 2。

(3) 根据发送结果执行本地事务(如果写入失败,此时 half 消息对业务不可见,本地逻辑不执行):图中步骤 3。

(4) 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见):图中步骤 4

5.8.2 事务补偿流程

(1) 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次“回查”:图中步骤 5。

(2) Producer 收到回查消息,检查回查消息对应的本地事务的状态:图中步骤 6。

(3) 根据本地事务状态,重新 Commit 或者 Rollback::图中步骤 6。

其中,补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况。

5.8.3 事务消息状态

事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction: 提交状态,它允许消费者消费此消息(完成图中了 1,2,3,4 步,第 4 步是 Commit)。
  • TransactionStatus.RollbackTransaction: 回滚状态,它代表该消息将被删除,不允许被消费(完成图中了 1,2,3,4 步, 第 4 步是 Rollback)。
  • ransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态(完成图中了 1,2,3 步, 但是没有 4 或者没有 7,无法 Commit
5.8.4 创建事务性生产者

使用 TransactionMQProducer 类创建生产者,并指定唯一的 ProducerGroup,就可以设置自定义线程池来处理这些检查请求。执行本地事务后、需
要根据执行结果对消息队列进行回复

/**
 * @author Crazy.X
 * 事务消息-消息发送方
 */
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        // 创建消息生产者
        TransactionMQProducer producer = new TransactionMQProducer("TransactionProducer");
        // 设置NameServer的地址
        producer.setNamesrvAddr("192.168.3.200:9876");
        // 创建线程池
        ExecutorService executorService = new ThreadPoolExecutor(
                2,
                5,
                100,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2000), r -> {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        });
        // 设置生产者回查线程池
        producer.setExecutorService(executorService);
        // 生产者设置监听器
        producer.setTransactionListener(transactionListener);
        // 启动消息生产者
        producer.start();
        String[] tags = new String[]{"TagA", "TagB", "TagC"};
        for (int i = 0; i < 3; i++) {
            try {
                Message msg = new Message(
                        "TransactionTopic",
                        tags[i % tags.length],
                        "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                //1,2步  半事务的发送,确认。
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);
                Thread.sleep(1000);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}
5.8.5 实现事务的监听接口

当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务(步骤 3)。它返回前一节中提到的三个事务状态之一。
checkLocalTranscation 方法用于检查本地事务状态(步骤 5),并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。

public class TransactionListenerImpl implements TransactionListener {
    // 事务状态记录
    private AtomicInteger transactionIndex = new AtomicInteger(0);
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    //执行本地事务 3
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("执行本地事务");
        int value = transactionIndex.getAndIncrement();

        //0,1,2
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);

        //这里模拟的不进行步骤4  A系统不知道的--UNKNOW
        return LocalTransactionState.UNKNOW;
    }

    //检查本地事务状态  默认是60s,一分钟检查一次
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        //打印每次回查的时间
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
        System.out.println("checkLocalTransaction:" + df.format(new Date()));// new Date()为获取当前系统时间
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【中间状态】");
                    return LocalTransactionState.UNKNOW;
                case 1:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【提交状态】");
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【回滚状态】");
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                default:
                    System.out.println("MQ检查消息【" + msg.getTransactionId() + "】事务状态【提交状态】");
                    return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
        //  System.out.println("MQ检查消息【"+msg.getTransactionId()+"】事务状态【提交状态】");
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}
5.8.6 消息消费者
/**
 * @author Crazy.X
 * 事务消息-消费者
 */
public class TransactionalConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TransactionalConsumer");
        // 设置NameServer的地址
        consumer.setNamesrvAddr("192.168.3.200:9876");
        // 订阅Topic
        consumer.subscribe("TransactionTopic", "*");
        //负载均衡模式消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            try {
                for (MessageExt msg : msgs) {
                    String topic = msg.getTopic();
                    String msgBody = new String(msg.getBody(), StandardCharsets.UTF_8);
                    String msgPro = msg.getProperty("a");

                    String tags = msg.getTags();
                    System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro + " ,msg : " + msgBody);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;

            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
5.8.7 使用场景

用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。如何保证数据
的完整性?

可以使用 RocketMQ 的分布式事务保证在下单失败后系统数据的完整性

5.8.8 使用限制
  1. 事务消息不支持延时消息和批量消息。
  2. 事务回查的间隔时间:BrokerConfig. transactionCheckInterval 通过 Broker 的配置文件设置好。3. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的
    transactionCheckMax 参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认
    情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  3. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用
    户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
  4. 事务性消息可能不止一次被检查或消费。
  5. 事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。
  6. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事
    务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
  7. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ 服务器能通过它们的生产者
    ID 查询到消费者。