RocketMQ事务消息解析

RocketMQ事务消息解析

之前在网上看到了一篇关于RocketMQ的事务消息的文章,感觉讲的并不好,甚至有错误的地方,所以就想自己来写一篇文章,讲一讲我对与RocketMQ的事务消息的理解,不一定会正确,各位在看的时候,可以结合自己的思考,看看是否有一定的参考性,这里会单刀直入的讲解重点,而不会讲一些铺垫的东西,例如什么是RocketMQ,什么是事务消息,什么是事务等等,如果你遇到了一些名词,自己不是很理解,需要自己去其它地方学习下。

应用的场景

电商支付场景中,向DB中写入了订单信息,并且发送了1个MQ消息,通知其它系统处理积分、优惠券等服务,DB中的事物还是比较容易的,只要不是分库的,就好操作,相关的内容网上也很多,就不过多说了,现在假设数据库的事物是T1,发送MQ的事务消息是T2,那么伪代码就是如下:

执行T1(写DB)
if 成功
	执行T2(发MQ)

如果你这么写,就会有很大的问题,因为T2有可能会失败,这样T1没有回滚,就出现了问题;也有可能是T2超时了,导致T2实际到底有没有成功,不知道,这个时候也会有问题。这里是1个基本的场景,所以理论上来说,是不能这么写代码的,那么要怎么写呢? RocketMQ给了一个事务消息的选项。

事务消息

Rocket MQ的事务消息,可以保证MQ如果发送成功,DB事务也一定成功,DB失败了,MQ也一定不会成功,这样就解决了上述的问题,怎么做到的呢?接下来,我主要的目的,就是给大家讲清楚,怎么实现DB成功,MQ成功,DB失败,MQ也一定失败的。

首先要实现1个TransactionListener的接口,这个接口有2个方法

/**
     * When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
     *
     * @param msg Half(prepare) message
     * @param arg Custom business parameter
     * @return Transaction state
     */
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);

/**
     * When no response to prepare(half) message. broker will send check message to check the transaction status, and this
     * method will be invoked to get local transaction status.
     *
     * @param msg Check message
     * @return Transaction state
     */
LocalTransactionState checkLocalTransaction(final MessageExt msg);

这里要重点理解2个方法的注释,不要觉的是英文的注释,就不想看,从这个注释上看,可以理解,Rocket MQ在假设一种未知的状态,什么状态呢? 就是收不到prepare(half) message的回应,这里引申出来了1个概念,叫做预处理消息,或者叫准备状态的消息,RocketMQ的事务消息是基于两阶段提交实现的,也就是说消息有两个状态,prepared和commited。当消息执行完send方法后,进入的prepared状态。这里我们要讲一下LocalTransactionState这个类,这个类有3种状态:

  • COMMIT_MESSAGE:提交消息,这个消息由prepared状态进入到commited状态,消费者可以消费这个消息
  • ROLLBACK_MESSAGE:回滚
  • UNKNOW:未知状态

这里的一个场景是,未知状态是什么意思? 先放下,然后看看我们该怎么用这2个方法。这里我主要写伪代码。

这里我们降低难度,假设DB的事务就是1个insert,向订单表插入了1条记录

public class TransactionListenerImpl implements TransactionListener {
	
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {

        try {
            // 开启DB事务
        	startDBTransaction();
        	//执行事务内容
        	long id = insert();
        	//提交DB事务
        	commit();
        	if id > 0 return COMMIT_MESSAGE;
            else return ROLLBACK_MESSAGE;
        } catch() {
            // 回滚DB事务
            rollback();
            return ROLLBACK_MESSAGE;
        }  
    }
    
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        
        //执行SQL,检查刚才的事务,是否成功了,一般msg里,有刚才DB事务的ID,这里是订单ID
        //根据订单ID去查订单存在不,存在就说明DB事务成功了,不存在就说明DB事务失败了
        long id = msg.getId();
        int count = getFromDbByOrderId(id);
        if(count > 0) {
            //说明订单插入成功了
            return COMMIT_MESSAGE;
        } else {
            return ROLLBACK_MESSAGE;
        }
    }
}

这么做,是怎么实现事务消息的呢?RocketMQ的内部应该是这么做的(我没看源码,不确定具体的代码)

boolean succ = sendMq() //准备状态,消费者看不见该条消息
if(succ) {
  boolean succ1 = executeLocalTransaction()
  // A情况 如果执行到这里挂了,或者异常了,触发checkLocalTransaction
  if(succ1) {
     commitMQ() //把MQ的状态,从准备改为可消费,消费者可以消费该条消息
     // B情况 如果执行到这里挂了,或者异常了,触发checkLocalTransaction
  } else {
     rollbackMQ() //把MQ的状态,从准备改为取消,消费者不消费这条MQ消息
     // C情况 如果执行到这里挂了,或者异常了,触发checkLocalTransaction
  }
} else {
  	return 失败
}

//如果在执行期间挂了,恢复之后还是会checkLocalTransaction,看看DB事务是否成功
//RocketMQ有个后台线程,一直在轮询prepare message,这东西肯定在1个队列里
//重试5次,是我随意写的,具体看源码吧,这个不重要,重要的是思想。
MessageExt prepareMsg = getPrepareMessageFromQueue();
while(prepareMsg && 重试次数 < 5) {
    LocalTransactionState state = checkLocalTransaction(prepareMsg)
    if(state == COMMIT_MESSAGE) {
        //提交MQ事务
        //从队列里移除这条消息
        //下一条
        prepareMsg = getPrepareMessageFromQueue();
    } else if(state == ROLLBACK_MESSAGE) {
        //回滚MQ事务
        //从队列里移除这条消息
        //下一条
        prepareMsg = getPrepareMessageFromQueue();
    } else {
        //这里是UNKNOW状态,这里应该什么都不做,直接获取下一条prepare message
        //DB里的事务不应该是UNKNOW状态
        //除非DB超时了,没有告诉MQ数据库事务到底是成功还是失败
        prepareMsg = getPrepareMessageFromQueue();
        //重试次数+1
    } 
}

核心思想:

  • 先发MQ,再执行DB的事务,再根据DB事务的状态,决定MQ消息是否要给消费者消费,如果DB成功了,MQ通过重试机制,保证prepare(half) message可以变更为commited状态。
  • DB的事务,必须在 executeLocalTransaction() 这个方法里写。
  • MQ自己内部保证 commitMQ() 或者 rollbackMQ() 一定能成功。
  • 如果MQ挂了,再启动的时候,可以再去DB里查数据库事务是否成功,保证消息的最终一致。

这里可以看到,RocketMQ通过先发MQ消息,再执行DB事务,保证了在发送MQ这个环境一旦出现错误,可以通过再次回溯DB,查看DB的事务状态,来判断是提交MQ的事务消息,还是回滚MQ的事务消息,这里其实也有问题,就是一旦MQ执行commitMQ失败了,去回溯DB的时候,恰好DB也挂了,而且MQ重试了几次,DB都没有恢复,这个时候该怎么办呢?大家可以思考下。

看到这里,我们再回过头来看,未知状态是什么?其实就是各种异常情况导致的未知状态,就是上述代码中的A情况、B情况、C情况,可能还会有网络超时、MQ自己挂了、MQ消息超时等异常情况,这里RocketMQ是通过回溯DB查看DB的事务状态来决定MQ的事务状态的,本质上属于2PC,并不是真正的分布式事务。这里分布式事务的难点在于通信信道的不可靠,具体可以看看2军问题,2军问题和拜占庭将军问题是完全不同的2个问题,大家一定要分清楚。

全文完。

关注我的博客,获取更多Java编程知识: 双King的技术博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值