声明: 本篇主要对所用到的技术做了归纳总结,对源码讲解较少,如果有基础的朋友可直接下载源码结合时序图更能容易理解;基础比较弱的朋友建议先看看资料自看源码这样更容易理解。这里的部分资料来源于网络,所以这里对那些资料提供者表达衷心的感谢。
方案:
- 业务流程:Tss库向Saas转移库存,order为记录表。
- 技术栈:Springboot+RocketMQ+mysq+postgreSQL
- 代码地址:https://github.com/bao17634/SpringBoot-RocketMQ-Demo.git
1、方案时序图:
1.1、正常流程
- 1)produce先向MQ server发送消息。
- 2)MQ server将消息持久化成功后,向produce发送ACK消息,确认消息是否发送成功,此时消息为半消息。
- 3)如消息发送成功,则produce开始执行本地事务;如消息接收失败,后续也不执行任何操作。
- 4) 如produce本地事务执行成功,produce向MQ server发送的commit进行确认,此时消息就会标记为可投递的消息;如MQ server收到Rollback的确认,就会删除半消息,consumer也不会接收到此消息,后面也无任何操作。
- 5)MQ server将已标记的消息发送给consumer,consumer在接收到消息后就会执行相关业务操作。
1.2、MQ长时间按未收到produce的消息
- 1)执行过程中MQ server长时间未收到produce的消息,就会调用checkLocalTransaction()方法进行回查。
- 2)回查本地事务是否执行成功,如果执行成功则会让MQ server重新提交二次确认消息(Commit或RollBack),后续操作就会按照流程1.1中的步骤4和步骤5来执行。
1.3、消费端消息消费失败
- 1)消费消费RocketMQ定义的有两种状态码:
CONSUME_SUCCESS
、RECONSUME_LATER
;如果消费端返回RECONSUME_LATER
或出现异常则表示消息消费失败。 - 2)MQ在消费端消息消费失败时,其内部的核心组件Broker在一段时间内(两小时内)会重新向消费端发送消息,最大可能的保证消息消费成功。
MQ消息重试次数与时间关系如下图所示:
可以看到,RocketMQ采用了“时间衰减策略”进行消息的重复投递,即重试次数越多,消息消费成功的可能性越小。
1.4 补充说明:
- checkLocalTransaction()方法其实是MQ包中TransactionListener接口中方法的实现,所以在MQ回调checkLocalTransaction()是在MQ内部回调。但是在本方案我主要是检查本地事是否执行成功,跟执行本地事务的方法写在一起,所以我在时序图中将checkLocalTransaction()方法画在produce上,也为了理解方便。
- 消费成功的消息MQ不会立即删除,还会继续存储在MQ的CommitLog文件中,在消费端消费完消息之后,在响应体中,broker会返回下一次应该拉取的位置,PushConsumer通过这一个位置,更新自己下一次的pull请求。这样就保证了正常情况下,消息只会被投递一次(消费端拉取消息的方式有两种:
push
、pull
)。 - 消息存储在CommitLog之后,清理只会在以下任一条件成立才会批量删除消息CommitLog文件。
- 消息文件过期(默认72小时),且到达清理时点(默认是凌晨4点),删除过期文件。
- 消息文件过期(默认72小时),且磁盘空间达到了水位线(默认75%),删除过期文件。
- 磁盘已经达到必须释放的上限(85%水位线)的时候,则开始批量清理文件(无论是否过期),直到空间充足。
- 若磁盘空间达到危险水位线(默认90%),出于保护自身的目的,broker会拒绝写入服务。
- 详解请点击。
1.5 部分代码如下:
(1)produce配置
@Override
public void afterPropertiesSet() throws Exception {
producer.setNamesrvAddr("127.0.0.1:9876");
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r){
Thread thread = new Thread(r);
thread.setName("client-transaction-message-check-thread");
return thread;
}
});
//开启多线程,用于回查
producer.setExecutorService(executorService);
//设置回调事务检查监听器
producer.setTransactionListener(transactionListener);
try {
producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
(2)向MQ发送消息
Message msg = new Message("orderMessage", "order",orderDTO.getOrder().getOrderCode(),
JSON.toJSONString(orderDTO).getBytes(RemotingHelper.DEFAULT_CHARSET));
//生产者通过sendMessageInTransaction向RocketMQ发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
(3)配置consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderDemo");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("orderMessage", "*");
(4)consumer接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> message, ConsumeConcurrentlyContext context) {
ConsumeConcurrentlyStatus concurrentlyStatus = consumerService.consumeOrderMessage(message);
return concurrentlyStatus;
}
});
(5)异常回调方法
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
LocalTransactionState state = LocalTransactionState.UNKNOW;
try {
OrderDTO orderDTO = JSON.parseObject(msg.getBody(), OrderDTO.class);
if (orderDTO != null) {
state = LocalTransactionState.ROLLBACK_MESSAGE;
}
boolean isCommit = tssHouseService.checkTransferStatus(msg.getTransactionId());
} catch (Exception e) {
e.printStackTrace();
}
return state;
}
2、RocketMQ
RocketMQ 是一个分布式消息传递和流媒体平台,具有低延迟,高性能和可靠性,万亿级容量和灵活的可扩展性。
rocketMQ特点:
- 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。
- Producer、Consumer队列都可以分布式。
- Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合。
- 能够保证严格的消息顺序
- 提供丰富的消息拉取模式
- 高效的订阅者水平扩展能力
- 实时的消息订阅机制
- 亿级消息堆积能力
- 较少的依赖
2.1、RocketMQ、ActiveMQ、Kafka性能对比
消息中间件 | 客户端SDK | 协议规范 | 消息订购 | 预定消息 | 批量留言 | 广播消息 | 邮件过滤 | 触发服务器重新发送消息 | 消息存储 | 消息追溯 | 消息优先级 | 高可用和故障转移 | 消息跟踪 | 组态 | 管理和操作工具 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ActiveMQ | java、.NET、C++ | 推模型、支持OPenWire、STOMP、AMQP、AMQP、MQTT、JMS | 独家消费者或独家排队可以确保订购 | 支持 | 不支持 | 支持 | 支持 | 不支持 | 使用JDBC和高性能日志(如levelDB,kahaDB) | 支持 | 支持 | 根据存储,如果使用kahadb,则需要ZooKeeper服务器 | 不支持 | 默认配置为低级别,用户需要优化配置参数 | 支持 |
kafka | Java,Scala | 拉模型,支持TCP | 确保在分区内对消息进行排序 | 不支持 | 支持,使用异步生产者 | 不支持 | 支持,您可以使用Kafka Streams过滤邮件 | 不支持 | 高性能文件存储 | 支持的偏移量表示 | 不支持 | 支持,需要ZooKeeper服务器 | 不支持 | Kafka使用键值对格式进行配置。可以从文件或以编程方式提供这些值 | 支持,使用terminal命令查看消息的信息 |
RocketMQ | Java,C ++,Go | 拉模型,支持TCP,JMS,OpenMessaging | 确保严格的消息排序,并可以优雅地扩展 | 支持 | 支持,使用同步模式以避免消息丢失 | 支持 | 支持的基于SQL92的属性过滤器表达式 | 支持 | 高性能和低延迟的文件存储 | 支持的时间戳和偏移量2表示 | 不支持 | 支持的Master-Slave模型,没有其他套件 | 支持 | 开箱即用,用户只需要注意一些配置 | 支持Web和终端命令,用于消息信息的展示 |
2.2、RocketMQ 核心组件图
RocketMQ是开源的消息中间件,它主要由NameServer,Producer,Broker,Consumer四部分构成。
NameServer
NameServer主要负责Topic和路由信息的管理。
Producer
消息生产者,负责产生消息,一般由业务系统负责产生消息。
Broker
消息中转角色,负责存储消息,转发消息。
Consumer
消息消费者,负责消息消费,一般是后台系统负责异步消费。
2.3、RokcetMQ 物理部署图
NameServer
NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker
Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
Producer
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Consumer
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
2.4、NameServer 路由注册、删除机制
- Broker每30秒向NameServer发送心跳包,心跳包中包含topic的路由信息
- NarneServer 收到 Broker 心跳包后 更新 brokerLiveTable 中的信息, 特别记录心跳时间 lastUpdateTime。
- NarneServer 每隔 10s 扫描 brokerLiveTable, 检 测表中上次收到心跳包的时间,比较当前时间 与上一次时间,如果超过120s,则认为 broker 不可用,移除路由表中与该 broker相关的所有信息。
- 消息生产者拉取主题的路由信息,即消息生产者并不会立即感知 Broker 服务器的新增与删除。
2.5、RocketMQ的消息领域模型图
Topic
- Topic表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息等。一条消息必须有一个Topic。
- 最细粒度的订阅单位,一个Group可以订阅多个Topic的消息。
Tag
Tag表示消息的第二级类型,比如交易消息又可以分为:交易创建消息,交易完成消息等。RocketMQ提供2级消息分类,方便灵活控制。
Group
Group组,一个组可以订阅多个Topic。
Message Queue
消息的物理管理单位。一个Topic下可以有多个Queue,Queue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力。在 RocketMQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用 Offset 来访问,offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限。也可以认为 Message Queue 是一个长度无限的数组,Offset 就是下标。
2.6、RocketMQ 消息存储设计原理图
CommitLog
消息存储文件,所有消息主题的消息都存储在 CommitLog 文件中。 Commitlog 文件存储的逻辑视图如图所示
ConsumeQueue
消息消费队列,消息到达 CommitLog 文件后,将异步转发到消息 消费队列,供消息消费者消费。ConsumeQueue存储格式如下:
- 单个 ConsumeQueue 文件中默认包含 30 万个条目,单个文件的长度为 30w × 20 字节, 单个 ConsumeQueue 文件可以看出是一个 ConsumeQueue 条目的数组,其下标为 ConsumeQueue 的逻辑偏移量,消息消费进度存储的偏移量 即逻辑偏移量。
- ConsumeQueue 即为 Commitlog 文件的索引文件, 其构建机制是当消息到达 Commitlog 文件后, 由专门的线程 产生消息转发任务,从而构建消息消费队列文件与下文提到的索引文件。
2.7 安装RocketMQ的步骤
注:下载最新版的rocketMQ版本,官方要求JDK版本1.8+,但是在用JDK11测试的时候 报一些不知名的错误后面就用JDK1.8,所以此处推荐用JDK1.8
3. 此方案总结
本方案基于支持事务消息的MQ(RocketMQ),其支持事务消息的方式采用类似于二阶段提交。此方案其实也是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
优点:
- 消息数据独立存储,降低业务系统与消息系统之间的耦合。
- 吞吐量优于本地消息表方案。
- 实现最终一致性,不需要依赖本地数据库事务。
缺点:
- 一次消息发送需要两次网络请求(half消息 + commit/rollback)。
- 需要实现消息回查接口。
- 实现难度大,主流的MQ不支持,仅支持MQ也只有RocketMQ且事务消息部分代码也未开源。