可靠消息最终一致性:是指当事务发起方执行完成本地事务后发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理成功,即强调的是只要消息发给事务参与方最终事务要达到一致。
该方案通常是利用消息中间件完成,如下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件
之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事
务问题。
因此可靠消息最终一致性方案要解决以下几个问题:
(1)本地事务与消息发送的原子性问题
事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。针对这点,我么可能这样:
先发送消息,再操作数据库:
begin transaction;
// 1.发送消息到MQ
// 2.数据业务操作
commit transation;
这种情况是无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据业务操作失败。所以我们可能就有第二种方案,先进行数据库操作,再发送消息:
begin transaction;
// 1.数据业务操作
// 2.发送消息到MQ
commit transation;
这种情况看似没有问题,理想情况下如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果发送消息到MQ超时异常,数据库回滚,此时MQ有可能已经正常发送,那么仍然无法做到原子性。
(2)事务参与方接收消息的可靠性
事务参与方必须能够从消息中间件接收到消息,并且如果接收消息失败可以重复接收消息。
(3)消息重复消费的问题
由于网络的存在,假设一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,那么就会导致消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性。这一点消息中间件是无法保证的。
针对上面的几个问题,解决方案主要有以下两个:
本地消息表是指通过本地事务保证数据业务操作和消息的一致性,其操作大致如下:
begin transaction;
// 1.执行数据业务操作
// 2.新增消息记录
commit transation;
然后通过定时任务将消息发送至消息中间件(解决问题1:本地事务与消息发送的原子性问题),待消息确认发送给消费方成功后再将消息删除(解决问题2:事务参与方接收消息的可靠性)。
需要注意的是:
只有当消费方消费成功后,向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
RocketMQ事务消息主要是为了解决生成者的消息发送与本地事务执行的原子性问题,RocketMQ 的 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在。而 RocketMQ本身提供的存储机制为事务消息提供了持久化能力,RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
RocketMQ事务消息实际上是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。执行流程如下:
(1)Producer发送事务消息
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注
意此时这条消息消费者(MQ订阅方)是无法消费到的。
(2)MQ Server回应消息发送成功
MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。
(3)Producer 执行本地事务
Producer 端执行业务代码逻辑,通过本地数据库事务控制。
(4)消息投递
若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将消息状态标记为可消费,此时MQ订阅方即正常消费消息;若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后,将删除消息 。
MQ订阅方消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
(5)事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer
来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此
只需关注本地事务的执行状态即可。RoacketMQ提供RocketMQLocalTransactionListener接口:
public interface RocketMQLocalTransactionListener {
/**
* 发送prepare消息成功此方法被回调,该方法用于执行本地事务
* @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
* @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
/**
* @param msg 通过获取transactionId来判断这条消息的本地事务执行状态
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}