vlambda博客
学习文章列表

架构设计 | 基于消息中间件,图解柔性事务一致性

一、最大努力通知

TCC分段提交适用分布式架构中对一致性、实时性要求较高的业务场景,在实际业务中也存在实时性比较低的业务,例如常见的短信通知,客户端消息,运营体系更新等业务,这时候为了减轻核心流程的复杂度和压力,可以采取最大努力通知方式实现柔性事务的管理。

例如常见的第三方支付业务中,本地业务和支付端业务处理完成之后都会生成消息通知,基本流程如下:

  • 本地业务预处理完成之后;

  • 请求第三方支付服务;

  • 支付操作成功对该账号发送消息;

  • 支付服务回调本地业务;

  • 本地业务生成系统通知消息;

上述流程的消息场景中有一些基础特点,在核心业务处理完成之后,发送消息通知,允许失败,在指定时间段内或者指定重试次数之后,允许消息丢失情况存在,即消息的不可靠性。

在实际的支付系统中,启动每日对账校验时会对当日的流水做校验,如果发现支付流水有未完成的流程,会有状态弥补,后续可以继续处理,这种手段在对账中很常用。

二、可靠消息

分布式事务基于可靠消息最终一致性的实现方案,既然是可靠消息,则要求MQ必须支持事务管理,这样才能保证业务前后一致性。

1、RocketMQ事务消息

RocketMQ在4.3版中开始支持分布式事务消息,采用2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示:

架构设计 | 基于消息中间件,图解柔性事务一致性

上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

1.1 发送及提交

(1)发送消息(half消息,即发送但不被消费);

(2)服务端响应消息写入结果;

(3)根据发送结果执行本地事务,如果写入失败,此时half消息对业务不可见,本地逻辑不执行;

(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

1.1 补偿流程

(1)对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”;

(2)Producer收到回查消息,检查回查消息对应的本地事务的状态;

(3)根据本地事务状态,重新Commit或者Rollback;

其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

1.3 设计原理

在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。

2、最终一致性

基于上述RocketMQ事务消息可靠性的特点,即可以实现某类业务下事务的最终一致性。消息发送一致性是指产生消息的业务动作与消息发送一致,也就是说如果业务操作成功,那么由这个业务操作所产生的异步消息一定要发送出去,否则就业务失败回滚,消息也会丢弃。

流程基本如下:

架构设计 | 基于消息中间件,图解柔性事务一致性

  • 发送half事务消息,无法被消费;

  • 本地业务代码逻辑处理完成;

  • 发送确认消息,标识该消息可以消费;

  • 如果消息生产方异常,取消整体动作;

该流程主要针对消息生产方,在实际开发中,消息的消费方也一样很难处理,要保证最终一致性,必然会面对一个问题,消费方异常,消息不断的重试,可能存在部分业务处理成功,部分业务处理失败的情况,这时候就要解决服务接口的幂等性问题。

三、幂等接口

1、幂等简介

编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。就是说,一次和多次请求某一个资源会产生同样的作用影响。

在复杂的异步流程中,尤其注意失败重试问题,通常支付流程中,每次接口被请求,对每一步数据更新的操作,都会前置一步状态查询的流程,用来判断下一步的数据更新是否该执行。

2、幂等接口

在系统服务接口请求中,任何明确的接口响应,例如失败或成功,这样业务流程都好处理,但是例如支付场景如果请求超时,如何判断服务的结果状态:客户端请求超时,本地服务超时,请求支付超时,支付回调超时,客户端响应超时等,或者基于MQ的不断重试机制,在部分业务异常状态下,始终没有返回成功,则消息会一直重试。

这就需要设计流程化的状态管理,尤其在消息重试机制下,很少会再次对重试的业务接口使用重度的事务控制,有些业务被执行完毕,只需要判断一个状态,下次消息重试跳过即可,只需要把未处理的业务补偿处理即可,在重试机制下,在部分业务没有全部执行成功之前,消息会一直重试,直到最终全部完成。

四、源代码地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent