分布式事务
https://www.bilibili.com/video/BV1U341157BX
事务ACID
在分布式场景下ACID特性含义会被扩展。
- 原子性(Atomicity)一个事务内涉及到的分布式多个节点的操作要么一起成功,要么一起失败
- 一致性(Consistency):跨节点场景下也不能出现中间状态。
- 隔离性(Isolation):一个分布式事务在一个节点哪怕完成了,在整个分布式事务完成之前,也不能被其他事务读取使用
- 持久性(Durability)事务提交完成后的数据不仅要在当前节点保存,还应该在分布式场景下有备份高克yognxing
早期分布式事务的三个方向分别是
- 俩阶段模型,XA事务模型
- TCC事务模型
- Saga事务模型
常见的方案
俩阶段提交
一个直接的思路就是三个系统都等到整个交易快照完成时再一起提交。根据这个思路就诞生了俩阶段提交的方法,
阶段1
协调器收到客户端commit命令,决定提交T。然后协调器向各节点发送prepare T消息,各节点决定该节点上T是提交还是中止并返回给协调器。
第二阶段
协调器收到各个节点的ready或don't commit响应,如果都是ready,那么就决定提交T,并向所有节点发送commit消息;如果收到来自一个节点或多个节点的don't commit T的消息,就决定丢弃T,并向所有节点发送abort的消息。
XA事务就是把俩阶段提交标准化后形成指定的数据库接口,再由一个事务管理器作为协调者统一管理俩阶段提交,相当于标准化的俩阶段提交。
很多单机数据库,如oracle、DB2、Mysql支持XA接口,应用程序实现的一个事务管理器,然后跳读下面数据库的接口实现俩阶段提交,从而实现分布式事务。XA事务允许各个系统选择不同类型的数据库,只要它们都支持XA接口。
TCC事务
TCC将整个业务逻辑分为三块Try、comfirm,和cancel三个操作
Try就是尝试扣除用户月、尝试扣除库存;confirm就是Try的结果都成功,可以证实把订单状态状态改为已支付,通知仓库发货;而cancel则是try失败,那么就要取消教育,回复月,回复库存。TCC是业务逻辑补偿的应用层设计实现。
TCC的一个关键点是,Try阶段如果完成,那么很多状态就已经保存起来,再出现故障也不会影响事务的晚餐,也就是说,哪怕confirm阶段或cancel阶段失败,也会通过不断重试完成交易,而不是回滚。
TCC的过程类似于俩阶段提交,Try过程和俩阶段prepare比较相似。但TCC没有事务管理的抽象,Try、confirm、cancel和业务场景密切绑定,要考虑具体业务来实现。
SATA事务模型
Saga本来是为解决长事务而出现的分布式事务模型,长事务的是需要太长时间而不允许它们保持其他事务所需要的锁的事务。
电商交易如果采用俩阶段提交,则订单系统的子系统要等到用户确认收货交易完成才能提交,而物流的时间一般比较长,这个时间可能需要几天。而订单系统事务可能会拖住一些资源,如用户的余额,这个锁维持几天的时间,会导致用户无法消费,这显然是不可行的。
Saga是把分布式事务看做一个个单机事务的合集,对每一个单机事务A,Saga都有一个补偿事务$A^{-1}$。如果我们执行A,又执行$A^{-1}$,那么锁产生的数据库状态同A和$A^{-1}$都未执行前一样。
同样都是需要业务逻辑接入,其中TCC适用于对一致性要求高、性能敏感的场景。
SAGA的核心的回滚,像一个执行-如失败反向补偿的厂业务流程,适用于执行流程长,业务流程复杂的场景。
体现在分布式整体上,只要我们能确认一个单机事务都执行成功,那么整个分布式事务就是成功的,可以提交。如果任意一个单机事务不成功,那么就要把它和它之前的单机事务回滚掉,但它之前的单机事务已经提交过了,无法回滚,这时候就需要执行它们的补偿事务。
电商交易系统的例子可以通过设计补偿事务,把库存和用户余额加回,来实现分布式事务。
分布式事务模型
在单机数据库的基础上是心爱一个中间层(proxy),中间层基于单机事务加saga来实现分布式事务。高可用通过单机数据库的主备复制实现。
如ShardingSphere-Proxy中,可以通过配置将分布式事务管理委托给Seata。当ShardingSphere-Proxy接收到一个跨多个数据库节点的事务请求时,它会将事务信息发送给Seata的全局事务协调器。全局事务协调器根据事务类型(如Base事务)和配置规则,对事务进行协调和管理。在事务执行过程中,ShardingSphere-Proxy会负责将事务操作路由到相应的数据库节点,并监控事务的执行状态。一旦事务执行完成,Seata会根据事务的执行结果,进行全局的提交或回滚操作,确保分布式事务的原子性和一致性。
原生数据库:它们基于俩阶段提交实现分布式事务,不存在单机事务。比如蚂蚁的oceanbase和TIDB。
SAGA生成补偿事务的思想是通用的,分布式数据库可以通过解析单机事务的SQL,从而生成逆SQL,从而生成补偿事务(比如AT模式)
实现这一点要满足俩个条件
- 要有一个标志把同一个分布式事务的所有单机事务联系起来,以便在需要执行补偿事务的时候能找到它们
- 有的单机事务已提交,但它所在的分布式事务未提交,这时它提交的结果在分布式的角度仍处于脏数据,通常不允许其他事务对其进行读取。要单独进行处理。(通常使用分布式锁来实现)
TCC的一致性比SAGA更强。SAGA是可以直接提交和直接回滚,但TCC要先try。
前面介绍的俩阶段提交原型,可以看出,俩阶段提交对事物管理器的抽象天然适合封装在分布式数据库内部,这也是大部分原生数据库选择俩阶段提交的原因。

俩阶段提交有一个显著的特点:事务只有在prepare阶段才会在数据行上锁,整个执行过程都是无锁的,这意味着并行的事务很有可能修改了同一行的数据。
也就是说俩阶段提交是first commit win,是一种乐观事务模型(假设事务没有冲突发生)。
目前一些商业的原生分布式数据库对俩阶段提交的加锁进行了改造,支持了悲观锁(即在事务执行阶段就加锁),这一步拓展原生数据库的支持能力。
此外最新版本的俩阶段提交有严重的问题:协调及存在高可用问题,一旦宕机,在恢复或者新的协调器被选举出来之前,整个系统是不可用的。
google的percolator事务模型对俩阶段提交问题做出了优化:
把收到分布式事务请求的第一个节点作为俩阶段提交的协调者节点,一存储节点的高可用保证了协调节点的高可用,同时也不会出现些调整宕机了整个系统都不用的开启,因为任何一个节点都可以是协调者节点了。
此外Percolator在事务开始时和事务提交时都会取一个时间戳并写入数据库中,这个时间戳可以很便捷的支持分布式MVCC的实现。
数据库锁
所是用来管理共享资源的访问的机制。在数据库中除了用户数据要上锁,还会对缓冲池中LRU列表、数据库元数据等其他资源上锁。不同数据库对锁实现差异很大。Oracle、Mysql大部分都是采用行锁设计。
单机数据库的行锁会分为俩中:共享锁(S)和排它所(X),只有贡献锁之间才兼容。
以mysql为例slect ... lock in share mode上的是S锁,不会阻塞后续的其他share mode。
update/select... for update上的是X锁会阻塞读的操作,包括其他的share mode锁。
事务中的锁还有很重要的概念,交俩阶段锁理论(2Phase lock)加锁阶段和解锁阶段是俩个独立的阶段,不能有重叠。俩阶段锁理论和事务的原子性以及隔离性关系,密切,是保证并发调度准确性的必要条件。加锁是执行阶段,解锁是提交阶段。
在单机事务中大家很少考虑加锁和解锁的过程与代价。所资源默认堆所有事务是可见的,一行数据的加锁和解锁都可以看做原子操作。
在分布式数据库中,数据行分布在多台机器上,因为跨节点网络传输的不可靠性,加锁和解锁不在是实时切原子的了。
那么保证分布式上锁的正确性,需要在分布式事务中维持一个中心节点来记录所信息。加锁与解锁的成功和失败都以中心节点为准。
在编排式SAGA中,有一个中央协调者(Saga协调器)负责管理和控制整个事务的执行流程。协调者按照预定义的顺序依次触发每个本地事务,并在事务失败后处理补偿事务。这种方式逻辑清晰、易于管理,但存在单点故障风险。
而在协同式SAGA中,没有中央协调者,各个服务通过事件驱动的方式进行协调。每个服务自己决定何时开始下一个本地事务,通过订阅和发布事件来触发后续操作。这种方式去中心化、服务间耦合度低,但事务流程相对复杂,难以追踪。
SAGA:独立中心节点
俩阶段提交:协调者节点
任何情况下出现了中心节点就要考虑高可用,通常使用共识算法来实现对于锁表和协调者的高可用性。Percolator直接把俩阶段提交的协调者放在了分布式事务的一个随机的数据节点,把协调者的高可用问题转换成了数据节点高可用问题。
分布式锁通常只有X锁,因为实现分布式锁的代价本来就很大,如果还要实现分布式S锁就会增加非常多的维护和调度代价,并且S锁的使用本来就很少,因此一般没有实现分布式S锁的方案。对于select ... lock in share mode语句分布式数据库一般不支持(原生分布式数据库)或选择只在单机节点上S锁。
隔离级别
现在大部分关系型数据库都是基于MVCC的,要引入一个针对快照的新的隔离级别。SI(snapshot Isilation,SI)
SI隔离级别会去读书屋开始时间点的数据快照,从而避免不可重复读,但又不像用锁实现的可重复读隔离级别那样要阻塞写。(如果始终读最新的快照,则相当于RC,但我们将SI,一般是事务开始的时候的快照)
SI和RR的区别就是幻读和排序写。
我们把点查询成为item查询,非点查询(范围聚合等),称为predicate查询
因此我们可以把幻读定义为不可重复读的predicate版本。幻读和不可重复读都需要在一次事务中进行俩次读操作。不可重复读指的是俩次item查询到了不同的内容,而幻读指的是俩次predicate查询到了不同的结果。