分布式事务大体可以分为以下两种方式实现:

两段提交和三段提交会有性能问题以及带来的资源锁定问题,在实际中很少使用。

1.补偿型事务

翻译过来就是:补偿是一个独立的支持ACID特性的本地事务,用于在逻辑上取消服务提供者上一个ACID事务造成的影响,对于一个长事务(long-running transaction),与其实现一个巨大的分布式ACID事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地ACID事务来处理,执行完就立即提交 

 

2.可靠消息最终一致性

消息发送一致性:是指产生消息的业务动作与消息发送的一致。也就是说,如果业务操作成功,那么由这个业务操作所产生的消息一定要成功投递出去(一般是发送到kafkarocketmqrabbitmq等消息中间件中,不能是redis,redis的List队列实现和Stream队列实现里都无法保证消息不丢)。

@Transactional(rollbackFor = Exception.class)
public void execute(){
    //1.发送mq
    producer.send(msg);  
    //2.执行本地事务     
    businessRepository.save(entity);
}

 这种方式会造成数据不一致问题,即mq已经投递成功被消费者成功消费,而第二步本地事务执行失败,事务回滚。

 那把步骤1和步骤2顺序颠倒是不是就没有问题了呢。

@Transactional(rollbackFor = Exception.class)
public void execute(){
    //1.执行本地事务     
    businessRepository.save(entity);
    //2.发送mq
    producer.send(msg);  
}

其实还有问题,假设步骤1执行成功,步骤2消息已经投递成功被成功消费,但是服务与mq的连接超时或中断,导致步骤2抛出异常。此时步骤1的本地事务回滚,造成数据不一致。

解决这个问题的关键在于,需要确保上游服务本地事务和MQ消息的投递具有原子性,也就是说上游服务本地事务处理成功后要确保消息一定成功投递到消息中间件,否则消息不应该被投递到MQ服务。同样,被成功投递到消息中间件的消息,也一定要被下游服务成功处理,否则需要重新投递MQ消息。

以银行扣款为例:

1、在扣款之前,先发送预备消息
2、发送预备消息成功后,执行本地扣款事务
3、扣款成功后,再发送确认消息
4、消息端(加钱业务)可以看到确认消息,消费此消息,进行加钱

异常1:如果发送预备消息失败,下面的流程不会走下去;这个是正常的
异常2:
如果发送预备消息成功,但执行本地事务失败;这个也没有问题,因为此预备消息不会被消费端订阅到,消费端不会执行业务。
异常3:
如果发送预备消息成功,执行本地事务成功,但发送确认消息失败;这个就有问题了,因为用户A扣款成功了,但加钱业务没有订阅到确认消息,无法加钱。这里出现了数据不一致。这里就需要借助RocketMQ回查机制解决。核心思路就是【状态回查】,也就是RocketMq会定时遍历commitlog中的预备消息。
因为预备消息最终肯定会变为commit消息或Rollback消息,所以遍历预备消息去回查本地业务的执行状态,如果发现本地业务没有执行成功就rollBack,如果执行成功就发送commit消息。

上面的异常3,发送预备消息成功,本地扣款事务成功,但发送确认消息失败;因为RocketMq会进行回查预备消息,在回查后发现业务已经扣款成功了,就补发“发送commit确认消息”;这样加钱业务就可以订阅此消息了,异常2也解决了,因为本地事务没有执行成功,RocketMQ回查业务,发现没有执行成功,就会发送RollBack确认消息,把消息进行删除。

消息中间件回查业务

如果本地事务涉及N多张表,判断本地事务是否执行成功会很麻烦,且与业务耦合度高。这里可以设计Transaction表,将业务表和Transaction绑定在同一个本地事务中,如果扣款本地事务成功时,Transaction中应当已经记录该TransactionId的状态为「已完成」。当RocketMq回查时,只需要检查对应的TransactionId的状态是否是「已完成」就好,而不用关心具体的业务数据。