前言
在之前的文章中介绍了基于Seata实现的分布式事务的解决方案(AT、TCC模式),有兴趣的看一下
https://blog.youkuaiyun.com/hosaos/article/details/89136666
这两种方案都是偏向于强一致性解决方案。协调器会不断循环各个资源RM来进行事务的同时提交,回滚。
而往往很多场景不求事务的强一致性,只需达到事务的最终一致性,这时候,事务消息可以很好的满足需求。通过将本地事务与消息的发送放在一个本地事务中,来保证,本地事务执行成功时,消息一定被成功投递到消息服务器中,最终利用消息中间件的高可靠性,保证消息会被下游业务所消费。
RocketMq 4.3版本中开源了事务消息,本文会以RocketMq为例,介绍事务消息用法、设计思路及原理
思路与问题所在
上面提到了,要保证本地事务与消息的发送在一个事务中,如果以A给B转账100块为例子(A和B分别处在2个微服务中,对应2个数据库),具体怎么做呢?
-
场景一
先执行A扣钱100本地事务,再发送给B一条扣钱100消息,行么?
假设碰到网络问题,消息发送失败了。A扣了100,B却没加钱,肯定不行 -
场景二
那先发给B发送一条扣钱100消息,再执行A扣钱本地事务,行么?
如果消息发送成功了,这时候A服务所在数据库宕机了,岂不是B所在系统消费了消息,B加了100,A却没扣钱,也不对
问题关键点是什么? 只要A扣钱和发送消息不是一个原子操作,即不在一个事务中完成,那么,无论先后顺序如何,都会出现数据不一致性问题
那么聪明的人又会想到,我搞个本地消息表不就行了?
- 场景三
在一个事务中,同时操作如下两步
1、A扣钱100
2、将要发送的消息记录存入A所在数据库中(如transfer_money_message表)
那么A扣钱成功的同时,一定会有一条对应B扣钱的消息记录在数据库中,然后A所在系统单独启动一个定时器去扫描该消息表,并将状态为待发送的消息,投递到消息服务器中,失败重试,直到消息发送成功
这种方案行不行?当然可以,那么缺点又是什么?显而易见
业务方需要单独设计消息表,及定时发送消息的定时器,增加了与业务无关的开发负担
名词解释
再介绍RocketMq消息事务前,先介绍下几个关键名词
概念 | 解释 |
---|---|
prepare消息 | 又名Half Message,半消息,标识该消息处于"暂时不能投递"状态,不会被Comsumer所消费,待服务端收到生成者对该消息的commit或者rollback响应后,消息会被正常投递或者回滚(丢弃)消息 |
RMQ_SYS_TRANS_HALF_TOPIC | prepare消息在被投递到Mq服务器后,会存储于Topic为RMQ_SYS_TRANS_HALF_TOPIC的消费队列中 |
RMQ_SYS_TRANS_OP_HALF_TOPIC | 在prepare消息被commit或者rollback处理后,会存储到Topic为RMQ_SYS_TRANS_OP_HALF_TOPIC的队列中,标识prepare消息已被处理 |
RocketMQ-事务消息设计思路
先抛出两个核心概念:两阶段提交、事务状态定时回查,下面具体说明
两阶段提交
关于两阶段提交的基本概念,本文不再赘述,贴上一张图来说明
上面已经提到,因为消息发送是一个远程调用,由于网络的不稳定,无法和本地事务的执行处于一个原子操作中,针对这个缺点,RocketMQ基于两阶段提交协议做了如下改动
-
第一阶段:生产者向MQ服务器发送事务消息(prepare消息),服务端确认后回调通知生产者执行本地事务(此时消息为Prepare消息,存储于RMQ_SYS_TRANS_HALF_TOPIC队列中,不会被消费者消费)
-
第二阶段:生产者执行完本地事务后(业务执行完成,同时将消息唯一标记,如transactionId与该业务执行记录同时入库,方便事务回查),根据本地事务执行结果,返回Commit/Rollback/Unknow状态码
1、服务端若收到Commit状态码,则将prepare消息变为提交(正常消息,可被消费者消费)
2、收到Rollback则对消息进行回滚(丢弃消息)
3、若状态为Unknow,则等待MQ服务端定时发起消息状态回查,超过一定重试次数或者超时,消息会被丢弃
引用一张流程图来说明消息事务的两阶段提交
事务状态定时回查
在第二阶段中,生产者在本地事务执行完成后,需要向MQ服务器返回响应状态码,发送状态码的过程也是通过Netty发送网络请求,假设由于网络原因发送失败怎么办?本地事务已经提交/回滚了,但是Commit/Rollback状态码却没发出去,那么MQ服务器上这条prepare消息状态岂不是无法被投递/回滚
因此,MQ服务端会定时扫描存储于RMQ_SYS_TRANS_HALF_TOPIC中的消息,若消息未被处理,则向消费发送者发起回调检查,检查消息对应本地事务执行状态。从而保证消息事务状态最终能和本地事务的状态一致。上图中的5、6、7就是MQ服务端定时回查步骤。
事务消息Demo
先介绍下RocketMQ中事务消息的几个核心类
TransactionMQProducer
事务消息发送者, 核心方法如下
//发送事务消息,arg表示业务参数,能在回调执行本地事务时被取到
public TransactionSendResult sendMessageInTransaction(final Message msg,
final Object arg) throws MQClientException {
if (null == this.transactionListener) {
throw new MQClientException("TransactionListener is null", null);
}
return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
//设置事务状态回调监听器
public void setTransactionCheckListener(TransactionCheckListener transactionCheckListener) {
this.transactionCheckListener = transactionCheckListener;
}
2、TransactionCheckListener
事务状态回调监听器
/**
* prepare消息执行成功时,回调执行executeLocalTransaction方法,arg参数为sendMessageInTransaction时带入的业务参数
*
* @return Transaction state
*/
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
/**
* 检查消息对应本地事务执行状态的监听器,定时回调
*/
LocalTransactionState checkLocalTransaction(final MessageExt msg);
以用户1向用户2转账100块为例子