RocketMQ事务消息学习及刨坑过程

一、背景

MQ组件是系统架构里必不可少的一门利器,设计层面可以降低系统耦合度,高并发场景又可以起到削峰填谷的作用,从单体应用到集群部署方案,再到现在的微服务架构,MQ凭借其优秀的性能和高可靠性,得到了广泛的认可。随着数据量增多,系统压力变大,开始出现这种现象:数据库已经更新了,但消息没发出来,或者消息先发了,但后来数据库更新失败了,结果研发童鞋各种数据修复,这种生产问题出现的概率不大,但让人很郁闷。这个其实就是数据库事务与MQ消息的一致性问题,简单来讲,数据库的事务跟普通MQ消息发送无法直接绑定与数据库事务绑定在一起,例如上面提及的两种问题场景:

  1. 数据库事务提交后发送MQ消息;
  2. MQ消息先发,然后再提交数据库事务。

场景1的问题是数据库事务可能刚刚提交,服务器就宕机了,MQ消息没发出去,场景2的问题就是MQ消息发送出去了,但数据库事务提交失败,又没办法追加已经发出去的MQ消息,结果导致数据没更新,下游已经收到消息,最终事务出现不一致的情况。

二、事务消息的引出

我们以微服务架构的购物场景为例,参照一下RocketMQ官方的例子,用户A发起订单,支付100块钱操作完成后,能得到100积分,账户服务和会员服务是两个独立的微服务模块,有各自的数据库,按照上文提及的问题可能性,将会出现这些情况:

  • 如果先扣款,再发消息,可能钱刚扣完,宕机了,消息没发出去,结果积分没增加。
  • 如果先发消息,再扣款,可能积分增加了,但钱没扣掉,白送了100积分给人家。
  • 钱正常扣了,消息也发送成功了,但会员服务实例消费消息出现问题,结果积分没增加。
    购物场景MQ通信案例

由此引出的是数据库事务与MQ消息的事务一致性问题,rocketmq事务消息解决的问题:解决本地事务执行与消息发送的原子性问题。这里界限一定要明白,是确保MQ生产端正确无误地将消息发送出来,没有多发,也不会漏发。但至于发送后消费端有没有正常的消费掉(如上面提及的第三种情况,钱正常扣了,消息也发了,但下游消费出问题导致积分不对),这种异常场景将由MQ消息消费失败重试机制来保证,不在此次的讨论范围内。

常用的MQ组件针对此场景都有自己的实现方案,如ActiveMQ使用AMQP协议(二阶提交方式)保证消息正确发送,这里我们以RocketMQ为重点进行学习。

三、RocketMQ事务消息设计思路

根据CAP理论,RocketMQ事务消息通过异步确保方式,保证事务的最终一致性。设计流程上借鉴两阶段提交理论,流程图如下:RocetMQ事务消息设计图

  1. 应用模块遇到要发送事务消息的场景时,先发送prepare消息给MQ。
  2. prepare消息发送成功后,应用模块执行数据库事务(本地事务)。
  3. 根据数据库事务执行的结果,再返回Commit或Rollback给MQ。
  4. 如果是Commit,MQ把消息下发给Consumer端,如果是Rollback,直接删掉prepare消息。
  5. 第3步的执行结果如果没响应,或是超时的,启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息),处理结果同第4步。
  6. MQ消费的成功机制由MQ自己保证。

四、RocketMQ事务消息实现流程

以RocketMQ 4.5.2版本为例,事务消息有专门的一个队列RMQSYSTRANSHALFTOPIC,所有的prepare消息都先往这里放,当消息收到Commit请求后,就把消息再塞到真实的Topic队列里,供Consumer消费,同时向RMQSYSTRANSOPHALF_TOPIC塞一条消息。简易流程图如下:RocketMQ事务消息实现流程

上述流程中,请允许我这样划分模块职责:

  1. RocketMQ Client即我们工程中导入的依赖jar包,RocketMQ Broker端即部署的服务端,NameServer暂未体现。
  2. 应用模块成对出现,上游为事务消息生产端,下游为事务消息消费端(事务消息对消费端是透明的,与普通消息一致)。

应用模块的事务因为中断,或是其他的网络原因,导致无法立即响应的,RocketMQ当做UNKNOW处理,RocketMQ事务消息还提供了一个补救方案:定时查询事务消息的数据库事务状态简易流程图如下:RocketMQ定时任务回查事务状态实现流程

五、源码剖析

讲解的思路基本上按照如下流程图,根据模块职责和流程逐一分析。

  1. 环境准备
    阅读源码前需要在IDE上获取和调试RocketMQ的源码,这部分请自行查阅方法。
  2. 应用模块(事务消息生产端)核心源码
    创建一个监听类,实现TransactionListener接口,在实现的数据库事务提交方法和回查事务状态方法模拟结果。
/**
 * @program: rocket
 * @description: 调试事务消息示例代码
 * @author: Huang
 * @create: 2019-10-16
 **/
public class SelfTransactionListener implements TransactionListener {
   private AtomicInteger transactionIndex = new AtomicInteger(0);
   private AtomicInteger checkTimes = new AtomicInteger(0);

   private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
   /**
    * 执行本地事务
    *
    * @param message
    * @param o
    * @return
    */
   @Override
   public LocalTransactionState executeLocalTransaction(Message message, Object o) {
      String msgKey = message.getKeys();
      System.out.println("start execute local transaction "   msgKey);
      LocalTransactionState state;
      if (msgKey.contains("1")) {
         // 第一条消息让他通过
         state = LocalTransactionState.COMMIT_MESSAGE;
      } else if (msgKey.contains("2")) {
         // 第二条消息模拟异常,明确回复回滚
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值