分布式事务及解决方案
基础概念
什么是事务
什么是事务?举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是一个事务的例子,交钱和交货必
须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。
明白上述例子,再来看事务的定义:
事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。
本地事务
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据
库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的
事务又被称为本地事务。
回顾一下数据库事务的四大特性 ACID:
- A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失
败的情况。
- C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元,
转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出100元,李四账户没有增加100元这就出现了数
据错误,就没有达到一致性。
- I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事
务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。
- D(Durability):持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作
要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚
分布式事务
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操
作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分
事务、创建订单减库存事务,银行转账事务等都是分布式事务。
我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:
1 | begin transaction; |
但是在分布式环境下,会变成下边这样:
1 | begin transaction; |
可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚
了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应
用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
分布式事务产生的场景
- 典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的
同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务。
- 单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信
息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信
息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨
数据库实例产生分布式事务。
3、多服务访问同一个数据库实例 比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原
因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。
分布式事务基础理论
通过前面的介绍,我们了解到了分布式事务的基础概念。与本地事务不同的是,分布式系统之所以叫分布式,是因为提供服务的各个节点分布在不同机器上,相互之间通过网络交互。不能因为有一点网络问题就导致整个系统无法提供服务,网络因素成为了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持,接下来,我们先引入分布式事务的CAP理论。在讲解分布式事务控制解决方案之前需要先学习一些基础理论,通过理论知识指导我们确定分布式事务控制的目标,从而帮助我们理解每个解决方案。
CAP理论
理解CAP
CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍
性。
下边我们分别来解释:
为了方便对CAP理论的理解,我们结合业务场景来理解CAP。
如下图,是商品信息管理的执行流程:
整体执行流程如下:
商品服务请求主数据库写入商品信息(添加商品、修改商品、删除商品)
主数据库向商品服务响应写入成功。
商品服务请求从数据库读取商品信息。
C - Consistency:
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。
上图中,商品信息的读写要满足一致性就是要实现如下目标:
商品服务写入主数据库成功,则向从数据库查询新数据也成功。
商品服务写入主数据库失败,则向从数据库查询新数据也失败。
如何实现一致性?
写入主数据库后要将数据同步到从数据库。
写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。分布式系统一致性的特点:
由于存在数据同步的过程,写操作的响应会有一定的延迟。
为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。
A - Availability :
可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。
上图中,商品信息读取满足可用性就是要实现如下目标:
从数据库接收到数据查询的请求则立即能够响应数据查询结果。
从数据库不允许出现响应超时或响应错误。
如何实现可用性?
写入主数据库后要将数据同步到从数据库。
由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照 约定返回一个默认信息,但不能返回错误或响应超时。
分布式系统可用性的特点:所有请求都有响应,且不会出现响应超时或响应错误。
P - Partition tolerance :
通常分布式系统的各个结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,这叫分区容忍性。
上图中,商品信息读写满足分区容忍性就是要实现如下目标:
主数据库向从数据库同步数据失败不影响读写操作。
其一个结点挂掉不影响另一个结点对外提供服务。
如何实现分区容忍性?
尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。
添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。分布式分区容忍性的特点:
分区容忍性分是布式系统具备的基本能力。
CAP组合方式
同时具备 CAP?
在所有分布式事务场景中不会同时具备CAP三个特性,因为在具备了P的前提下C和A是不能共存的。
比如:
下图满足了P即表示实现分区容忍:
本图分区容忍的含义是:
主数据库通过网络向从数据同步数据,可以认为主从数据库部署在不同的分区,通过网络进行交互。
当主数据库和从数据库之间的网络出现问题不影响主数据库和从数据库对外提供服务。
其一个结点挂掉不影响另一个结点对外提供服务。
如果要实现C则必须保证数据一致性,在数据同步的时候为防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败从数据库要返回错误信息或超时信息。
如果要实现A则必须保证数据可用性,不管任何时候都可以向从数据查询数据,则不会响应超时或返回错误信息。通过分析发现在满足P的前提下C和A存在矛盾性。
CAP的组合方式
所以在生产中对分布式事务处理时要根据需求来确定满足 CAP 的哪两个方面。
AP:放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。 例如:
上边的商品管理,完全可以实现AP,前提是只要用户可以接受所查询的到数据在一定时间内不是最新的即可。通常实现AP都会保证最终一致性,后面讲的BASE理论就是根据AP来扩展的,一些业务场景 比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。
CP:放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。
CA:放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,我们最常用的关系型数据就满足了CA。
上边的商品管理,如果要实现CA则架构如下:
主数据库和从数据库中间不再进行数据同步,数据库可以响应每次的查询请求,通过事务隔离级别实现每个查询请求都可以返回最新的数据。
CAP 在实际应用中
- 注册中心,选择 CP 还是 AP?
- 分布式锁,选择 CP 还是 AP?
- 分布式事务,选择 CP 还是 AP?
注册中心,选择 CP 还是 AP?
在讨论 CAP 之前先明确下服务注册中心主要是解决什么问题:
- 服务注册:实例将自身服务信息注册到注册中心,这部分信息包括服务的主机 IP 和服务的 Port,以及暴露服务自身状态和访问协议信息等。
- 服务发现:实例请求注册中心所依赖的服务信息,服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
Zookeeper 选择 CP
Zookeeper 保证 CP,即任何时刻对 Zookeeper 的访问请求能得到一致性的数据结果,同时系统对网络分割具备容错性,但是它不能保证每次服务的可用性。
从实际情况来分析,在使用 Zookeeper 获取服务列表时,如果 ZK 正在选举或者 ZK 集群中半数以上的机器不可用,那么将无法获取数据。所以说,ZK 不能保证服务可用性。
Eureka 选择 AP
Eureka 保证 AP,Eureka 在设计时优先保证可用性,每一个节点都是平等的。
一部分节点挂掉不会影响到正常节点的工作,不会出现类似 ZK 的选举 Leader 的过程,客户端发现向某个节点注册或连接失败,会自动切换到其他的节点。
只要有一台 Eureka 存在,就可以保证整个服务处在可用状态,只不过有可能这个服务上的信息并不是最新的信息。
ZK 和 Eureka 的数据一致性问题
先要明确一点,Eureka 的创建初心就是为一个注册中心,但是 ZK 更多是作为分布式协调服务的存在。
只不过因为它的特性被 Dubbo 赋予了注册中心,它的职责更多是保证数据(配置数据,状态数据)在管辖下的所有服务之间保持一致。
所以这个就不难理解为何 ZK 被设计成 CP 而不是 AP,ZK 最核心的算法 ZAB,就是为了解决分布式系统下数据在多个服务之间一致同步的问题。
更深层的原因,ZK 是按照 CP 原则构建,也就是说它必须保持每一个节点的数据都保持一致。
如果 ZK 下节点断开或者集群中出现网络分割(例如交换机的子网间不能互访),那么 ZK 会将它们从自己的管理范围中剔除,外界不能访问这些节点,即使这些节点是健康的可以提供正常的服务,所以导致这些节点请求都会丢失。
而 Eureka 则完全没有这方面的顾虑,它的节点都是相对独立,不需要考虑数据一致性的问题,这个应该是 Eureka 的诞生就是为了注册中心而设计。
相对 ZK 来说剔除了 Leader 节点选取和事务日志机制,这样更有利于维护和保证 Eureka 在运行的健壮性。
再来看看,数据不一致性在注册服务中会给 Eureka 带来什么问题,无非就是某一个节点被注册的服务多,某个节点注册的服务少,在某一个瞬间可能导致某些 IP 节点被调用数多,某些 IP 节点调用数少的问题。
也有可能存在一些本应该被删除而没被删除的脏数据。
服务注册应该选择 AP 还是 CP
对于服务注册来说,针对同一个服务,即使注册中心的不同节点保存的服务注册信息不相同,也并不会造成灾难性的后果。
对于服务消费者来说,能消费才是最重要的,就算拿到的数据不是最新的数据,消费者本身也可以进行尝试失败重试。总比为了追求数据的一致性而获取不到实例信息整个服务不可用要好。
所以,对于服务注册来说,可用性比数据一致性更加的重要,选择 AP。
分布式锁,是选择 CA 还是选择 CP?
这里实现分布式锁的方式选取了三种:
- 基于数据库实现分布式锁
- 基于 Redis 实现分布式锁
- 基于 Zookeeper 实现分布式锁
基于数据库实现分布式锁
构建表结构:
利用表的 UNIQUE KEY idx_lock(method_lock)作为唯一主键,当进行上锁时进行 Insert 动作,数据库成功录入则以为上锁成功,当数据库报出 Duplicate entry 则表示无法获取该锁。
不过这种方式对于单主却无法自动切换主从的 MySQL 来说,基本就无法实现 P 分区容错性(MySQL 自动主从切换在目前并没有十分完美的解决方案)。
可以说这种方式强依赖于数据库的可用性,数据库写操作是一个单点,一旦数据库挂掉,就导致锁的不可用。这种方式基本不在 CAP 的一个讨论范围。
基于 Redis 实现分布式锁
Redis 单线程串行处理天然就是解决串行化问题,用来解决分布式锁是再适合不过。
实现方式:
1 | setnx key value Expire_time 获取到锁 返回 1 , 获取失败 返回 0 |
为了解决数据库锁的无主从切换的问题,可以选择 Redis 集群,或者是 Sentinel 哨兵模式,实现主从故障转移,当 Master 节点出现故障,哨兵会从 Slave 中选取节点,重新变成新的 Master 节点。
哨兵模式故障转移是由 Sentinel 集群进行监控判断,当 Maser 出现异常即复制中止,重新推选新 Slave 成为 Master,Sentinel 在重新进行选举并不在意主从数据是否复制完毕具备一致性。
所以 Redis 的复制模式是属于 AP 的模式。保证可用性,在主从复制中“主”有数据,但是可能“从”还没有数据。
这个时候,一旦主挂掉或者网络抖动等各种原因,可能会切换到“从”节点,这个时候可能会导致两个业务线程同时获取得两把锁。
这个过程如下:
- 业务线程 -1 向主节点请求锁
- 业务线程 -1 获取锁
- 业务线程 -1 获取到锁并开始执行业务
- 这个时候 Redis 刚生成的锁在主从之间还未进行同步
- Redis 这时候主节点挂掉了
- Redis 的从节点升级为主节点
- 业务线程 -2 想新的主节点请求锁
- 业务线程 -2 获取到新的主节点返回的锁
- 业务线程 -2 获取到锁开始执行业务
- 这个时候业务线程 -1 和业务线程 -2 同时在执行任务
上述的问题其实并不是 Redis 的缺陷,只是 Redis 采用了 AP 模型,它本身无法确保我们对一致性的要求。
Redis 官方推荐 Redlock 算法来保证,问题是 Redlock 至少需要三个 Redis 主从实例来实现,维护成本比较高。
相当于 Redlock 使用三个 Redis 集群实现了自己的另一套一致性算法,比较繁琐,在业界也使用得比较少。
能不能使用 Redis 作为分布式锁?这个本身就不是 Redis 的问题,还是取决于业务场景。
我们先要自己确认我们的场景是适合 AP 还是 CP , 如果在社交发帖等场景下,我们并没有非常强的事务一致性问题,Redis 提供给我们高性能的 AP 模型是非常适合的。
但如果是交易类型,对数据一致性非常敏感的场景,我们可能要寻找一种更加适合的 CP 模型。
基于 Zookeeper 实现分布式锁
刚刚也分析过,Redis 其实无法确保数据的一致性,先来看 Zookeeper 是否适合作为我们需要的分布式锁。
首先 ZK 的模式是 CP 模型,也就是说,当 ZK 锁提供给我们进行访问的时候,在 ZK 集群中能确保这把锁在 ZK 的每一个节点都存在。
这个实际上是 ZK 的 Leader 通过二阶段提交写请求来保证的,这个也是 ZK 的集群规模大了的一个瓶颈点。
①ZK 锁实现的原理
说 ZK 的锁问题之前先看看 Zookeeper 中几个特性,这几个特性构建了 ZK 的一把分布式锁。
ZK 的特性如下:
- 有序节点:当在一个父目录下如 /lock 下创建 有序节点,节点会按照严格的先后顺序创建出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保证各个自节点按照排序命名生成。
- 临时节点:客户端建立了一个临时节点,在客户端的会话结束或会话超时,Zookepper 会自动删除该节点 ID。
- 事件监听:在读取数据时,我们可以对节点设置监听,当节点的数据发生变化(1 节点创建 2 节点删除 3 节点数据变成 4 自节点变成)时,Zookeeper 会通知客户端。
结合这几个特点,来看下 ZK 是怎么组合分布式锁:
- 业务线程 -1,业务线程 -2 分别向 ZK 的 /lock 目录下,申请创建有序的临时节点。
- 业务线程 -1 抢到 /lock0001 的文件,也就是在整个目录下最小序的节点,也就是线程 -1 获取到了锁。
- 业务线程 -2 只能抢到 /lock0002 的文件,并不是最小序的节点,线程 2 未能获取锁。
- 业务线程 -1 与 lock0001 建立了连接,并维持了心跳,维持的心跳也就是这把锁的租期。
- 当业务线程 -1 完成了业务,将释放掉与 ZK 的连接,也就是释放了这把锁。
②ZK 分布式锁的代码实现
ZK 官方提供的客户端并不支持分布式锁的直接实现,我们需要自己写代码去利用 ZK 的这几个特性去进行实现。
究竟该用 CP 还是 AP 的分布式锁
首先得了解清楚我们使用分布式锁的场景,为何使用分布式锁,用它来帮我们解决什么问题,先聊场景后聊分布式锁的技术选型。
无论是 Redis,ZK,例如 Redis 的 AP 模型会限制很多使用场景,但它却拥有了几者中最高的性能。
Zookeeper 的分布式锁要比 Redis 可靠很多,但他繁琐的实现机制导致了它的性能不如 Redis,而且 ZK 会随着集群的扩大而性能更加下降。
总结
CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99..%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证P和A,舍弃C强一致,保证最终一致性。
基于CAP理论,分布式事务必须在AP和CP模式之间进行选择,而A(可靠性)对于在线业务来说又是不可或缺的,所以最后对于分布式事务的处理则演变为现在的AP加数据最终一致性方案。
BASE理论 — AP 方案的补充
理解强一致性和最终一致性
CAP理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边我们举的例子主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。
Base理论介绍
BASE 是 **Basically Available(基本可用)、Soft state(软状态)**和 **Eventually consistent (最终一致性)**三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。
- Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出现问题了,商品依然可以正常浏览。
- Soft state(软状态):由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的”支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
- Eventually consistent (最终一致性):最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的”支付中”状态,最终会变为“支付成功”或者”支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
分布式事务解决方案
- XA/2PC
- TCC
- SAGA
- 基于消息的分布式事务
- 最大努力通知型分布式事务
XA Specification/2PC 和 DTP 模型
最早的分布式事务产品可能是 AT&T 在 20 世纪 80 年代推出的 Tuxedo (Transactions for Unix, Extended for Distributed Operations),Tuxedo 最早是为了电信领域的 OLTP 系统研发的分布式事务中间件,后来标准化组织 X/Open 吸收采纳了 Tuxedo 的设计思想和一些接口,推出了==分布式事务规范:XA Specification==。
DTP 模型
XA 规范中定义了分布式事务处理模型(DTP),这个模型中包含四个核心角色:
- RM (Resource Managers):资源管理器,提供数据资源的操作、管理接口,保证数据的一致性和完整性。最有代表性的就是数据库管理系统,当然有的文件系统、MQ 系统也可以看作 RM。
- TM (Transaction Managers):事务管理器,是一个协调者的角色,协调跨库事务关联的所有 RM 的行为。
- AP (Application Program):应用程序,按照业务规则调用 RM 接口来完成对业务模型数据的变更,当数据的变更涉及多个 RM 且要保证事务时,AP 就会通过 TM 来定义事务的边界,TM 负责协调参与事务的各个 RM 一同完成一个全局事务。
- CRMs (Communication Resource Managers):主要用来进行跨服务的事务的传播。
2PC
XA 规范中分布式事务是构建在 RM 本地事务(此时本地事务被看作分支事务)的基础上的,TM 负责协调这些分支事务要么都成功提交、要么都回滚。XA 规范把分布式事务处理过程划分为两个阶段,所以又叫两阶段提交协议(two phrase commit):
- 预备阶段
TM 记录事务开始日志,并询问各个 RM 是否可以执行提交准备操作。
RM 收到指令后,评估自己的状态,尝试执行本地事务的预备操作:预留资源,为资源加锁、执行操作等,但是并不提交事务,并等待 TM 的后续指令。如果尝试失败则告知 TM 本阶段执行失败并且回滚自己的操作,然后不再参与本次事务(以 MySQL 为例,这个阶段会完成资源的加锁,redo log 和 undo log 的写入)。
TM 收集 RM 的响应,记录事务准备完成日志。
- 提交/回滚阶段
这个阶段根据上个阶段的协调结果发起事务的提交或者回滚操作。
如果所有 RM 在上一个步骤都返回执行成功,那么:
- TM 记录事务 commit 日志,并向所有 RM 发起事务提交指令。
- RM 收到指令后,提交事务,释放资源,并向 TM 响应“提交完成”。
- 如果 TM 收到所有 RM 的响应,则记录事务结束日志。
如果有 RM 在上一个步骤中返回执行失败或者超时没有应答,则 TM 按照执行失败处理,那么:
- 记录事务 abort 日志,向所有 RM 发送事务回滚指令。
- RM 收到指令后,回滚事务,释放资源,并向 TM 响应回滚完成。
- 如果 TM 收到所有 RM 的响应,则记录事务结束日志。
针对部分场景,XA 规范还定义了如下优化措施:
- 如果 TM 发现整个事务只涉及到一个 RM,那么就会将整个过程退化为一阶段提交。
- 如果 RM 收到的 AP 的数据操作是只读操作,那么它可以在阶段 1 就将事务完成并告知 TM 其不再参与阶段 2 的过程。会有脏读的风险。
- 如果 RM 在阶段1完成后,长时间等不到阶段 2 的指令,那么其可以自动提交或者回滚本地事务。这叫做 Heuristic Completion,注意这种场景有可能会破坏事务的一致性,产生异常。
XA 规范中详细定义了各个核心组件之间的交互接口,以 TM 和 RM 的交互接口为例,如下图,一次完整的全局事务,TM 和 RM 之间的交互还是比较频繁的:
事务的执行过程中,宕机和网络超时都有可能发生,针对这些异常场景,不同 XA 规范的实现,对异常处理做法可能不同,可参考如下:
- TM 在阶段 1 中询问 RM 前宕机,恢复后无需做任何操作。
- TM 在阶段 1 中询问 RM 后宕机,可能只有部分 RM 收到了阶段 1 的请求,因此此时需要向 RM 发起回滚请求。
- TM 在阶段 1 中询问 RM 完毕,但是在就准备完成日志时宕机,因不清楚宕机前的事务协商的结果,因此恢复后需要向 RM 发起回滚请求。
- TM 在阶段 1 中记录完毕事务准备完成日志后宕机,恢复后可以根据日志发起提交或者回滚的指令。
- TM 在阶段 2 中记录 commit/abort 日志前宕机,恢复后可以根据日志发起提交或者回滚指令。
- TM 在阶段 2 中记录事务结束日志前宕机,恢复后可以根据日志发起提交或者回滚指令。
- TM 在阶段 2 中记录事务结束日志后宕机,恢复后无需做任何操作。
- 阶段 1 中,RM 有超时情况时,TM 按失败处理,给所有 RM 发送回滚指令。
- 阶段 2 中,RM 有超时情况是,TM 需要对超时的 RM 持续重复发送指令。
举例:MYSQL如何实现XA规范
在 MySQL 的 InnoDB 存储引擎中,开启 binlog 的情况下,MySQL 会同时维护 binlog 日志与 InnoDB 的 redo log,为了保证这两个日志的一致性,MySQL 使用了 XA 事务。
Binlog 中的 Xid
当事务提交时,在 binlog 依赖的内部 XA 中,额外添加了 Xid 结构,binlog 有多种数据类型,包括以下三种:
- statement 格式,记录为基本语句,包含 Commit
- row 格式,记录为基于行
- mixed 格式,日志记录使用混合格式
不论是 statement 还是 row 格式,binlog 都会添加一个 XID_EVENT 作为事务的结束,该事件记录了事务的 ID 也就是 Xid,在 MySQL 进行崩溃恢复时根据 binlog 中提交的情况来决定如何恢复。
Binlog 同步过程
下面来看看 Binlog 下的事务提交过程,整体过程是先写 redo log,再写 binlog,并以 binlog 写成功为事务提交成功的标志。
当有事务提交时:
- 第一步,InnoDB 进入 Prepare 阶段,并且 write/sync redo log,写 redo log,将事务的 XID 写入到 redo 日志中,binlog 不作任何操作;
- 第二步,进行 write/sync Binlog,写 binlog 日志,也会把 XID 写入到 Binlog;
- 第三步,调用 InnoDB 引擎的 Commit 完成事务的提交,将 Commit 信息写入到 redo 日志中。
如果是在第一步和第二步失败,则整个事务回滚;如果是在第三步失败,则 MySQL 在重启后会检查 XID 是否已经提交,若没有提交,也就是事务需要重新执行,就会在存储引擎中再执行一次提交操作,保障 redo log 和 binlog 数据的一致性,防止数据丢失。
在实际执行中,还牵扯到操作系统缓存 Buffer 何时同步到文件系统中,所以 MySQL 支持用户自定义在 Commit 时如何将 log buffer 中的日志刷到 log file 中,通过变量 innodb_flush_log_at_trx_Commit
的值来决定。在 log buffer 中的内容称为脏日志,感兴趣的话可以查询资料了解下。
特点剖析
XA 是出现最早的分布式事务规范,主流数据库 Oracle、MySQL、SQLServer 等都支持 XA 规范,J2EE 中的 JTA 规范也是参照 XA 规范编写的,与 XA 规范兼容。
XA 两阶段提交协议设计上是要像本地事务一样实现事务的 ACID 四个特性:
- 原子性:在 prepare 和 commit 阶段保证事务是原子性的。
- 一致性:XA 协议实现的是==强一致性==。
- 隔离性:XA 事务在完成之前一直持有资源的锁,所以可以做到写隔离。
- 持久性:基于本地事务实现,所以这一点没有问题。
XA 是在资源管理层面实现的分布式事务模型,对业务的==入侵度较低==。
XA 两阶段提交协议可以覆盖分布式事务的三种场景,但是全局事务的执行过程中,RM ==一直持有资源的锁==,如果参与的 RM 过多,尤其是跨服务的场景下,网络通信的次数和时间会急剧变多,所以阻塞的时间更长,系统的吞吐能力变得很差,事务死锁出现的概率也会变大,所以并不适合微服务架构场景中的跨服务的分布式事务模式。
每一个 TM 域来说,由于 TM 是单点,==存在单点故障风险==,如果 TM 在阶段1之后挂掉,会导致参与的 RM 长时间收不到阶段 2 的请求而长期持有资源的锁,影响业务的吞吐能力。同时一次完整的全局事务,TM 和 RM 之间的交互多达 8 次,太繁琐,非常影响系统的处理性能。
XA 两阶段协议==可能会造成脑裂的异常==,假如 TM 在阶段 2 通知 RM 提交事务时,如果指令发出后就宕机了,而只有部分 RM 收到了提交请求,那么当 TM 恢复的时候,就无法协调本次事务所有的 RM 本地事务的一致性了。
XA 要处理的异常场景非常多,对框架的实现有一定的挑战,开源的实现,可以参考:Atomikos,Bitronix。
针对 XA 两阶段提交中的问题,有人提出了三阶段提交的改进方案,三阶段提交方案主要解决了单点故障问题,并在 RM 侧也引入了超时机制,以避免资源的长时间锁定。但是三阶段提交方案依然无法避免脑裂的异常情况出现,实际应用案例很少,感兴趣的同学可以自行找相关资料了解。
TCC
TCC (Try、Commit、Cancel) 是一种补偿型事务,该模型要求应用的每个服务提供 try、confirm、cancel 三个接口,它的核心思想是通过对资源的预留(提供中间态),尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源。
TCC 也是一种两阶段提交协议,可以看作 2PC/XA 的一种变种,但是不会长时间持有资源锁。
TCC 模型将事务的提交划分为两个阶段:
- 阶段 1
完成业务检查(一致性)、预留业务资源(准隔离性),即 TCC 中的 try。
- 阶段 2
如果 try 阶段所有业务资源都预留成功,则执行 confirm 操作,否则执行 cancel 操作:
- confirm:不做任何业务检查,仅仅使用预留的资源执行业务操作,如果失败会一直重试。
- cancel:取消执行业务操作,释放预留的资源,如果失败会一直重试。
Try 阶段: 调用 Try 接口,尝试执行业务,完成所有业务检查,预留业务资源。
Confirm 或 Cancel 阶段: 两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。
- Confirm 操作: 对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用 Try 阶段预留的业务资源。
- Cancel 操作: 在业务执行错误,需要回滚的状态下执行业务取消,释放预留资源。
Try 阶段失败可以 Cancel,如果 Confirm 和 Cancel 阶段失败了怎么办?
TCC 中会添加事务日志,如果 Confirm 或者 Cancel 阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等。
举例
我们以一个简单的电商系统为例,小明在淘宝上花 100 元买了一本书,获赠 10 个积分,产品上有如下几个操作:
- 订单系统创建商品订单
- 支付系统接受小明的支付
- 库存系统扣减产品库存
- 会员系统给小明账户增加会员积分
这几个动作需要作为一个事务执行,要同时成功或者同时撤销。
如果采用 TCC 事务模式,那么各个系统需要改造为如下状态:
1)订单系统
- try:创建一个订单,状态显示为“待支付”
- confirm:更新订单的状态为“已完成”
- cancel:更新订单的状态为“已取消”
2)支付系统
- try:假设小明账户中有 1000 元,冻结小明账户中的 100 元,此时小明看到的余额依然是 1000 元。
- confirm:将账户余额变为 900 元,并清除冻结记录。
- concel:清除冻结记录。
3)库存系统
- try:假设库存中还剩 10 本书,冻结其中的一本书,现实库存依然有 10 本书。
- confirm:将剩余库存更新为 9 本书,并清除冻结记录。
- cancel:清除冻结记录。
4)会员系统
- try:假设小明原因积分 3000 分,给小明账户预增加 10 积分,账户显示的积分依然是 3000 分。
- confirm:将账户积分更新为 3010,并清除预增加记录。
- cancel:清除预增加记录。
特点剖析
TCC 事务具备事务的四个特性:
- 原子性:事务发起方协调各个分支事务全部提交或者全部回滚。
- 一致性:TCC 事务提供最终一致性。
- 隔离型:通过 try 预分配资源的方式来实现数据的隔离。
- 持久性:交由各个分支事务来实现。
实际开发中,TCC 的本质是把数据库的二阶段提交上升到微服务来实现,从而避免数据库二阶段中长事务引起的低性能风险。
所以说,TCC 解决了跨服务的业务操作原子性问题,比如下订单减库存,多渠道组合支付等场景,通过 TCC 对业务进行拆解,可以让应用自己定义数据库操作的粒度,可以降低锁冲突,提高系统的业务吞吐量。
TCC 的不足主要体现在对微服务的侵入性强,TCC 需要对业务系统进行改造,业务逻辑的每个分支都需要实现 try、Confirm、Cancel 三个操作,并且 Confirm、Cancel 必须保证幂等。
另外 TCC 的事务管理器要记录事务日志,也会损耗一定的性能。
同时 TCC 事务为了解决异步网络中的通信失败或超时带来的异常情况,要求业务方在设计实现上要遵循三个策略:
- 允许空补偿:原因是异常发生在阶段 1 时,部分参与方没有收到 try 请求从而触发整个事务的 cancel 操作,try 失败或者没有执行 try 操作的参与方收到 cancel 请求时,要进行空补偿操作。
- 保持幂等性:原因是异常发生在阶段 2 时,比如网络超时,则会重复调用参与方的 confirm/cancel 方法,因此需要这两个方法实现上保证幂等性。
- 防止资源悬挂:原因网络异常导致两个阶段无法保证严格的顺序执行,出现参与方侧 try 请求比 cancel 请求更晚到达的情况,cancel 会执行空回滚而确保事务的正确性,但是此时 try 方法也不可以再被执行。
TCC 事务将分布式事务从资源层提到业务层来实现,可以让业务灵活选择资源的锁定粒度,并且全局事务执行过程中不会一直持有锁,所以系统的吞吐量比 2PC/XA 模式要高很多。
支持 TCC 事务的开源框架有:ByteTCC、Himly、TCC-transaction。
XA/2PC vs TCC
TCC 事务模型的思想类似 2PC 提交,下面对比 TCC 和基于 2PC 事务 XA 规范对比。
- 第一阶段 在 XA 事务中,各个 RM 准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update 等);而在 TCC 中,是主业务操作请求各个子业务服务预留资源。
- 第二阶段 XA 事务根据第一阶段每个 RM 是否都 prepare 成功,判断是要提交还是回滚。如果都 prepare 成功,那么就 commit 每个事务分支,反之则 rollback 每个事务分支。在 TCC 中,如果在第一阶段所有业务资源都预留成功,那么进入 Confirm 步骤,提交各个子业务服务,完成实际的业务处理,否则进入 Cancel 步骤,取消资源预留请求。
区别
- 2PC/XA 是数据库或者存储资源层面的事务,实现的是强一致性,在两阶段提交的整个过程中,一直会持有数据库的锁。
- TCC 关注业务层的正确提交和回滚,在 Try 阶段不涉及加锁,是业务层的分布式事务,关注最终一致性,不会一直持有各个业务资源的锁。
TCC 的核心思想是针对每个业务操作,都要添加一个与其对应的确认和补偿操作,同时把相关的处理,从数据库转移到业务中,以此实现跨数据库的事务。
Saga
Saga 并不是一个新概念,其相关论文在 1987 年就发布了,和 XA 两阶段提交规范出现的时间差不多。
Saga 和 TCC 一样,也是一种补偿事务,但是它没有 try 阶段,而是把分布式事务看作一组本地事务构成的事务链。
==事务链中的每一个正向事务操作,都对应一个可逆的事务操作==。Saga 事务协调器负责按照顺序执行事务链中的分支事务,分支事务执行完毕,即释放资源。如果某个分支事务失败了,则按照反方向执行事务补偿操作。
假如一个 Saga 的分布式事务链有 n 个分支事务构成,[T1,T2,…,Tn],那么该分布式事务的执行情况有三种:
- T1,T2,…,Tn:n 个事务全部执行成功了。
- T1,T2,…,Ti,Ci,…,C2,C1:执行到第 i (i<=n) 个事务的时候失败了,则按照 i->1 的顺序依次调用补偿操作。如果补偿失败了,就一直重试。补偿操作可以优化为并行执行。
- T1,T2,…,Ti (失败),Ti (重试),Ti (重试),…,Tn:适用于事务必须成功的场景,如果发生失败了就一直重试,不会执行补偿操作。
举例
假如国庆节小明要出去玩,从北京出发,先去伦敦,在伦敦游玩三天,再去巴黎,在巴黎游玩三天,然后再返回北京。整个行程中涉及不同航空公司的机票预订以及伦敦和巴黎当地的酒店预订,小明的计划是如果任何一张机票或酒店预订不上,就取消本次出行计划。假如综合旅游出行服务平台提供这种一键下单的功能,那么这就是一个长事务,用 Saga 模式编排服务的话,就如下图所示:任何一个环节失败的话,就通过补偿操作取消前面的行程预订。
特点剖析
Saga 事务是可以保障事务的三个特性:
- 原子性:Saga 协调器可以协调事务链中的本地事务要么全部提交,要么全部回滚。
- 一致性:Saga 事务可以实现最终一致性。
- 持久性:基于本地事务,所以这个特性可以很好实现。
但是 Saga 不保证事务隔离性的,本地事务提交后变更就对其他事务可见了。其他事务如果更改了已经提交成功的数据,可能会导致补偿操作失败。比如扣款失败,但是钱已经花掉了,业务设计上需要考虑这种场景并从业务设计上规避这种问题。
Saga 事务和 TCC 事务一样,对业务实现要求高,要求业务设计实现上遵循三个策略:
- 允许空补偿:网络异常导致事务的参与方只收到了补偿操作指令,因为没有执行过正常操作,因此要进行空补偿。
- 保持幂等性:事务的正向操作和补偿操作都可能被重复触发,因此要保证操作的幂等性。
- 防止资源悬挂:网络异常导致事务的正向操作指令晚于补偿操作指令到达,则要丢弃本次正常操作,否则会出现资源悬挂问题。
虽然 Saga 和 TCC 都是补偿事务,但是由于提交阶段不同,所以两者也是有不同的:
- Saga 是不完美补偿,补偿操作会留下之前原始事务操作的痕迹,需要考虑对业务上的影响。
- TCC 是完美补偿,补偿操作会彻底清理之前的原始事务操作,用户是感知不到事务取消之前的状态信息的。
- TCC 的事务可以更好的支持异步化,但是 Saga 模式一般在补偿阶段比较适合异步化。
Saga 模式非常适合于业务流程长的长事务的场景,实现上对业务侵入低,所以非常适合微服务架构的场景。同时 Saga 采用的是一阶段提交模式,不会对资源长时间加锁,不存在“木桶效应”,所以采用这种模式架构的系统性能高、吞吐高。
阿里巴巴的 Seata 开源项目和华为的 ServiceComb 开源项目都支持 Saga 模式。
基于消息的分布式事务
基于消息的分布式事务模式核心思想是通过消息系统来通知其他事务参与方自己事务的执行状态。
消息系统的引入更有效的将事务参与方解耦,各个参与方可以异步执行。
该种模式的难点在于解决本地事务执行和消息发送的一致性:两者要同时执行成功或者同时取消执行。
实现上主要有两种方式:
- 基于事务消息的方案
- 基于本地消息的方案
基于事务消息的分布式事务
普通消息是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
解决这个问题,需要引入事务消息,事务消息和普通消息的区别在于事务消息发送成功后,==处于 prepared 状态==,不能被订阅者消费,等到事务消息的状态更改为可消费状态后,下游订阅者才可以监听到次消息。
本地事务和事务消息的发送的处理流程如下:
- 事务发起者预先发送一个事务消息。
- MQ 系统收到事务消息后,将消息持久化,消息的状态是“待发送”,并给发送者一个 ACK 消息。
- 事务发起者如果没有收到 ACK 消息,则取消本地事务的执行;如果收到了 ACK 消息,则执行本地事务,并给 MQ 系统再发送一个消息,通知本地事务的执行情况。
- MQ 系统收到消息通知后,根据本地事务的执行情况更改事务消息的状态,如果成功执行,则将消息更改为“可消费”并择机下发给订阅者;如果事务执行失败,则删除该事务消息。
- 本地事务执行完毕后,发给 MQ 的通知消息有可能丢失了。所以支持事务消息的 MQ 系统有一个定时扫描逻辑,扫描出状态仍然是“待发送”状态的消息,并向消息的发送方发起询问,询问这条事务消息的最终状态如何并根据结果更新事务消息的状态。因此事务的发起方需要给 MQ 系统提供一个事务消息状态查询接口。
- 如果事务消息的状态是“可发送”,则 MQ 系统向下游参与者推送消息,推送失败会不停重试。
- 下游参与者收到消息后,执行本地事务,本地事务如果执行成功,则给 MQ 系统发送 ACK 消息;如果执行失败,则不发送 ACK 消息,MQ 系统会持续推送给消息。
基于本地消息的分布式事务
基于事务消息的模式对 MQ 系统要求较高,并不是所有 MQ 系统都支持事务消息的,RocketMQ 是目前为数不多的支持事务小的 MQ 系统。如果所依赖的 MQ 系统不支持事务消息,那么可以采用本地消息的分布式模式。
该种模式的核心思想是事务的发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中。业务执行成功,则同时记录一条“待发送”状态的消息到本地消息表中。系统中启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并将其发送到 MQ 系统中,如果发送失败或者超时,则一直发送,直到发送成功后,从本地消息表中删除该记录。后续的消费订阅流程则与基于事务消息的模式雷同。
特点剖析
基于消息的分布式事务模式对 ACID 特性的支持如下:
- 原子性:最终可以实现分支事务都执行或者都不执行。
- 一致性:提供最终一致性。
- 隔离性:不保障隔离性。
- 持久性:由本地事务来保证。
基于消息的分布式事务可以将分布式系统之间更有效的解耦,各个事务参与方之间的调用不再是同步调用。
对 MQ 系统的要求较高,对业务实现也有一定的侵入性,要么提供事务消息状态查询接口,要么需要维护本地消息表。并且原则上只接受下游分支事务的成功,不接受事务的回滚,如果失败就要一直重试,适用于对最终一致性敏感度较低的业务场景,例如跨企业的系统间的调用,适用的场景有限。
最大努力通知型分布式事务
最大努力通知型的分布式事务解决方案,也是基于 MQ 系统的一种解决方案,但是不要求 MQ 消息可靠。
举例
假设小明通过联通的网上营业厅为手机充话费,充值方式选择支付宝支付。整个操作的流程如下:
- 小明选择充值金额“50 元”,支付方式“支付宝”。
- 联通网上营业厅创建一个充值订单,状态为“支付中”,并跳转到支付宝的支付页面(此时进入了支付宝的系统中)。
- 支付宝验明确认小明的支付后,从小明的账户中扣除 50 元,并向联通的账户中增加 50 元。执行完毕后向 MQ 系统发送一条消息,消息的内容标识支付是否成功,消息发送允许失败。
- 如果消息发送成功,那么支付宝的通知服务会订阅到该消息,并调用联通的接口通知本次支付的结果。如果此时联通的服务挂掉了,导致通知失败了,则会按照 5min、10min、30min、1h、…、24h 等递增的时间间隔,间隔性重复调用联通的接口,直到调用成功或者达到预订的时间窗口上限后,则不再通知。这就是尽最大努力通知的含义。
- 如果联通服务恢复正常,收到了支付宝的通知,如果支付成功,则给账户充值;如果支付失败,则取消充值。执行完毕后给支付宝通知服务确认响应,确认响应允许失败,支付宝系统会继续重试。所以联通的充值接口需要保持幂等性。
- 如果联通服务故障时间很久,恢复正常后,已超出支付宝通知服务的时间窗口,则联通扫描“支付中”的订单,主动向支付宝发起请求,核验订单的支付结果。
特点剖析
最大努力通知型方案本质是通过引入定期校验机制来对最终一致性做兜底,对业务侵入性较低、对 MQ 系统要求较低,实现比较简单,适合于对最终一致性敏感度比较低、业务链路较短的场景,比如==跨平台、跨企业的系统间的业务交互==。
分布式事务中间件
Seata
seata 是阿里开源的,阿里是国内最早一批进行应用分布式(微服务化)改造的企业,所以很早就遇到微服务架构下的分布式事务问题。阿里对于分布式事务的解决方案历程如下:
- 2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
- 2016 年,TXC 经过产品化改造,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品,在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
- 2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
- 2019 - fescar被重命名为了seata(simple extensiable autonomous transaction architecture)。
待续。。。
参考资料
- 《神一样的CAP理论被应用在何方》-https://developer.51cto.com/art/201909/602506.htm
- 《如何选择分布式事务解决方案?》 - https://developer.aliyun.com/article/762770
- 《分布式事务中间件TXC》(http://mw.alibaba-inc.com/product-txc.html)
- 《弹力设计之补偿事务》(https://www.jianshu.com/p/8095001d79bb)
- 《分布式事务中间件ServiceComb》(http://servicecomb.apache.org/cn/docs/distributed-transactions-saga-implementation/)
- 《深入理解两阶段提交》(https://sq.163yun.com/blog/article/165554812476866560)
- 《Seata》(https://seata.io/zh-cn/)
- 《TCC事务原理》(https://www.cnblogs.com/jajian/p/10014145.html)
- 《TCC事务异常场景》(https://blog.csdn.net/dm_vincent/article/details/92432059
- 《Compensating Transaction Pattern》(https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)
- 《基于消息的分布式事务》(https://www.jianshu.com/p/04bad986a4a2)
- 《分布式事务概述》(http://www.tianshouzhi.com/api/tutorials/distributed_transaction/383)
- 《初识Open/X XA》(https://www.jianshu.com/p/6c1fd2420274)
- 《DTP: XA Specification》(https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf)
- 《DTP Model》(https://pubs.opengroup.org/onlinepubs/009249599/toc.pdf)
- 《XA规范与TCC事务模型》https://cloud.tencent.com/developer/article/1627837