使用分布式事务的目的是保证分布式系统中数据的一致性
不同的分布式事务实现方案对数据一致性有着不同程度的支持,下面就来分析各种方案。
为什么只能在A和C之间做出取舍?
分布式系统中,必须满足 CAP 中的 P,此时只能在 C/A 之间作出取舍。
如果选择了CA,舍弃了P,说白了就是一个单体架构。
CAP理论告诉我们只能在C、A之间选择,在分布式事务的最终解决方案中一般选择牺牲一致性来获取可用性和分区容错性。
这里的 “牺牲一致性” 并不是完全放弃数据的一致性,而是放弃强一致性而换取弱一致性。
一致性可以分为以下三种:
- 强一致性
- 弱一致性
- 最终一致性
强一致性
系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。
也称为:原子一致性(Atomic Consistency)、线性一致性(Linearizable Consistency)
简言之,在任意时刻,所有节点中的数据是一样的。例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
总结:
- 一个集群需要对外部提供强一致性,所以只要集群内部某一台服务器的数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能正常的对外提供服务。
- 保证了强一致性,务必会损耗可用性。
弱一致性
系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。
但即使过了不一致时间窗口这段时间后,后续对该数据的读取也不一定是最新值。
所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
例如12306买火车票,虽然最后看到还剩下几张余票,但是只要选择购买就会提示没票了,这就是弱一致性。
最终一致性
是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值。
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。
简单说,就是在一段时间后,节点间的数据会最终达到一致状态。
总结
弱一致性即使过了不一致时间窗口,后续的读取也不一定能保证一致,而最终一致过了不一致窗口后,后续的读取一定一致。
一个一个分析方案
- 画一个大图,确认分布式事务方案各种方案结构
- 2PC
- 基本概念
- 流程描述
- 优劣分析
- 场景选择
- 3PC
- TCC
- SAGA
Seata:https://seata.apache.org/zh-cn/docs/overview/what-is-seata
DTM:https://dtm.pub/guide/why.html
分布式系统调用链路中允许存在任意种类数据库,数据库可以部署在任意节点。根据是否同一微服务、是否同种类数据库、是否同数据库实例、是否同数据库schema,有如下待解决的分布式事务场景:
| 服务 | 数据库 | 解决方案 |
|---|---|---|
| 不同微服务 | 同种、同实例、同schema | Row 1, Column 3 |
| 同种、同实例、不同schema | Row 2, Column 3 | |
| 同种、不同实例 | Row 2, Column 3 | |
| 不同种 | Row 2, Column 3 | |
| 同一微服务 | 同种、同实例、同schema | Row 1, Column 3 |
| 同种、同实例、不同schema | Row 2, Column 3 | |
| 同种、不同实例 | Row 2, Column 3 | |
| 不同种 | Row 2, Column 3 |
下面补充一些图:drawio
2PC,Two phase Commit
基本概念
2PC,Two phase Commit,两阶段提交,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase)。
- 准备阶段,prepare phase:事务管理器给每个数据库参与者发送 prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件),准备阶段执行事务但不进行事务的提交。
- 提交阶段,commit phase:事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
流程描述
第一阶段,准备阶段
1.1. 管理器向所有参与者发送做好事务提交准备的命令,并等待参与者响应
1.2. 参与者执行事务,但不提交,随后阻塞等待协调者的后续指令
1.3. 参与者返回是否可执行事务提交或者参与者响应超时,只会出现这三种情况
第二阶段,提交阶段
若第一阶段所有参与者均执行成功事务
2.1. 事务管理器向所有参与者发送事务提交命令,并等待参与者响应
2.2. 参与者执行事务提交,释放占有资源
2.3. 参与者返回执行事务提交成功(理论上会出现极低概率参与者响应超时)
若第一阶段有任意参与者执行事务失败或者参与者响应超时
2.1. 事务管理器向事务执行成功的参与者发送事务回滚命令,并等待这些参与者响应
2.2. 参与者执行事务回滚,释放占有资源
2.3. 参与者返回执行事务回滚成功(理论上会出现极低概率参与者响应超时)
相关实现
X/Open XA 规范
Distributed Transaction Processing: The XA Specification
X/Open XA 规范(X/Open eXtended Architecture Specification)是 X/Open(后来与 The Open Group 合并)于 1991 年发布的分布式事务处理 (DTP, distributed transaction processing) 规范。XA 规范基于 2PC 方案为分布式事务处理制定接口标准,Oracle、MySQL 等数据库均提供了 XA 支持。
AP: ,应用程序。也就是业务层。哪些操作属于一个事务,就是AP定义的。 这个角色要做两件事情,一方面是定义构成整个事务所需要的所有操作,另一方面是亲自访问资源节点来执行操作。
RM:Resource Manager,资源管理器。一般是数据库,也可以是其他的资源管理器,如消息队列(如JMS数据源),文件系统等。 这个角色是管理着某些共享资源的自治域,比如说一个MySQL数据库实例。在DTP里面,还有两个要求,一是RM自身必须是支持事务的,二是RM能够根据
将全局(分布式)事务标识定位到自己内部的对应事务。
TM: Transaction Manager,事务管理器。接收AP的事务请求,对全局事务进行管理,管理事务分支状态,协调RM的处理,通知RM哪些操作属于哪些全局事务以及事务分支等等。这个也是整个事务调度模型的核心部分。 这个角色能与AP和RM直接通信,协调AP和RM来实现分布式事务的完整性。主要的工作是提供AP注册全局事务的接口,颁发全局事务标识(GTID之类 的),存储/管理全局事务的内容和决策并指挥RM做commit/rollback。
XA的主要限制
必须要拿到所有数据源,而且数据源还要支持XA协议。目前MySQL中只有InnoDB存储引擎支持XA协议。
性能比较差,要把所有涉及到的数据都要锁定,是强一致性的,会产生长事务。
数据锁定:数据在事务未结束前,为了保障一致性,根据数据隔离级别进行锁定。
协议阻塞:本地事务在全局事务 没 commit 或 callback前都是阻塞等待的。
性能损耗高:主要体现在事务协调增加的RT成本,并发事务数据使用锁进行竞争阻塞。
Seata AT
https://blog.csdn.net/crazymakercircle/article/details/143200817#t83
优劣分析
缺点:
- 超时问题:第一阶段执行事务时,事务执行失败或者超时都会导致分布式事务回滚,但超时会带来一些问题:
- 第一个问题,假定某节点会超时,那么执行事务时,正常节点执行完毕事务后,需要等到异常节点超时后资源才能被回滚,这样会因为一个异常节点较长时间锁定多个正常节点资源的情况
- 第二个问题,超时分为两种情况,第一种是由于服务不可用导致子事务无法执行,第二种是事务正常执行但耗时过长,导致响应超时,第二种会导致部分分布式事务执行成功造成数据不一致
- 单点故障:在 2PC 中,事务协调者扮演了举足轻重的作用,由于事务协调者的重要性,一旦事务协调者发生故障,事务的参与者就会一直阻塞下去。尤其是在第二阶段,如果协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。还有一个问题,就是当事务协调者发出 commit 指令之前,如果宕机了,此时虽然可以重新选举一个新的协调者出来,但是还是无法解决因为事务协调者宕机导致的事务参与者处于阻塞状态的问题。
场景选择
…
3PC
基本概念
3PC分为CanCommit、PreCommit、DoCommit三个阶段,其中CanCommit由2PC准备阶段演变而来,DoCommit由2PC提交阶段演变而来。3PC针对2PC中的超时问题进行了优化,主要体现在CanCommit阶段的独立,和PreCommit允许超时自动提交而不是像2PC中的回滚。
- CanCommit阶段,事务管理器向各个参与者询问是否可以进行事务执行,确保参与者没有宕机,处于可用状态。CanCommit的意义在于提前检查参与者是否是由于不可用而超时,这样直接回滚即可。由于在真正开始执行事务之前直接回滚,并没有真正锁定资源,性能上有一定提升。
- PreCommit阶段,事务管理器向各个参与者发送事务执行命令,参与者执行事务,当参与者超时时,被认为事务正常执行,仅是执行时间超过了超时阈值,分布式事务不会进行回滚,这一点不回滚的判断是基于CanCommit阶段进行可用性检测之后的结论。
- DoCommit阶段,事务管理器向各个参与者发送事务提交命令,参与者提交事务
流程描述
- 第一阶段,CanCommit阶段
1.1. 管理器向所有参与者发送是否可以执行事务的问询,并等待参与者响应
1.2. 参与者检查自身状态,响应是否可以执行事务
1.3. 参与者返回是否可执行事务或者参与者响应超时
第二阶段,PreCommit阶段
若第一阶段有任一参与者响应不可执行事务或者响应超时,直接回滚分布式事务
若第一阶段所有参与者均响应可执行事务
2.1. 事务管理器向所有参与者发送事务执行命令,并等待参与者响应
2.2. 参与者锁定资源,执行事务但不提交,并将 Undo 和 Redo 信息记录到事务日志中
2.3 参与者返回事务提交成功或者失败(理论上会出现极低概率参与者正常执行事务但超时或者极低极低概率参与者不可用)
第三阶段,DoCommit阶段
若第二阶段所有参与者执行事务成功
3.1. 事务管理器向所有参与者发送事务提交命令,并等待参与者响应
3.2. 参与者执行事务提交,释放占有资源
3.3. 参与者返回执行事务提交成功(理论上会出现极低极低概率参与者响应超时,交由TM重试解决),随后阻塞等待下一步指令
若第二阶段任一参与者执行事务失败或超时
3.1. 事务管理器向事务执行成功的参与者发送事务回滚命令,并等待这些参与者响应
3.2. 参与者执行事务回滚,释放占有资源
3.3. 参与者返回执行事务回滚成功(理论上会出现极低极低概率参与者响应超时,交由TM重试解决
相关实现
3PC 过于理论,主流数据库和主流分布式事务中间件中均未实现3PC。
TCC
基本概念
TCC 是指 Try Confirm Cancel,核心思路:将针对某资源的操作分为三个子操作,try,用于预留资源,confirm,用于执行操作,cancel,用于confirm失败时释放预留的资源,三个子操作均保证为原子操作。一个 TCC 事务中可能会有多个业务操作,其整个执行过程如下:
第一阶段:所有业务操作执行其try子操作,预留资源
第二阶段:若由某业务操作try子操作失败,则所有try执行成功的业务操作执行cancel子操作,若所有业务操作try子操作执行均成功,则所有业务操作执行confirm子操作。
TCC 中 confirm 操作是最终完成业务操作的阶段,但整个阶段中最容易失败的是 try 阶段,因为 try 阶段用于预留资源,,在预留资源之后,CC部分操作就不容易失败。
- Confirm 与 Cancel 如果失败,由TCC框架进行重试补偿
- 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入
注意事项
允许空回滚
空回滚出现的原因是 Try 超时或者丢包,导致 TCC 分布式事务二阶段的 回滚,触发 Cancel 操作,此时事务参与者未收到Try,但是却收到了Cancel 请求,如下图所示:
所以 cancel 接口在实现时需要允许空回滚,也就是 Cancel 执行时如果发现没有对应的事务 xid 或主键时,需要返回回滚成功,让事务服务管理器认为已回滚。
防悬挂控制
悬挂指的是二阶段的 Cancel 比 一阶段的Try 操作先执行,出现该问题的原因是 Try 由于网络拥堵而超时,导致事务管理器生成回滚,触发 Cancel 接口,但之后拥堵在网络的 Try 操作又被资源管理器收到了,但是 Cancel 比 Try 先到。但按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,所以此时应该拒绝执行空回滚之后到来的 Try 操作,否则会产生数据不一致。因此我们可以在 Cancel 空回滚返回成功之前,先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口执行前先检查这条事务xid或业务主键是否已经标记为回滚成功,如果是则不执行 Try 的业务操作。
幂等控制
由于网络原因或者重试操作都有可能导致 Try - Confirm - Cancel 3个操作的重复执行,所以使用 TCC 时需要注意这三个操作的幂等控制,通常我们可以使用事务 xid 或业务主键判重来控制。
何时选择基于 TCC 实现的分布式事务?
TCC 方案适用于时效性要求高,如转账、支付等场景,因此 TCC 方案在电商、金融领域落地较多,但是上述原因导致 TCC 方案大多被研发实力较强、有迫切需求的大公司所采用。微服务倡导服务的轻量化、易部署,而 TCC 方案中很多事务的处理逻辑需要应用自己编码实现,对业务的侵入强,复杂且开发量大。因此,TCC 实际上是最为复杂的一种情况,其能处理所有的业务场景,但无论出于性能上的考虑,还是开发复杂度上的考虑,都应该尽量避免该类事务。
Saga
基本概念
saga,英音,[‘sɑ:ɡə],n. 萨迦(尤指古代挪威或冰岛讲述冒险经历和英雄业绩的长篇故事,这些故事的特点是它们由一系列事件组成,每个事件都相对独立,但又共同构成了一个完整的故事线);(讲述许多年间发生的事情的)长篇故事;传说,冒险故事,英雄事迹。
Saga 是针对长活事务(LLT,long lived transaction)的解决方案,相关论文见 SAGAS。其核心思路是将长活事务拆分为多个子事务,每个子事务都可以独立执行,所有子事务共同完成一个完整的业务流程。
对于分布式事务而言,长活事务就是分布式事务本身,子事务就是每个服务的本地事务。
流程描述
每个 saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成,正常情况下子事务依次执行直至所有子事务执行完毕,saga事务执行完毕,当某个子事务执行失败时,saga有两种恢复策略:向后恢复和向前恢复。
向后恢复,backward recovery
- 每个 saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成
- 采用向后恢复策略,则需要为每个Ti实现一个相反的幂等补偿操作Ci,Ci本身也是一个幂等子事务,Ci与Ti两者操作互逆。
- 当某个子事务Ti执行失败时,saga事务协调器按照(T1, T2, Ti-1)的反序依次调用(Ci-1, …, C1)完成补偿操作,回滚saga事务
- 理论上,补偿事务不会失败,但由于复杂的实际情况(网络波动、机房断电),当补偿失败时,需要人工介入
从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿,对应的执行顺序为:T1,T2,T3,C3,C2,C1。
向前恢复(forward recovery)
向前恢复是当子事务失败时,重试当前子事务,直至子事务成功执行。若业务中子事务最终都会成功或者补偿事务难以定义,则可适用向前恢复。
向前恢复的前提假设是每个子事务最终都会成功,适用于必须要让事务成功的场景
的前提假设是,
- 每个 saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成
- 向前恢复没有必要提供补偿事务
- 当某个子事务Ti执行失败时,saga事务协调器重试该子事务
- 理论上,子事务执行最终会成功,但由于复杂的实际情况(网络波动、机房断电),并不能无限次重试子事务,当最终无法重试成功时,需要人工介入
相关实现
saga 事务由两种实现思路:
- 命令协调,Order Orchestrator
- 时间编排,Event Choreographyo
命令协调
中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图:
- ① 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
- ② OSO 向库存服务请求扣减库存,库存服务回复处理结果。
- ③ OSO 向订单服务请求创建订单,订单服务回复创建结果。
- ④ OSO 向支付服务请求支付,支付服务回复处理结果。
- ⑤ 主业务逻辑接收并处理 OSO 事务处理结果回复。
中央协调器 OSO 必须事先知道执行整个事务所需的流程,如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚,基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
事件编排
命令协调方式基于中央协调器实现,所以有单点风险,但是事件编排方式没有中央协调器。事件编排的实现方式中,每个服务产生自己的时间并监听其他服务的事件来决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件,该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
① 事务发起方的主业务逻辑发布开始订单事件。
② 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
③ 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
④ 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
⑤ 主业务逻辑监听订单已支付事件并处理。
如果事务涉及 2 至 4 个步骤,则非常合适使用事件编排方式,它是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。
Seata Saga
Saga 模式 RM 驱动分支事务的行为包含以下两个阶段:
(1)执行阶段:
① 向 TC 注册分支。
② 执行业务方法。
③ 向 TC 上报业务方法执行情况:成功或失败。
(2)完成阶段:
全局提交,RM 不需要处理。
全局回滚,收到 TC 的分支回滚请求,执行业务定义的补偿回滚方法。
基于状态机引擎的 Saga 实现:
目前SEATA提供的Saga模式是基于状态机引擎来实现的,机制是:
通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
4、Saga事务的优缺点:
(1)命令协调设计的优缺点:
① 优点:
服务之间关系简单,避免服务间循环依赖,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。
② 缺点:
- 中央协调器处理逻辑容易变得庞大复杂,导致难以维护。
- 存在协调器单点故障风险。
(2)事件编排设计的优缺点:
① 优点:
- 避免中央协调器单点故障风险。
- 当涉及的步骤较少服务开发简单,容易实现。
② 缺点:
- 服务之间存在循环依赖的风险。
- 当涉及的步骤较多,服务间关系混乱,难以追踪调测。
由于 Saga 模型没有 Prepare 阶段,因此事务间不能保证隔离性。当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。
注意事项
- Saga只允许两个层次的嵌套,顶级的Saga和简单子事务
- 补偿也有需考虑的事项:
- 补偿事务从语义角度撤消了事务Ti的行为,但未必能将数据库返回到执行Ti时的状态。(例如,如果事务触发导弹发射, 则可能无法撤消此操作,发送电邮的事务可以通过发送解释问题的另一封电邮来补偿)
何时选择基于 Saga 实现的分布式事务?
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口,接入简单??
Saga 方案适用于无需马上返回业务发起方最终状态的场景,例如:你的请求已提交,请稍后查询或留意通知之类的场景。Saga 方案中所有的本地子事务执行过程中,都无需等待其调用的子事务执行,减少了加锁的时间,这在事务流程较多较长的业务中性能优势更为明显。同时,其利用队列进行进行通讯,具有削峰填谷的作用。因此该形式适用于不需要同步返回发起方执行最终结果、可以进行补偿、对性能要求较高、不介意额外编码的业务场景。
Saga的使用条件
Saga看起来很有希望满足我们的需求。所有长活事务都可以这样做吗?这里有一些限制:
Saga只允许两个层次的嵌套,顶级的Saga和简单子事务
在外层,全原子性不能得到满足。也就是说,sagas可能会看到其他sagas的部分结果
每个子事务应该是独立的原子行为
在我们的业务场景下,各个业务环境(如:航班预订、租车、酒店预订和付款)是自然独立的行为,而且每个事务都可以用对应服务的数据库保证原子操作。
补偿也有需考虑的事项:
补偿事务从语义角度撤消了事务Ti的行为,但未必能将数据库返回到执行Ti时的状态。(例如,如果事务触发导弹发射, 则可能无法撤消此操作)
但这对我们的业务来说不是问题。其实难以撤消的行为也有可能被补偿。例如,发送电邮的事务可以通过发送解释问题的另一封电邮来补偿。
对于消息发送的异常情况分析,我们可以看到,使用基于普通消息的最终一致性分布式事务方案无论如何,都无法保证业务处理与消息发送两边的一致性,其根本的原因就在于:远程调用,结果最终可能为成功、失败、超时;而对于超时的情况,处理方最终的结果可能是成功,也可能是失败,调用方是无法知晓的。为了保证两边数据的一致性,我们只能从其他地方寻找新的突破口。
Distributed Transactions Manager,DTM
https://dtm.pub/guide/start.html
https://dtm.pub/resource/blogs-theory.html
https://github.com/dtm-labs/dtm
分析分布式事务
xa
2pc
3pc
尝试手动画图吧,drawio
四点前
尝试阅读hexo tree 主题代码
建立 health 站点
八点前
洗衣服 退货
写周报
小狗钱钱阅读
11点前
是不是考虑做一个基础平台快速启动呢????借用阿里云平台完成对整个微服务的验证。
30 岁 找到目标
事务:事务是由一组操作构成的可靠的独立的工作单元,事务具备ACID的特性,即原子性、一致性、隔离性和持久性。
分布式系统中的数据库事务问题:进行微服务拆分后,每个节点负责相对独立的一部分功能,原本由一个服务完成的功能,现在由一个调用链完成,调用链的每个节点就是一个服务的一个操作,理想情况期望整个调用链能达到像本地事务一样的操作,即例如当调用链中任意节点执行失败时,可以回滚整个调用链。
由于分布式链路中每个节点均可独立使用数据库,例如同种类同实例数据库、同种类不同示例数据库或者不同种类不同示例数据库,且不同种类数据库对事务的支持程度不同,故没有一个统一分布式事务方案可以对任意数据进行事务的统一管理,另外,由于微服务架构使得调用链路变长,以及要使得整个链路是一个事务,性能代价也是分布式事务方案选型的一个重要影响因素。当前针对分布式系统中的数据库事务问题,整体上的解决方案是根据具体的业务场景,权衡取舍,因地制宜,采用满足该场景的分布式事务方案。
根据CAP、BASE理论,以及考虑到分布式事务的性能问题,大体上分布式事务方案可以分为两大类:
- 刚性事务:CP型,保证分布式事务涉及的各数据库之间的强一致性,整体对外表现像一个本地事务一样。
- 柔性事务:AP型,允许分布式事务涉及的各数据库之间为弱一致性,大体对外表象像一个本地事务,允许出现预期中且可接受的不一致情况,并具有不一致时的补偿操作。
mysql在执行分布式事务(外部XA)的时候,mysql服务器相当于xa事务资源管理器,与mysql链接的客户端相当于事务管理器。
https://github.com/YCITRG/yecao-mall-sql-for-transaction
野草商城
客户选好若干种类和数量的商品,合适的优惠券,点击下单:
- 客户端向订单服务发起请求,请求中包含的信息如下:
- 用户认证信息
- 要购买的商品种类和数量
- 使用的优惠券
- 订单服务调用库存服务扣减库存,若扣减失败,整个链路回滚,若扣减成功,继续
- 订单服务调用优惠券服务验券并消耗,若验券失败,整个链路回滚,若验券成功,继续
- 订单服务调用支付服务,创建一笔支付单,创建失败,整个链路回滚,若创建成功,返回支付单id
- 订单服务发送消息队列,15分钟延迟消息,用于取消未支付的订单、恢复库存和退回优惠券
- 订单服务响应客户端,返回支付单id
库存扣减
-- 扣减库存,在库存服务执行
XA START "811916921139674";
UPDATE tb_inventory
SET stock_quantity = stock_quantity - 100
WHERE product_id = 1
AND stock_quantity - reserved_quantity >= 100;
UPDATE tb_inventory
SET stock_quantity = stock_quantity - 20
WHERE product_id = 2
AND stock_quantity - reserved_quantity >= 20;
UPDATE tb_inventory
SET stock_quantity = stock_quantity - 6
WHERE product_id = 3
AND stock_quantity - reserved_quantity >= 6;
XA END "811916921139674";
XA PREPARE "811916921139674";
-- 验证优惠券,在优惠券服务执行
XA START "391495333076255";
UPDATE tb_coupon
SET is_used = 1
WHERE coupon_id = 3
AND user_id = 1
AND is_used = 0;
XA END "391495333076255";
XA PREPARE "391495333076255";
-- 创建订单,在订单服务执行
XA START "660515977379407";
INSERT INTO tb_order (order_id, user_id, order_status, total_amount) VALUE (1, 1, "CREATED", 100 * 2.5 + 20 * 39.9 + 6 * 9.9 - 20);
INSERT INTO tb_order_item (order_id, product_id, quantity, unit_price)
VALUES (1, 1, 100, 2.5),
(1, 2, 20, 39.9),
(1, 3, 6, 9.9);
XA END "660515977379407";
XA PREPARE "660515977379407";
-- 创建支付单,在支付服务执行
XA START "994749426564062";
INSERT INTO tb_payment_order (order_id, payment_amount, payment_status) VALUE (1, 100 * 2.5 + 20 * 39.9 + 6 * 9.9 - 20, "PENDING");
XA END "994749426564062";
XA PREPARE "994749426564062";
-- 提交分布式事务,在TM执行
XA COMMIT "811916921139674";
XA COMMIT "391495333076255";
XA COMMIT "660515977379407";
XA COMMIT "994749426564062";
当一个全局事务开启后就不能再次开启了,否则会报错:
Error Code: 1399. XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
XA {START|BEGIN} xid [JOIN|RESUME] 启动xid事务 (xid 必须是一个唯一值; 不支持[JOIN|RESUME]子句)
XA END xid [SUSPEND [FOR MIGRATE]] 结束xid事务 ( 不支持[SUSPEND [FOR MIGRATE]] 子句)
XA PREPARE xid 准备、预提交xid事务
XA COMMIT xid [ONE PHASE] 提交xid事务
XA ROLLBACK xid 回滚xid事务
XA RECOVER 查看处于PREPARE 阶段的所有事务
- 订单服务(Order Service)
- 负责订单的创建、更新、查询等操作。订单服务是整个电商平台的核心服务之一,需要处理订单的生成、状态变更、查询等业务逻辑。
- 订单ID作为唯一键,确保订单的唯一性和可追溯性。
- 库存服务(Inventory Service)
- 负责商品库存的管理,包括库存的查询、扣减、增加等操作。
- 当订单服务创建订单时,库存服务需要根据订单中的商品信息进行库存扣减。库存服务需要具备高可用性和一致性,以确保库存数据的准确性。
- 优惠券服务(Coupon Service)
- 负责优惠券的管理,包括优惠券的发放、查询、使用等操作。
- 用户在下单时选择优惠券后,优惠券服务需要在订单提交时扣减相应的优惠券。同时,优惠券服务还需要处理优惠券的有效性验证、过期处理等业务逻辑。
- 支付服务(Payment Service)
- 负责处理支付相关的业务,包括创建支付单、处理支付请求、查询支付状态等。
- 当订单创建成功后,支付服务需要根据订单信息创建支付单,并提供支付接口供用户进行支付操作。支付服务还需要与外部支付渠道(如支付宝、微信支付等)进行交互,确保支付流程的顺利进行。
- 用户服务(User Service)
- 虽然您没有直接提到用户服务,但在整个电商平台中,用户服务是非常重要的组成部分。
- 用户服务负责用户账号的管理,包括用户注册、登录、权限验证等操作。在订单创建、优惠券使用等环节,用户服务需要提供用户信息的验证和授权功能,以确保业务的安全性和准确性。
分析分布式系统问题,需要站在CAP、BASE理论上思考
本地事务:当事务由资源管理器本地管理时被称作本地事务。本地事务的优点就是支持严格的ACID特性,高效,可靠,状态可以只在资源管理器中维护,而且应用编程模型简单。但是本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器。
全局事务:当事务由全局事务管理器进行全局管理时成为全局事务,事务管理器负责管理全局的事务状态和参与的资源,协同资源的一致提交回滚。
分布式事务:由多个不同服务的本地事务参与,且通常数据保存到不同种类的不同数据库
本地事务的情况下,通常所有事务相关的业务操作,会被我们封装到一个Service方法中。而在分布式的情况下,请求链路被延展,拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。在这种情况下,事务无疑变得更复杂。
基于上述两个复杂性,期望有一个统一的分布式事务方案,能够像本地事务一样,以几乎无侵入的方式,满足各种存储介质,各种复杂链路,是不现实的。
至少,在当前,还没有一个十分成熟的解决方案。所以,一般情况下,在分布式下,事务会被拆分解决,并根据不同的情况,采用不同的解决方案。
本地消息表
本地消息表方案最初是由 ebay 提出的,后来通过支付宝等公司的布道,在国内被广泛使用。对于不支持事务消息的 MQ 则可以采用此方案,其核心的设计思路就是将事务消息存储到本地数据库中,并且消息数据的记录与业务数据的记录必须在同一个事务内完成。将消息数据保存到 DB 之后,就可以通过一个定时任务到 DB 中去轮询查出状态为待发送的消息,然后将消息投递给 MQ,成功收到 MQ 的 ACK 确认之后,再将 DB 中消息的状态更新或者删除消息。交互流程如下图所示:
处理步骤如下:
- 消息生产者在本地事务中处理业务更新操作,并写一条事务消息到本地消息表,该消息的状态为
待发送,业务操作和写消息表都在同一个本地事务中完成。 - 定时任务不断轮询从本地消息表中查询出状态为
待发送状态的消息,并将查出的所有消息投递到 MQ Server。 - MQ Server 接收到消息之后,就会将消息进行持久化,然后返回 ACK 给到消息生产者。
- 消息生产者收到了 MQ Server 的 ACK 之后,再从本地消息表中查询出对应的消息记录,将消息的状态更新为
已发送,或者直接删除消息记录。 - MQ Server 返回 ACK 给到消息生产者之后,接着就会将消息发送给消息消费者。
- 消息消费者接收到消息之后,执行本地事务,最后返回 ACK 给到 MQ Server。
因为 MQ 宕机或网络中断等原因,生产者有可能会向 MQ 发送重复消息,因此,消费者接收消息后的处理需要支持幂等。
该方案,相比 MQ 事务消息方案,其优点就是弱化了对 MQ 的依赖,因为消息数据的可靠性依赖于本地消息表,而不依赖于 MQ。还有一个优点就是容易实现。缺点则是本地消息表与业务耦合在一起,难以做成通用性,且消息数据与业务数据同个数据库,占用了业务系统资源。本地消息表是基于数据库来做的,而数据库是要读写磁盘 I/O 的,因此在高并发下是有性能瓶颈的。
1、什么是本地消息表:
本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理,在该方案中主要有两种角色:事务主动方和事务被动方。事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样可以避免以下两种情况导致的数据不一致性:
- 业务处理成功、事务消息发送失败
- 业务处理失败、事务消息发送成功
2、本地消息表的执行流程:
① 事务主动方在同一个本地事务中处理业务和写消息表操作
② 事务主动方通过消息中间件,通知事务被动方处理事务消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
③ 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
④ 事务主动方接收中间件的消息,更新消息表的状态为已处理。
一些必要的容错处理如下:
当①处理出错,由于还在事务主动方的本地事务中,直接回滚即可
当②、③处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,通知事务被动方重新读取消息处理业务即可。
如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。
3、本地消息表的优缺点:
(1)优点:
从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
方案轻量,容易实现。
(2)缺点:
与具体的业务场景绑定,耦合性强,不可公用
消息数据与业务数据同库,占用业务系统资源
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限
MQ事务消息
基于 MQ 自身的事务消息方案,据了解,目前只有 RocketMQ 提供了支持,其他主流的 MQ 都还不支持,所以我们对该方案的解说都是基于 RocketMQ 的。该方案的设计思路是基于 2PC 的,事务消息交互流程如下图所示:
其中,涉及几个概念要说明一下:
- 事务消息:消息队列 MQ 提供类似 X/Open XA 的分布式事务功能,通过 MQ 事务消息能达到分布式事务的最终一致。
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了 MQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,MQ 服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查。
事务消息发送步骤如下:
- 发送方将半事务消息发送至 MQ 服务端。
- MQ 服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。
事务消息回查步骤如下:
- 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作。
有一点需注意,如果发送方没有及时收到 MQ 服务端的 Ack 结果,那就可能造成 MQ 消息的重复投递,因此,订阅方必须对消息的消费做幂等处理,不能造成同一条消息重复消费的情况。
MQ 事务消息方案的最大缺点就是对业务具有侵入性,业务发送方需要提供回查接口。
六、MQ事务消息:
1、MQ事务消息的执行流程:
基于MQ的分布式事务方案本质上是对本地消息表的封装,整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了MQ内部,而不是业务数据库中,如下图:
由于将本地消息表存在了MQ内部,那么MQ内部的处理尤为重要,下面主要基于 RocketMQ4.3 之后的版本介绍 MQ 的分布式事务方案
2、RocketMQ事务消息:
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,而 RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:
(1)正常情况:
在事务主动方服务正常,没有发生故障的情况下,发消息流程如下:
步骤①:发送方向 MQ Server(MQ服务方)发送 half 消息
步骤②:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功
步骤③:发送方开始执行本地事务逻辑
步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
最终步骤:MQ Server 如果收到的是 commit 操作,则将半消息标记为可投递,MQ订阅方最终将收到该消息;若收到的是 rollback 操作则删除 half 半消息,订阅方将不会接受该消息
(2)异常情况:
在断网或者应用重启等异常情况下,图中的步骤④提交的二次确认超时未到达 MQ Server,此时的处理逻辑如下:
步骤⑤:MQ Server 对该消息发起消息回查
步骤⑥:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
最终步骤:MQ Server基于 commit/rollback 对消息进行投递或者删除。
3、MQ事务消息的优缺点:
(1)优点:相比本地消息表方案,MQ 事务方案优点是:
消息数据独立存储 ,降低业务系统与消息系统之间的耦合
吞吐量大于使用本地消息表方案
(2)缺点:
一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
业务处理服务需要实现消息状态回查接口。
最大努力通知
最大努力通知型也是基于事务消息型扩展而来的,其应用场景主要用于通知外部的第三方系统。即是说,最大努力通知型方案,主要解决的其实是跨平台、跨企业的系统间的业务交互问题。而事务消息型方案则适用于同个网络体系的内部服务间的分布式事务。
七、最大努力通知:
最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到主动方发送的消息,此时可以调用事务主动方提供的消息校对的接口主动获取
在可靠消息事务中,事务主动方需要将消息发送出去,并且让接收方成功接收消息,这种可靠性发送是由事务主动方保证的;但是最大努力通知,事务主动方仅仅是尽最大努力(重试,轮询….)将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。
所以最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。
选型
至此,可以解决分布式事务问题的方案我们基本都讲了个遍,那要把分布式事务落地到我们的交易系统中,应该如何选型呢?我们将每种方案先做个对比吧,看下表:
| 属性 | XA/2PC | TCC | Saga | MQ事务消息 | 本地消息表 | 最大努力通知型 |
|---|---|---|---|---|---|---|
| 事务一致性 | 强 | 中 | 弱 | 弱 | 弱 | 弱 |
| 性能 | 低 | 中 | 高 | 高 | 高 | 高 |
| 业务侵入性 | 小 | 大 | 小 | 中 | 中 | 中 |
| 复杂性 | 中 | 高 | 中 | 低 | 低 | 低 |
| 维护成本 | 低 | 高 | 中 | 中 | 低 | 中 |
而具体如何选型,其实还是需要根据场景而定。在第一篇文章就说过,我们应该由场景驱动架构,离开场景谈架构就是耍流氓。
如果是要解决和外部第三方系统的业务交互,比如交易系统对接了第三方支付系统,那我们就只能选择最大努力通知型。
如果对强一致性有刚性要求的短事务,对高性能和高并发则没要求的场景,那可以考虑用 XA/2PC,如果是用 Java 的话,那落地实现可以直接用 Seata 框架的 XA 模式。
如果对一致性要求高,实时性要求也高,执行时间确定且较短的场景,就比较适合用 TCC,比如用在互联网金融的交易、支付、账务事务。落地实现如果是 Java 也建议可以直接用 Seata 的 TCC 模式。
Saga 则适合于业务场景事务并发操作同一资源较少的情况,因为 Saga 本身不能保证隔离性。而且,Saga 没有预留资源的动作,所以补偿动作最好也是容易处理的场景。
MQ 事务消息和本地消息表方案适用于异步事务,对一致性的要求比较低,业务上能容忍较长时间的数据不一致,事务涉及的参与方和参与环节也较少,且业务上还有对账/校验系统兜底。如果系统中用到了 RocketMQ,那就可以考虑用 MQ 事务消息方案,因为 MQ 事务消息方案目前只有 RocketMQ 支持。否则,那就考虑用本地消息表方案。
REF
https://blog.csdn.net/a745233700/article/details/122402303
https://blog.csdn.net/crazymakercircle/article/details/143200817#t83
