
分布式 - 分布式事务
分布式事务主要解决分布式一致性的问题(数据的分布式操作导致仅依靠本地事务无法保证原子性)。
分布式事务场景
下单扣库存
在电商系统中,下单是用户最常见操作。在下单接口中必定会涉及生成订单 id, 扣减库存等操作,对于微服务架构系统,订单 id 与库存服务一般都是独立的服务,此时就需要分布式事务来保证整个下单接口的成功。
支付
转账是最经典那的分布式事务场景,假设用户 A 使用银行 app 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后增加用户 B 账户中的余额。此时就会出现 2 种异常情况:1. 用户 A 的账户扣款成功,用户 B 账户余额增加失败 2. 用户 A 账户扣款失败,用户 B 账户余额增加成功。对于银行系统来说,以上 2 种情况都是不允许发生,此时就需要分布式事务来保证转账操作的成功。
分布式基础理论
CAP 原则
CAP 是什么?
- 一致性(Consistency) : 所有的客户端都能返回最新的操作。另外对于一致性又区分了三类,不同的类型对于 CAP 理论的实现就有所不同。
- 强一致:任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。
- 弱一致:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
- 最终一致:不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。
- 可用性(Availability) : 非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
- 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成。
CAP 定理
C A P 无法同时满足:
- AP:如果一个系统满足 AP,那么一致性又得不到保证
- CA:如果在某个分布式系统中无副本,那么必然满足强一致性,同时也满足可用性,但是如果这个数据宕机了,那么可用性就得不到保证。
- CP:可用性得不到保障
BASE 定理
BASE 理论是 Basically Available(基本可用),Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。
其核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available
基本可用是相对于正常的系统来说的,常见如下情况:
- 响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给用户结果,而基本可用的搜索引擎可以在 2 秒作用返回结果。
- 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
Soft state
软状态是相对原子性来说的:
- 原子性(硬状态):要求多个节点的数据副本都是一致的,这是一种”硬状态”。
- 软状态(弱状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延迟。
Eventually Consistent
最终一致性是相对强一致性来说的:
- 系统并不保证连续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。但会尽可能保证在某个时间级别(比如秒级别)之后,可以让数据达到一致性状态。最终一致性是弱一致性的特定形式。
- 对于软状态,我们允许中间状态存在,但不可能一直是中间状态,必须要有个期限,系统保证在没有后续更新的前提下,在这个期限后,系统最终返回上一次更新操作的值,从而达到数据的最终一致性,这个容忍期限(不一致窗口的时间)取决于通信延迟,系统负载,数据复制方案设计,复制副本个数等。DNS 就是一个典型的最终一致性系统。
柔性事务
不同于 ACID 的刚性事务,在分布式场景下基于 BASE 理论,就出现了柔性事务的概念。要想通过柔性事务来达到最终的一致性,就需要依赖于一些特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样;但是都不满足的话,是不可能做柔性事务的。
幂等操作
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,支付流程中第三方支付系统告知系统中某个订单支付成功,接收该支付回调接口在网络正常的情况下无论操作多少次都应该返回成功。
分布式事务实现方案
分布式事务实现方案从类型上去分刚性事务、柔型事务。
- 刚性事务:通常无业务改造,强一致性,原生支持回滚/隔离性,低并发,适合短事务。此类实现方案有 XA 协议(2PC、JTA、JTS)、3PC
- 柔性事务:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。此类实现方案有 TCC/FMT、Saga(状态机模式、Aop 模式)、本地事务消息、消息事务(半消息)、最多努力通知型事务
本地消息表
本地消息表最初是由 eBay 架构师 Dan Pritchett 在一篇解释 BASE 原理的论文《Base:An Acid Alternative》(
https://queue.acm.org/detail.cfm?id=1394128)中提及的,业界目前使用这种方案是比较多的,其核心思想是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
基于本地消息表的方案,每个事务发起方都需要额外新建事务消息记录表,用于记录分布式事务的消息的发生、处理状态。
事务发起方在处理完业务逻辑之后需要将当前事务保存在消息表中,之后将消息发送到消息中间件中,并将消息的状态设置为 “发送中”。
如果消息在投递过程中丢失怎么办呢?事务发起方可以设置一个定时任务主动扫描状态为 “发送中” 的消息重新投送。只有消息被业务方消费完毕返回消费成功的结果才确认成功并将消息状态改为“已发送”。
这里因为网络异常或者发送异常导致一个消息可能会被重复发送,所以要求接收方要做到幂等性,允许重复消费。
这种方案的好处就是方案简单,成本较低,实现也不复杂。
但是坏处也有很多,比如通过消息的方式延迟不好控制;本地消息表与业务耦合在一起没有做到通用性;本地消息表基于数据库来实现,有一定的瓶颈。
事务消息
上面说的本地消息表的模式无法支持本地事务执行和消息发送一致性的问题,如果能在本地事务执行和发消息这两个操作上加上事务,那岂不是完美。
基于这个思路, 在 MQ 中存储消息的状态才是真理,消息生产者先把消息发送给 MQ,此时消息状态为“待确认”,接着生产者去执行本地事务,如果执行成功就给 MQ 发送消息让他更改消息状态为 “待发送”并发送消息,如果执行失败则删除消息。
这样就保证了本地事务和消息发送一致性问题。
- 首先事务发起方先往 MQ 发送一条预读消息,这条消息与普通消息的区别在于他只对 MQ 可见不会向下传播。
- MQ 接收到消息后,先进行持久化,则存储中会新增一条状态为
待发送的消息,接着给事务发起方返回处理完成的 ACK;事务发起方收到处理完成的 ACK 之后开始执行本地事务。
- 发起方会根据本地事务的执行状态来决定这个预读消息是应该继续往前还是回滚。另外 MQ 也应该支持自己反查来解决异常情况,如果发起方本地事务已经执行完毕发送消息到 MQ,但是消息因为网络原因丢失,那么怎么解决。所以这个反查机制很重要。
- 本地事务执行成功以后,MQ 也接收到成功通知,接着将消息状态更新为可发送,然后将消息推送给下游的消费者,这个时候消费者就可以去处理自己的本地事务 。
注意点:由于 MQ 通常都会保证消息能够投递成功,因此,如果业务没有及时返回 ACK 结果,那么就有可能造成 MQ 的重复消息投递问题。因此,对于消息最终一致性的方案,消息的消费者必须要对消息的消费支持幂等,不能造成同一条消息的重复消费的情况。
资料:
https://xiaomi-info.github.io/2020/01/02/distributed-transaction/