采用消息中间件实现最终一致性的分布式事务

时间:2022-03-01 00:38:32

基于可靠消息服务的方案是通过消息中间件保证上下游应用数据操作的一致性。假设有A和B两个系统,分别可以处理任务A和任务B。此时存在一个业务流程,需要将任务A和任务B在同一个事务中处理。就可以使用消息中间件来实现这种分布式事务。 采用消息中间件实现最终一致性的分布式事务

第一步:消息由系统A投递到中间件

  1. 在系统A处理任务A前,首先向消息中间件发送一条消息
  2. 消息中间件收到后将该条消息持久化,但并不投递,此时下游系统B仍然不知道该条消息的存在。持久化成功后,向A回复一个确认应答
  3. 系统A收到确认应答后,则可以开始处理任务A
  4. 任务A处理完成后,向消息中间件发送Commit。该请求发送完成后,对系统A而言,该事务的处理过程就结束了
  5. 如果消息中间件收到Commit,则向B系统投递消息,从而触发任务B的执行;
  6. 当任务B执行完成后,系统B向消息中间件返回一个确认应答,此时,这个分布式事务完成

如果任务A处理失败,那么需要进入回滚流程

  • 若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。系统A发完之后便可以认为回滚已经完成,它便可以去做其他的事情
  • 消息中间件收到回滚请求后,直接将该消息丢弃不投递给系统B

超时询问机制

上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。 系统A除了实现正常的业务流程外,还需要提供一个事务询问的接口,供消息中间件调用。当消息中间件收到发布的事务型消息后便开始计时,如果到了超时没收到确认指令,也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果,中间件根据这三种的结果做出不同反应:

  • 提交:将该消息投递给系统B
  • 回滚:直接将这条消息丢弃
  • 处理中:继续等待

第二步:消息由中间件投递到系统B

消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统立即进行任务的处理,任务处理完成后便向消息中间件返回应答。

  • 如果消息中间件收到确认应答后便认为该事务处理完毕
  • 如果消息中间件在等待确认应答超时之后会重新投递,直到下游消费者返回消费成功响应为止。一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要人工干预。这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂度问题。

投递过程的可靠性保证

我们知道当上游系统A发出commit请求之后认为事务已经完成,便可以处理其他的任务了;那么消息中间件是怎么保证消息一定会被下游系统B成功消费呢?这是使用消息中间件投递过程的可靠性来保证的

消息中间件向系统B投递完消息后便进入阻塞等待状态,如果消息在传递过程中丢失或者消息的确认应答在返回途中丢失,那么消息中间件在等待超时后会重新投递直到消息被系统B成功消费为止

之所以是重新投递而不是回滚,这就涉及到整套分布式事务系统的实现成本问题。如果回滚的话,系统A就要提供回滚接口,这增加了开发成本,业务系统的复杂度也会随之提高

异步与同步

基于可靠消息服务的分布式事务,前半部分使用异步,注重性能;后半部分使用同步,注重开发成本。

上游系统A向消息中间件提交完消息后便可以去做别的事情。然而消息中间件将消息投递给下游系统B后,它会阻塞等待直到下游系统返回B确认应答。 之所以这么设计:

  • 首先:上游系统和消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道,用户体验尤为重要,因此这种异步通信方式能够极大程度地降低用户等待时间;
  • 其次:下游系统与消息中间件采用同步虽然降低系统并发度,但实现成本较低。在对并发度要求不是很高或者服务器资源较为充裕的情况下,我们可以选择使用同步来降低系统的复杂度