分布式事务实战:分布式环境下的最终一致性与消息事务

摘要:CAP 理论中的强一致性与可用性的告诉我们两者不可兼得,并由此催生出了 BASE 理论,将强一致性和可用性弱化为最终一致性和基本可用性。本文主要叙述笔者对最终一致性实现的理解,希望对大家有帮助。


1 - 分布式事务

在单机应用上,我们使用事务是很方便的,因为所有的业务逻辑都在本地,数据库事务就能解决 ACID 问题,特别是使用一些J2EE的框架,每一层的业务逻辑都给我们安排得妥妥当当的。

当系统已经被拆分部署到多个服务器实例上时,一般每个服务器都只负责维护一个子系统一张/数张表。与单机相比,业务还是那个业务,但从直接调用本地的下层服务变成了一个远程的RPC调用。

在分布式环境下,一个远程调用是不可靠的(因为网络是不可靠的),我们无法保证在一台服务器上发出的请求一定能在另一台服务器上执行成功,也无法保证执行结果能够准确/准时地返回。有可能被调用的服务执行失败,也有可能执行成功,只是报文丢失。因此导致数据不一致,这些实际上也是分布式事务的问题。

常见的分布式事务方案有:

  1. 两阶段提交
  2. TCC(Try/Confirm/Cancel)
  3. 消息事务+最终一致性

可以参考这篇高点击率文章——高并发下分布式事务的解决方案,文章篇幅较短,但比较全面,可以简单了解这几种方案。

下面会介绍“消息事务+最终一致性”的分布式事务方案,给出我们设计阶段的细节流程供大家参考。

2 - 消息事务与最终一致性的设计

最终一致性不要求系统的数据实时一致,允许数据同步之间存在时延,只要保证最终返回给用户的一定是最新的数据即可。

我们解决分布式事务的核心是将分布式事务转化为本地事务,实现的核心组件是消息中间件/消息队列

2.1 场景

以我们实训课做的互联网教学网站为例。用户可以根据喜好购买视频资源,这会触发以下几个动作:

  1. 用户点击“购买”,发起一条创建订单的请求。
  2. “订单”服务接受到请求,往本地数据库插入一条订单记录,订单状态为“待支付”。
  3. 浏览器重定向到支付页面,用户输入相关支付信息,点击“支付”,发起支付请求。
  4. “支付”服务接受请求后,确认支付成功后,向“订单”服务发起请求,修改本地数据库的订单状态为“已支付”,同时向“课程”服务发起请求,将用户添加到该课程的成员名单下,也是往本地数据库插入一条记录。
  5. 浏览器重定向到购买成功的页面,结束。

这是我们互联网电商平台比较常见的场景了,相信多数人即使没有做过,对这个流程也会一定了解。

其中的步骤 4 就涉及到分布式事务,“支付成功”、“订单状态更新”、“课程名单修改”这三个动作必须一起完成/回滚。
在这里插入图片描述

2.2 @Transactional的本地事务管理

在理想的情况下,我们可以简单地使用下面的顺序图描述这个场景,这里没有任何分布式事务的管理,事务管理仅仅停留在某个方法上的@Transactional,事务只对本地数据库起作用,回滚的也只有本地数据库。
在这里插入图片描述
但受网络影响或者服务器资源问题,请求失败是很可能的,假如某个请求丢失怎么办?某个下游服务宕机了怎么办?对于本地事务而言,已经发出的请求就像泼出去的水,覆水难收。我们必须通过分布式事务执行补偿措施,要么重试,要么回滚。

2.3 最终一致性的事务管理

结合消息中间件实现最终一致性。我们可以使用异步的方式完成下游服务的调用,这样在保证事务的基础上提高了系统的响应速度,只要消息发送成功,不需等待下游服务执行就能响应用户。(可以看到我特意拉长了下游服务的 lifeline)
在这里插入图片描述
在上游服务中,我们需要保证两点:

  1. 如果支付成功,那么消息必须发送成功。
  2. 如果消息发送不成功,支付必须回滚。

简单来说,本地业务逻辑和消息发送必须同时成功/回滚

问:怎么保证?
答:如果我们的消息都是直接发送到消息队列的,比如直接调用send()方法,那么将支付、消息发送放在一个本地事务即可。如果消息发送失败,扔个异常出来,直接回滚本地事务。缺点是不支持多个消息发送,比如,“更新订单状态”的消息发送成功,“修改课程名单”的消息发送失败,哦豁~完蛋,回滚不了,“更新订单状态”的消息已经发送出去了,甚至下游服务可能已经开始执行了。

我们可以引入可靠消息机制,确保无论如何都能将消息发送出去。

在本地维持一个消息发送的库表,消息发送的时候不直接发送到消息队列,而是向库表插入这条消息,只要能插入就算这个消息发送成功了。如果插入失败,回滚事务。后台开一个线程定期扫描库表,如果有消息滞留在库表就尝试发送,发送失败就重试,发送成功就从库表删除消息记录。
在这里插入图片描述
当然,也有极端情况,消息中间件集体挂掉了怎么办,消息怎么重试都失败。要么停止业务,等待中间件重新上线,要么准备消息中间件的替代品。这里涉及到消息中间件的高可用性问题,不多做讨论。推荐 最终一致性分布式事务如何保障实际生产中99.99%高可用?里面有提到使用降级的消息中间件方案。

对于下游服务来说,使用异步的方式,代表只要消息发送成功,我们无论如何都要保证对应的下游服务执行成功,否则就会有一致性的问题出现。

问:怎么保证下游服务一定执行成功呢?
答:可以使用消息中间件的ACK机制,一般来说,消息中间件会提供自动/手动确认,消息被确认后才会被移出队列,也就是被消费掉。我们可以使用手动确认,只有下游服务执行成功才确认消息。否则,消息一直没有被消费,消息中间件就会不断重试,发送消息。

问:消息队列尝试重复发送消息,怎么处理幂等问题
答:正如我们上面所说的,网络是不可靠的,有可能下游服务执行成功了,但是消息队列并没有接收到回传的ACK,就会导致消息队列再次发送消息,下游服务重复收到同一条消息。保证消息的幂等性一般有两种方法:

  1. 在业务逻辑中保证,有些业务逻辑天生就是幂等性的,比如 Redis 的 set,重复 set 一条记录并没有影响;但业务数据库的库表里重复插入一条记录就有可能有问题,我们可以使用主键,重复插入相同的主键数据库会报错;库表字段的更新一般来说也能保证幂等性,重复更新同一个字段影响不大。
  2. 如果无法在业务中保证幂等性,可以增加一个库表来记录已经处理过的消息,使用唯一标识符区分消息。执行前,对照一下库表,如果消息已经执行过就忽略它。

3 - 总结

  1. 异步调用的响应时间短,用户体验好。但不适用于同步调用,比如需要下游服务反馈的业务。
  2. 上游业务需要确保消息的发送、下游业务需要确保消息的消费。
  3. 具体的方案选择还是需要根据业务场景,其他诸如两阶段、TCC的方案也有利有弊。
  4. 引入消息中间件必然增加风险,需要做好权衡。

4 - 参考

  1. 面试:如何保证分布式数据最终一致性
  2. 高并发下分布式事务的解决方案
  3. 拜托,面试请不要再问我TCC分布式事务的实现原理!
  4. 最终一致性分布式事务如何保障实际生产中99.99%高可用?

正文结束,欢迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章