分布式事务解决方案之可靠消息最终一致性、最大努力通知

点击上方蓝色字体,选择“设为星标”

优质文章,及时送达

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第1张图片

本文来源:https://dwz.cn/730BLvt0

上篇文章主要介绍了分布式事务解决方案之2PC、TCC,传送门,这篇文章将主要讲解分布式事务解决方案之可靠消息最终一致性、最大努力通知。

  • 分布式事务解决方案之可靠消息最终一致性

    • 什么是可靠消息最终一致性事务

    • 解决方案

      • 本地消息表方案

      • RocketMQ事务消息方案

  • 分布式事务解决方案之最大努力通知

    • 什么是最大努力通知

    • 解决方案

  • 分布式事务对比分析

1.分布式事务解决方案之可靠消息最终一致性

1.1 什么是可靠消息最终一致性事务

可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能 够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致

此方案是利用消息中间件完成,如下图:

事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件 之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事 务问题。

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第2张图片

因此可靠消息最终一致性方案要解决以下几个问题:

1.本地事务与消息发送的原子性问题 本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实 现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最 终一致性方案的关键问题。

先来尝试下这种操作,先发送消息,再操作数据库:

begin transaction; 
    //1.发送MQ
    //2.数据库操作
commit transation;

这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败。

你立马想到第二种方案,先进行数据库操作,再发送消息:

begin transaction;
    //1.数据库操作
    //2.发送MQ
commit transation;

这种情况下貌似没有问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数 据库回滚,但MQ其实已经正常发送了,同样会导致不一致。

2.事务参与方接收消息的可靠性

事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。

3.消息重复消费的问题

由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重 复消费。

要解决消息重复消费的问题就要实现事务参与方的方法幂等性。

1.2 解决方案

1.2.1 本地消息表方案

本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后 通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。

下面以注册送积分为例来说明:

下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第3张图片

交互流程如下:

1、用户注册 

用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致) 下边是伪代码

begin transaction; 
    //1.新增用户
    //2.存储积分消息日志
commit transation;

这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性

2、定时任务扫描日志

如何保证将消息发送给消息队列呢?

经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息 中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。

3、消费消息

如何保证消费者一定能消费到消息呢?

这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重 试向消费者来发送消息。

积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复 投递此消息。

由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性

1.2.2 RocketMQ事务消息方案

RocketMQ 是一个来自阿里巴巴的分布式消息中间件,于 2012 年开源,并在 2017 年正式成为 Apache 顶级项 目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在 RocketMQ 之 上,并且最近几年的双十一大促中,RocketMQ 都有抢眼表现。Apache RocketMQ 4.3之后的版本正式支持事务消 息,为分布式事务实现提供了便利性支持。

RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的 设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系 统发生异常时依然能够保证达成事务的最终一致性。

在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ 内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第4张图片

执行流程如下:为方便理解我们还以注册送积分的例子来描述 整个流程。

Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责 新增积分。

1、Producer 发送事务消息

Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注 意此时这条消息消费者(MQ订阅方)是无法消费到的。本例中,Producer 发送 ”增加积分消息“ 到MQ Server。

2、MQ Server回应消息发送成功 MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。

3、Producer 执行本地事务 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); }

发送事务消息:

以下是RocketMQ提供用于发送事务消息的API:

    TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup"); 
    producer.setNamesrvAddr("127.0.0.1:9876"); 
    producer.start();
   //设置TransactionListener实现
   producer.setTransactionListener(transactionListener); 
   //发送事务消息
    SendResult sendResult = producer.sendMessageInTransaction(msg, null);

1.3 小结

可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性, RocketMQ作为 消息中间件,

RocketMQ主要解决了两个功能:

  • 本地事务与消息发送的原子性问题。

  • 事务参与方接收消息的可靠性。可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消 息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。

2.分布式事务解决方案之最大努力通知

2.1 什么是最大努力通知

最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子:

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第5张图片

交互流程:

  1. 账户系统调用充值系统接口

  2. 充值系统完成支付处理向账户系统发起充值结果通知 若通知失败,则充值系统按策略进行重复通知

  3. 账户系统接收到充值结果通知修改充值状态。

  4. 账户系统未接收到通知会主动调用充值系统的接口查询充值结果

通过上边的例子我们总结最大努力通知方案的目标:

目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。

具体包括:

  1. 有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。

  2. 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息 信息来满足需求。

最大努力通知与可靠消息一致性有什么不同

1、解决方案思想不同

可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知 方来保证。

最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方

2、两者的业务应用场景不同

可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。

最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。

3、技术解决方向不同

可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。

最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消 息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

2.2 解决方案

通过对最大努力通知的理解,采用MQ的ack机制就可以实现最大努力通知

方案1:

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第6张图片

本方案是利用MQ的ack机制由MQ向接收通知方发送通知,流程如下:

  1. 发起通知方将通知发给MQ。使用普通消息机制将通知发给MQ。注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果。(后边会讲)

  2. 接收通知方监听 MQ

  3. 接收通知方接收消息,业务处理完成回应ack

  4. 接收通知方若没有回应ack则MQ会重复通知。

  5. 接收通知方可通过消息校对接口来校对消息的一致性。

方案2:

本方案也是利用MQ的ack机制,与方案1不同的是应用程序向接收通知方发送通知,如下图:

分布式事务解决方案之可靠消息最终一致性、最大努力通知_第7张图片

交互流程如下:

  1. 发起通知方将通知发给MQ。使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ。

  2. 通知程序监听 MQ,接收MQ的消息。方案1中接收通知方直接监听MQ,方案2中由通知程序监听MQ。通知程序若没有回应ack则MQ会重复通知。

  3. 通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知。通知程序调用接收通知方案接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消 息。接收通知方可通过消息校对接口来校对消息的一致性。

方案1和方案2的不同点:

  1. 方案1中接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。

  2. 方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收 通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知。

3.分布式事务对比分析:

在了解各种分布式事务的解决方案后,我们了解到各种方案的优缺点:

2PC 最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其 阻塞机制和最差时间复杂度高, 因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并 发较高以及子事务生命周期较长 (long-running transactions) 的分布式服务中。

如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实 现不同的回滚策略。典型的使用场景:满,登录送优惠券等。

可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消 息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注 册送积分,登录送优惠券等。

最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业 务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后 续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果 通知等。

最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业 务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后 续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果 通知等。


2PC TCC 可靠消息 最大努力通知
一致性 强一致性 最终一致 最终一致 最终一致
吞吐量
实现复杂度

总结:在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据 弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿 分布式事务与单机事务ACID做对比。

无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们 不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。


  • 请别再问我什么是分布式事务

  • 分布式事务解决方案之2PC、TCC

  • 天天都用消息队列,却不知道为啥要用MQ,这就有点尴尬了

  • 拜托,面试别再问我数据库的分库分表!

  • 学并发编程,透彻理解这三个核心是关键

  • 12306抢票,极限并发带来的思考

  • MySQL高频面试题的灵魂拷问

❤️「转发」和「在看」,是对我最大的支持❤️

你可能感兴趣的:(分布式事务解决方案之可靠消息最终一致性、最大努力通知)