此篇详细讲解了分布式事务相关知识,希望能帮助到你,一起学习加油吧!
在多种业务场景下,单机事务无法满足业务需求,涉及到分布式事务的需求,如以下几种情况:
对于分布式事务,有以下三个特性:一致性,可用性与分区容错性。
一致性 (Consistency)
数据一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。
分布式环境中,一致性是指 多个副本之间能否保持一致的特性 。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处理一致的状态。
数据一致性分为强一致性、弱一致性、最终一致性:
如果的确能像上面描述的那样时刻保证客户端看到的数据都是一致的,那么称之为强一致性。
如果允许存在中间状态,只要求经过一段时间后,数据最终是一致的,则称之为最终一致性。
此外,如果允许存在部分数据不一致,那么就称之为弱一致性。
可用性 (Availability)
系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在 有限的时间 内返回 正常的结果 。
分区容错性 (Partition tolerance)
即分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
对于 C、A、P 三个方面,同时满足这是不可能的。2000 年 7 月 Eric Brewer 教授仅仅提出来的是一个猜想,2 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP 理论,并且而一个分布式系统最多只能满足 CAP 中的 2 项。之后,CAP 理论正式成为分布式计算领域的公认定理。
可以做一个假设,对于分布式事务,通常分区容错性是需要做到的,如果做不到,那么分布式就没有意义了。在此基础上,对于 AP 两个方面,如果需要满足一致性 (强一致性),那么意味着任意时刻所有节点数据都应该相同。这就意味着对任一节点数据的读写必须同步到其他节点,其他节点在此期间无法操作该数据,否则就会造成数据不一致。由于这个限制,可用性就无法保证。如果在写入期间数据阻塞时间很长,那么必然会导致可用性的丧失。
放弃 P
放弃分区容错性的话,则放弃了分布式,放弃了系统的可扩展性。
放弃 A
放弃可用性的话,则在遇到网络分区或其他故障时,受影响的服务需要等待一定的时间,再此期间无法对外提供政策的服务,即不可用。
放弃 C
放弃一致性的话 (这里指强一致),则系统无法保证数据保持实时的一致性,在数据达到最终一致性时,有个时间窗口,在时间窗口内,数据是不一致的。
在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。但是对于金融业务而言,一致性往往是更重要的。对于金融数据甚至可以牺牲可用性来保证一致性,即使不可用也要保证数据是一致的。
A 的区别:
ACID 中的 A 指的是原子性 (Atomicity),是指事务被视为一个不可分割的最小工作单元,事务中的所有操作要么全部提交成功,要么全部失败回滚; CAP 中的 A 指的是可用性 (Availability),是指集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
C 的区别:
ACID 一致性是有关数据库规则,数据库总是 从一个一致性的状态转换到另外一个一致性的状态 。CAP 的一致性是 分布式多服务器之间复制数据令这些服务器拥有同样的数据 ,由于网速限制,这种复制在不同的服务器上所消耗的时间是不固定的,集群通过组织客户端查看不同节点上还未同步的数据维持逻辑视图,这是一种分布式领域的一致性概念。
ACID 里的一致性指的是事务执行前后,数据库完整性,而 CAP 的一致性,指的是分布式节点的数据的一致性。背景不同,无从可比。
CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸,对于 C 我们采用的方式和策略就是 保证最终一致性 。
BASE 是 Basically Available (基本可用)、Soft state (软状态) 和 Eventually consistent (最终一致性) 三个短语的缩写。BASE 基于 CAP 定理演化而来,核心思想是即时无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available (基本可用)
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用 性,但不等于系统不可用。允许响应时间的损失 (时间变长) 与功能的损失 (服务降级)。
Soft state (软状态)
指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
Eventually consistent (最终一致性)
强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
Eventually 含义为最终的意思,关于词典的解释为 in the end, especially after a long time or a lot of effort, problems, etc.
。即意味着 无论经过多长的时间,耗费多少的努力,都要达到这一目标,数据的最终一致性 。
对于最终一致性,又可分为以下几种:
因果一致性 (Causal consistency)
即进程 A 在更新完数据后通知进程 B,那么之后进程 B 对该项数据的范围都是进程 A 更新后的最新值。
读己之所写 (Read your writes)
进程 A 更新一项数据后,它自己总是能访问到自己更新过的最新值。
会话一致性 (Session consistency)
将数据一致性框定在会话当中,在一个会话当中实现读己之所写的一致性。即执行更新后,客户端在同一个会话中始终能读到该项数据的最新值。
单调读一致性 (Monotonic read consistency)
如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。
单调写一致性 (Monotoic write consistency)
一个系统需要保证来自同一个进程的写操作被顺序执行。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是: 即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性 。
BASE 理论是对 CAP 理论的延伸和补充,主要是对 AP 的补充。牺牲数据的强一致性,来保证数据的可用性,虽然存在中间装填,但数据最终一致。
ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。ACID 和 BASE 代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。
刚性事务指的是分布式事务要像本地式事务⼀样,具备数据强⼀致性。从 CAP 来看就是要达到 CP 状态。
常见的刚性事务方案有:XA 协议(2PC、JTA、JTS)、3PC。由于刚性事务同步阻塞,处理效率低,不适合⼤型⽹站分布式场景。
XA 规范是 X/Open 组织定义的分布式事务处理 (DTP,Distributed Transaction Processing) 标准。规范描述了全局的事务管理器与局部的资源管理器之间的接口。
对于 XA 模型,包含三个角色:
AP:Applicaiton,应用程序
业务层,哪些操作属于⼀个事务,就是 AP 定义的。
TM:Transaction Manager
接收 AP 的事务请求,对全局事务进⾏管理,管理事务分⽀状态,协调 RM 的处理,通知 RM 哪些操作属于哪些全局事务以及事务分⽀等等。这个也是整个事务调度模型的核⼼部分。
RM:Resource Manager,资源管理器
⼀般是数据库,也可以是其他的资源管理器,如消息队列 (如 JMS 数据源),⽂件系统等。
XA 规范的目的是允许多个资源 (如数据库,应用服务器,消息队列等) 在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。XA 规范使用两阶段提交 (2PC,Two-Phase Commit) 协议来保证所有资源同时提交或回滚任何特定的事务。目前知名的数据库,如 Oracle, DB2, mysql 等,都是实现了 XA 接口的,都可以作为 RM。
XA 规范定义了 (全局) 事务管理器 (Transaction Manager) 和 (局部) 资源管理器 (Resource Manager) 之间的接口。XA 接口是双向的系统接口,在事务管理器 (Transaction Manager) 以及一个或多个资源管理器 (Resource Manager) 之间形成通信桥梁。
XA 之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲 (参考 Fischer 等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源 (如数据库或 JMS 队列)
XA 是数据库的分布式事务,强一致性,在整个过程中,数据一张锁住状态,即从 prepare 到 commit、rollback 的整个过程中,TM 一直把持折数据库的锁,如果有其他人要修改数据库的该条数据,就必须等待锁的释放,存在⻓事务⻛险。
XA 模型的处理流程如下:
Java 平台上事务规范 JTA (Java Transaction API) 也定义了对 XA 事务的支持,实际上,JTA 是基于 XA 架构上建模的,在 JTA 中,事务管理器抽象为 javax.transaction.TransactionManager
接口,并通过底层事务服务 (即 JTS) 实现。像很多其他的 java 规范一样,JTA 仅仅定义了接口,具体的实现则是由供应商 (如 J2EE 厂商) 负责提供。目前 JTA 的实现主要由以下几种:
J2EE 容器所提供的 JTA 实现 (JBoss)。
独立的 JTA 实现: 如 JOTM,Atomikos。
这些实现可以应用在那些不使用 J2EE 应用服务器的环境里用以提供分布事事务保证。如 Tomcat, Jetty 以及普通的 java 应用。
JTA 定义了一套接口,其中约定了几种主要的⻆色: TransactionManager、UserTransaction、Transaction、XAResource,并定义了这些⻆色之间需要遵守的规范,如 Transaction 的委托给 TransactionManager 等。JTS 也是一组规范,对于 JTA 中需要⻆色之间的交互,JTS 就是约定了交互细节的规范。总体上来说 JTA 更多的是从框架的⻆度来约定程序⻆色的接口,而 JTS 则是从具体实现的⻆度来约定程序⻆色之间的接口,两者各司其职。
2.4.3.1. 2PC 过程
广泛应用在数据库领域,为了使得基于分布式架构的所有节点可以在进行事务处理时能够保持原子性和一致性。绝大部分关系型数据库,都是基于 2PC 完成分布式的事务处理。
2PC 分为两个阶段处理:
提交事务请求
事务询问。协调者向所有参与者发送事务内容,询问是否可以执行提交操作,并开始等待各参与者进行响应。
执行事务。各参与者节点,执行事务操作,并将 Undo 和 Redo 操作计入本机事务日志。
各参与者向协调者反馈事务问询的响应。成功执行返回 Yes,否则返回 No。
执行事务提交
协调者在阶段二决定是否最终执行事务提交操作。这一阶段包含两种情形:
执行事务提交:所有参与者 reply Yes,那么执行事务提交。
发送提交请求,协调者向所有参与者发送 Commit 请求。
事务提交,参与者收到 Commit 请求后,会正式执行事务提交操作,并在完成提交操作之后,释放在整个事务执行期间占用的资源。
反馈事务提交结果,参与者在完成事务提交后,写协调者发送 Ack 消息确认。
完成事务,协调者在收到所有参与者的 Ack 后,完成事务。
中断事务:当存在某一参与者向协调者发送 No 响应,或者等待超时。协调者只要无法收到所有参与者的 Yes 响应,就会中断事务。
发送回滚请求:协调者向所有参与者发送 Rollback 请求。
回滚:参与者收到请求后,利用本机 Undo 信息,执行 Rollback 操作。并在回滚结束后释放该事务所占用的系统资源。
反馈回滚结果:参与者在完成回滚操作后,向协调者发送 Ack 消息。
中断事务:协调者收到所有参与者的回滚 Ack 消息后,完成事务中断。
对于 2PC 的过程,总结如下:
2.4.3.2. 2PC 优缺点
优点:
2PC 解决的是分布式数据强一致的问题。
2PC 方案比较适合单体应用跨多个库的分布式事务。而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。
缺点:
性能问题
2PC 的提交在执行过程中,所有参与事务操作的逻辑都处于阻塞状态,也就是说,各个参与者都在等待其他参与者响应,无法进行其他操作。
单点故障问题
事务协调者是整个 XA 模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。如果 RM 资源没有超时机制的话,此时就会陷入永久的阻塞。
数据不一致问题
在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。同时若部分 RM 事务提交完成,但部分 RM 提交失败时,这时也会出现不一致问题。
针对 2PC 的缺点,研究者提出了 3PC,即 Three-Phase Commit。作为 2PC 的改进版,3PC 将原有的两阶段过程,重新划分为 CanCommit、 PreCommit 和 do Commit 三个阶段。
2.4.4.1. 3PC 过程
CanCommit
事务询问,协调者向所有参与者发送包含事务内容的 canCommit 的请求,询问是否可以执行事务提交,并等待应答。
各参与者反馈事务询问,正常情况下,如果参与者认为可以顺利执行事务,则返回 Yes,否则返回 No。
PreCommit
在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以执行事务的 PreCommit 操作。有以下两种可能:
执行事务预提交
发送预提交请求。协调者向所有节点发出 PreCommit 请求,并进入 prepared 阶段。
事务预提交。参与者收到 PreCommit 请求后,会执行事务操作,并将 Undo 和 Redo 日志写入本机事务日志;
各参与者成功执行事务操作,同时将反馈以 Ack 响应形式发送给协调者,同事等待最终的 Commit 或 Abort 指令。
中断事务:假如任意一个参与者向协调者发送 No 响应,或者等待超时,协调者在没有得到所有参与者响应时,即可以中断事务。
发送中断请求。协调者向所有参与者发送 Abort 请求。
中断事务。无论是收到协调者的 Abort 请求,还是等待协调者请求过程中出现超时,参与者都会中断事务。
DoCommit
在这个阶段,会真正的进行事务提交,同样存在两种可能:
执行提交
发送提交请求。假如协调者收到了所有参与者的 Ack 响应,那么将从预提交转换到提交状态,并向所有参与者,发送 doCommit 请求。
事务提交。参与者收到 doCommit 请求后,会正式执行事务提交操作,并在完成提交操作后释放占用资源。
反馈事务提交结果。参与者将在完成事务提交后,向协调者发送 Ack 消息;
完成事务。协调者接收到所有参与者的 Ack 消息后,完成事务。
中断事务:在该阶段,假设正常状态的协调者接收到任一个参与者发送的 No 响应,或在超时时间内,仍旧没收到反馈消息,就会中断事务。
发送中断请求。协调者向所有的参与者发送 abort 请求。
事务回滚。参与者收到 abort 请求后,会利用阶段二中的 Undo 消息执行事务回滚,并在完成回滚后释放占用资源。
反馈事务回滚结果。参与者在完成回滚后向协调者发送 Ack 消息。
中端事务。协调者接收到所有参与者反馈的 Ack 消息后,完成事务中断。
3PC 过程总结如下:
2.4.4.2. 3PC 优缺点
优点:
相对于 2PC,3PC 主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行 commit。而不会一直持有事务资源并处于阻塞状态。
通过 CanCommit、PreCommit、DoCommit 三个阶段的设计,相较于 2PC 而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
缺点:
由于网络原因,协调者发送的 abort 响应没有及时被参与者接收到,那么参与者在等待超时之后执行了 commit 操作。这样就和其他接到 abort 命令并执行回滚的参与者之间存在数据不一致的情况。
3PC 依然没有完全解决数据不一致的问题。假如在 DoCommit 过程,参与者 A 无法接收协调者的通信,那么参与者 A 会自动提交,但是提交失败了,其他参与者成功了,此时数据就会不一致。
数据锁定: 数据在事务未结束前,为了保障一致性,根据数据隔离级别进行锁定。
协议阻塞: 本地事务在全局事务没 commit 或 callback 前都是阻塞等待的。
性能损耗高: 主要体现在事务协调增加的 RT 成本,并发事务数据使用锁进行竞争阻塞。
协调者依赖独立的 J2EE 中间件 (早期重量级 Weblogic、Jboss、后期轻量级 Atomikos、Narayana 和 Bitronix)。
运维复杂,且并不是所有资源都支持 XA 协议。
柔性事务指的是, 不要求强⼀致性,⽽是要求最终⼀致性,允许有中间状态 。也就是 Base 理论,换句话说,就是 AP 状态。
柔性事务有两个特性: 基本可用和柔性状态。基本可用是指分布式系统出现故障的时候允许损失一部分的可用性。柔性状态是指允许系统存在中间状态,这个中间状态不会影响系统整体的可用性,比如数据库读写分离的主从同步延迟等。柔性事务的一致性指的是最终一致性。
与刚性事务相比,柔性事务的特点为:有业务改造,最终⼀致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。
通知型事务的主流实现是通过 MQ (消息队列) 来通知其他事务参与者自己事务的执行状态,引入 MQ 组件,有效的将事务参与者进行解耦,各参与者都可以异步执行,所以通知型事务又被称为异步事务。因此通知型事务主要适用于那些需要异步更新数据,并且对数据的实时性要求较低的场景。
2.5.1.1. 异步确保型事务
异步确保型事务指指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免 分布式事务中同步阻塞带来的数据操作性能的下降。主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,如订单和购物⻋、收货与清算、支付与结算等等场景。
2.5.1.1.1. MQ 事务消息方案
基于 MQ 的事务消息方案主要依靠 MQ 的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的 2PC 的思路,是二阶段提交的广义拓展。
半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知 MQ 发送。
事务消息的流程如下:
事务发起方首先发送半消息到 MQ。
MQ 通知发送方消息发送成功。
在发送半消息成功后执行本地事务。
根据本地事务执行结果返回 commit 或者是 rollback。
如果消息是 rollback, MQ 将丢弃该消息不投递; 如果是 commit,MQ 将会消息发送给消息订阅方。
订阅方根据消息执行本地事务。
订阅方执行本地事务成功后再从 MQ 中将该消息标记为已消费。
如果执行本地事务过程中,执行端挂掉,或者超时,MQ 服务器端将不停的询问 producer 来获取事务状态。
Consumer 端的消费成功机制有 MQ 保证。
事务消息的优缺点:
优点:事务消息形态过程简单,性能消耗小,发起方与跟随方之间的流量峰谷可以使用队列填平,同时业务开发工作量也基本与单机事务没有差别,都不需要编写反向的业务逻辑过程因此基于消息队列实现的事务是我们除了单机事务外最优先考虑使用的形态。
缺点:事务消息仍无法百分百满足数据一致性。设想这么一个场景,当本地事务提交后,事务消息提交失败的话则会出现不一致的现象。对于此种情况 Rocket 提供了事务反查机制,Kafka 则是直接抛出异常。即使消息成功发送,消息的消费情况还需要消费端去做控制,同时还要求消费端处理幂等问题。
2.5.1.1.2. 本地消息表方案
有时候我们目前的 MQ 组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们需要另外一种方案“基于 DB 本地消息表“。
本地消息表最初由 eBay 提出来解决分布式事务的问题。是目前业界使用的比较多的方案之一,它的核心思想就是将分布式事务拆分成本地事务进行处理。
发送消息方:
需要有一个消息表,记录着消息状态相关信息。
业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专⻔的投递工作线程进行事务消息投递到 MQ。
根据投递 ACK 去删除事务消息表记录消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
处理消息队列中的消息,完成自己的业务逻辑。
如果本地事务处理成功,则表明已经处理成功了。
如果本地事务处理失败,那么就会重试执行。
如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
本地消息表优缺点:
优点:
本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
无需提供回查方法,进一步减少的业务的侵入。
在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
缺点:
本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
本地消息表是基于数据库来做的,而数据库是要读写磁盘 IO 的,因此在高并发下是有性能瓶颈的。
2.5.1.1.3. MQ 事务消息与本地消息表对比
二者的共性:
事务消息都依赖 MQ 进行事务通知,所以都是异步的。
事务消息在投递方都是存在重复投递的可能,必要时需要有配套的机制去降低重复投递率,实现更友好的消息投递去重。
事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。
二者的区别:
MQ 事务消息
需要 MQ 支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理。
具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能。
DB 本地消息表
使用了数据库来存储事务消息,降低了对 MQ 的要求,但是增加了存储成本。
事务消息使用了异步投递,增大了消息重复投递的可能性。
2.5.1.2. 最大努力通知
最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方。最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景。
最大努力通知的一致性本质是通过引入定期校验机制实现最终一致性,对业务的侵入性较低,适合于对最终一致性敏感度比较低、业务链路较短的场景。
2.5.1.2.1. MQ 事务消息方案
最大努力通知事务在投递之前,跟异步确保型流程都差不多,关键在于投递后的处理。因为异步确保型在于内部的事务处理,所以 MQ 和系统是直连并且无需严格的权限、安全等方面的思路设计。最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:
业务主动方在完成业务处理后,向业务被动方 (第三方系统) 发送通知消息,允许存在消息丢失。
业务主动方提供递增多挡位时间间隔 (5min、10min、30min、1h、 24h),用于失败重试调用业务被动方的接口。
在通知 N 次之后就不再通知,报警+记日志+人工介入。
业务被动方提供幂等的服务接口,防止通知重复消费。
业务主动方需要有定期校验机制,对业务数据进行兜底,防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。
2.5.1.2.2. 本地消息表方案
消息的实现与异步确认式的本地消息表方案相同,对于消息消费的特性与最大努力特性的 MQ 事务消息相同,业务方需要提供重试机制,第三方系统需要提供幂等接口等。
2.5.1.3. 异步确保型事务与最大努力通知对比
从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互。异步确保型事务更适用于同网络体系的内部服务交付。
从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知。而异步确保型事务只需要消息消费者主动去消费。
从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;。而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。
2.5.1.4. 通知型事务的缺陷
通知型事务,是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
消息重复发送会导致业务处理接口出现重复调用的问题。消息消费过程中消息重复发送的主要原因就是消费者成功接收处理完消息后,消息中间件没有及时更新投递状态导致的。如果允许消息重复发送,那么消费方应该实现业务接口的幂等性设计。
基于消息实现的事务并不能解决所有的业务场景,例如以下场景: 某笔订单完成时,同时扣掉用户的现金。这里事务发起方是管理订单库的服务,但对整个事务是否提交并不能只由订单服务决定,因为还要确保用户有足够的钱,才能完成这笔交易,而这个信息在管理现金的服务里。这里我们可以引入基于补偿实现的事务。
补偿模式使用一个额外的协调服务来协调各个需要保证一致性的业务服务,协调服务按顺序调用各个业务微服务,如果某个业务服务调用异常 (包括业务异常和技术异常) 就取消之前所有已经调用成功的业务服务。
2.5.2.1. TCC 事务模型
TCC 提出了一种新的事务模型, 基于业务层面的事务定义,锁粒度完全由业务自己控制 ,目的是解决 复杂业务中,跨表跨库等大颗粒度资源锁定的问题 。TCC 把事务运行过程分成 Try、Confirm 、Cancel 两个阶段,每个阶段的逻辑由业务代码控制,避免了⻓事务,可以获取更高的性能。
TCC 事务模型包含三部分内容:
主业务服务
主业务服务为整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动。
从业务服务
从业务服务是整个业务活动的参与方,负责提供 TCC 业务操作,实现初步操作 (Try)、确认操作 (Confirm)、取消操作 (Cancel) 三个接口,供主业务服务调用。
业务活动管理器
业务活动管理器管理控制整个业务活动,包括记录维护 TCC 全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的 Confirm 操作,在业务活动取消时调用所有从业务服务的 Cancel 操作。
2.5.2.1.1. TCC 事务的工作流程
TCC 事务模型分为以下三个步骤:
Try 阶段:完成业务检查,预留必须的业务资源
此阶段失败的话,则 Cancel 所有预留的资源。
Confirm 阶段:真正执行的业务逻辑,不作任何业务检查
对业务系统做确认提交,确认执行业务操作。只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务有且只能成功一次。
此阶段失败的话,需要进行重试。重试失败的话需要进行多次重试甚至于人工介入进行恢复处理。
Cancel 阶段:释放 Try 阶段预留的资源
释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。同 Confirm 阶段,Cancel 阶段失败的话同样需要多次重试,甚至于人工介入。
2.5.2.1.2. TCC 事务模型特点
可查询操作
服务操作具有全局唯一的标识,操作唯一的确定的时间。
可补偿操作
Do 阶段:真正的执行业务处理,业务处理结果外部可⻅。Compensate 阶段:抵消或者部分撤销正向业务操作的业务结果,补偿操作满足幂等性。约束:补偿操作在业务上可行,由于业务执行结果未隔离或者补偿不完整带来的⻛险与成本可控。实际上,TCC 的 Confirm 和 Cancel 操作可以看做是补偿操作。
幂等操作
重复调用多次产生的业务结果与调用一次产生的结果相同。一是通过业务操作实现幂等性,二是系统缓存所有请求与处理的结果,最后是检测到重复请求之后,自动返回之前的处理结果。
TCC 模型严重依赖回滚和补偿代码
TCC 方案严重依赖回滚和补偿代码,最终的结果是回滚代码逻辑复杂,业务代码很难维护。所以,TCC 方案的使用场景较少,但是也有使用的场景。比如说跟钱打交道的,支付、交易相关的场景,大家会用 TCC 方案,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。
TCC 模型需要每个参与者实现 Try,Confirm 与 Cancel 接口,这对业务逻辑侵入是巨大的
2.5.2.1.3. TCC 模型与 XA/DTP 模型对比
TCC 模型中的主业务服务相当于 DTP 模型中的 AP,TCC 模型中的从业务服务相当于 DTP 模型中的 RM
在 DTP 模型中,应用 AP 操作多个资源管理器 RM 上的资源。而在 TCC 模型中,是主业务服务操作多个从业务服务上的资源。例如航班预定案例中,美团 App 就是主业务服务,而川航和东航就是从业务服务,主业务服务需要使用从业务服务上的机票资源。不同的是 DTP 模型中的资源提供者是类似于 Mysql 这种关系型数据库,而 TCC 模型中资源的提供者是其他业务服务。
TCC 模型中,从业务服务提供的 try、confirm、cancel 接口相当于 DTP 模型中 RM 提供的 prepare、commit、rollback 接口。
XA 协议中规定了 DTP 模型中定 RM 需要提供 prepare、commit、rollback 接口给 TM 调用,以实现两阶段提交。
TCC 模型中,从业务服务相当于 RM,提供了类似的 try、confirm、 cancel 接口
事务管理器
在 DTP 模型中,阶段 1 的 (prepare) 和阶段 2 的 (commit、rollback),都是由 TM 进行调用的。
在 TCC 模型中,阶段 1 的 try 接口是主业务服务调用 (绿色箭头),阶段 2 的 (confirm、cancel 接口) 是事务管理器 TM 调用 (红色箭头)。这就是 TCC 分布式事务模型的二阶段异步化功能,从业务服务的第一阶段执行成功,主业务服务就可以提交完成,然后再由事务管理器框架异步的执行各从业务服务的第二阶段。这里牺牲了一定的隔离性和一致性的,但是提高了⻓事务的可用性。
2.5.2.1.4. TCC 模型与 2CP 模型对比
在阶段 1
在 XA 中,各个 RM 准备提交各自的事务分支,事实上就是准备提交资源的更新操作 (insert、delete、update 等)。
在 TCC 中,是主业务活动请求 (try) 各个从业务服务预留资源。
在阶段 2
XA 根据第一阶段每个 RM 是否都 prepare 成功,判断是要提交还是回滚。如果都 prepare 成功,那么就 commit 每个事务分支,反之则 rollback 每个事务分支。
TCC 中,如果在第一阶段所有业务资源都预留成功,那么 confirm 各个从业务服务,否则取消 (cancel) 所有从业务服务的资源预留请求。
TCC 和 2PC 不同的是:
XA 是资源层面的分布式事务,强一致性
XA 事务中的两阶段提交内部过程是对开发者屏蔽的,开发者从代码层面是感知不到这个过程的。 XA 在两阶段提交的整个过程中,一直会持有资源的锁 。事务管理器在两阶段提交过程中,从 prepare 到 commit/rollback 过程中资源实际上一直都是被加锁的。由于在执行事务的全程都需要对相关数据加锁,一般高并发性能会比较差。
TCC 是业务层面的分布式事务,最终一致性
TCC 不会一直持有资源的锁,性能较好。但是对微服务的侵入性强,微服务的每个事务都必须实现 try、confirm、cancel 等 3 个方法,开发成本高,今后维护改造的成本也高。为了达到事务的一致性要求,try、confirm、cancel 接口必须实现幂等性操作。
由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个 TCC 事务时间拉⻓。TCC 考虑的是弱化每个步骤中对于资源的锁定,以达到一个能承受高并发的目的 (基于最终一致性)。
TCC 中的两阶段提交并没有对开发者完全屏蔽,也就是说从代码层面,开发者是可以感受到两阶段提交的存在。try、confirm、cancel 在执行过程中,一般都会开启各自的本地事务,来保证方法内部业务逻辑的 ACID 特性。其中:
try 过程的本地事务,是保证资源预留的业务逻辑的正确性。
confirm、cancel 执行的本地事务逻辑确认/取消预留资源,以保证最终一致性,也就是所谓的补偿型事务 (Compensation-Based Transactions)。由于是多个独立的本地事务,因此不会对资源一直加锁。
对于 confirm、cancel 来说执行的本地事务是补偿性事务。补偿是一个独立的支持 ACID 特性的本地事务,用于在逻辑上取消服务提供者上一个 ACID 事务造成的影响。对于一个⻓事务 (long-running transaction),与其实现一个巨大的分布式 ACID 事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地 ACID 事务来处理,执行完就立即提交。
2.5.2.2. Saga 模型
Saga 模型是把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块 (对应 TCC 中的 Confirm 和 Cancel),当 Saga 事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。因此 SAGA 适用于无需⻢上返回业务发起方最终状态的场景。
对于实现来说,同样可以采用事务消息与本地消息表的方式。
2.5.2.2.1. Saga 模型的工作模型
Saga 模型由三部分组成:
LLT (Long Live Transaction):由一个个本地事务组成的事务链。
本地事务:事务链由一个个子事务 (本地事务) 组成,LLT = T1+T2+T3+...+Ti。
补偿:每个本地事务 Ti 有对应的补偿 Ci。
Saga 的执行顺序有两种:
每个本地事务都成功时:T1, T2, T3, ..., Tn
本地事务失败时:T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n
失败时有两种恢复策略:
向后恢复:撤销掉之前所有成功都子事务。
向前恢复:重试失败的事务
向前恢复没有必要提供补偿事务,如果你的业务中,子事务 (最终) 总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。
2.5.2.2.2. Saga 模型的特点
Saga 只允许两个层次的嵌套,顶级的 Saga 和简单子事务。
在外层,全原子性不能得到满足。也就是说,sagas 可能会看到其他 sagas 的部分结果。每个子事务是独立的原子行为。
补偿事务从语义⻆度撤消了事务 Ti 的行为,但未必能将数据库返回到执行 Ti 时的状态。
相对于 TCC,无需锁定资源,因此会更有性能上的优势。但于此同时失败的风险也会更大,补偿动作更麻烦。
事件驱动模型,参与者可异步执行,高吞吐。
Saga 对于 ACID 的保证和 TCC 一样:
原子性 (Atomicity):正常情况下保证。
一致性 (Consistency):在某个时间点,会出现 A 库和 B 库的数据违反一致性要求的情况,但是最终是一致的。
隔离性 (Isolation):在某个时间点,A 事务能够读到 B 事务部分提交的结果。
持久性 (Durability):和本地事务一样,只要 commit 则数据被持久。
Seata 包含以下三个组件:
TC (Transaction Coordinator):事务协调者。
负责的事务 ID 的生成,事务注册、提交、回滚等。
TM (Transaction Manager):事务管理者。
定义事务的边界,负责告知 TC,分布式事务的开始,提交,回滚。TM 作为 SDK 的一部分和业务服务在一起。
RM (Resource Manager):资源管理者。
管理每个分支事务的资源,每一个 RM 都会作为一个分支事务注册在 TC。RM 也作为 SDK 一部分和业务服务在一起。
Seata 的执行流程如下:
TM 开启分布式事务 (TM 向 TC 注册全局事务记录)。
按业务场景,编排数据库、服务等事务内资源 (RM 向 TC 汇报资源准备状态 )。
TM 结束分布式事务,事务第一阶段结束 (TM 通知 TC 提交/回滚分布式事务)。
TC 汇总事务信息,决定分布式事务是提交还是回滚。
Seata AT 模式是最早支持的模式。AT 模式是指 Automatic (Branch) Transaction Mode,自动化分支事务。Seata AT 模式是增强型 2pc 模式,或者说是增强型的 XA 模型。总体来说,AT 模式,是 2pc 两阶段提交协议的演变,不同的地方,Seata AT 模式不会一直锁表。
2.7.2.1. AT 模式的执行流程
接下来以两个服务为例说明 AT 模式的执行流程,两个服务分别为余额服务与积分服务,余额服务扣款后积分服务增加相应积分:
第一阶段:事务执行
余额服务中的 TM,向 TC 申请开启一个全局事务,TC 会返回一个全局的事务 ID。
余额服务在执行本地业务之前,RM 会先向 TC 注册分支事务。
余额服务依次生成 undo log、执行本地事务、生成 redo log,最后直接提交本地事务。
余额服务的 RM 向 TC 汇报,事务状态是成功的。
余额服务发起远程调用,把事务 ID 传给积分服务。
积分服务在执行本地业务之前,也会先向 TC 注册分支事务。
积分服务次生成 undo log、执行本地事务、生成 redo log,最后直接提交本地事务。
积分服务的 RM 向 TC 汇报,事务状态是成功的。
积分服务返回远程调用成功给余额服务。
余额服务的 TM 向 TC 申请全局事务的提交/回滚。
第二阶段:事务提交与回滚
从 AT 模式第一阶段的流程来看, 分支的本地事务在第一阶段提交完成之后,就会释放掉本地事务锁定的本地记录 。这是 AT 模式和 XA 最大的不同点,在 XA 事务的两阶段提交中,被锁定的记录直到第二阶段结束才会被释放。所以 AT 模式减少了锁记录的时间,从而提高了分布式事务的处理效率。
AT 模式之所以能够实现第一阶段完成就释放被锁定的记录,是因为 Seata 在每个服务的数据库中维护了一张 undo_log 表,其中记录了对分支事务进行操作前后记录的镜像数据,即便第二阶段发生异常,只需回放每个服务的 undo_log 中的相应记录即可实现全局回滚:
2.7.2.2. AT 模式的数据隔离性
Seata 的事务是一个全局事务,它包含了若干个分支本地事务,在全局事务执行过程中(全局事务还没执行完),某个本地事务提交了,如果 Seata 没有采取任务措施,则会导致已提交的本地事务被读取,造成脏读,如果 数据在全局事务提交前已提交的本地事务被修改,则会造成脏写 。
由此可以看出,传统意义的脏读是读到了未提交的数据, Seata 脏读是读到了全局事务下未提交的数据 ,全局事务可能包含多个本地事务,某个本地事务提交了不代表全局事务提交了。
为了解决脏读与脏写的问题,Seata 引入了全局锁 (也是一个分布式锁) 概念。
2.7.2.2.1. 写隔离控制
先获取到本地锁,获取到后就可以修改本地数据了,只是还不能进行本地事务提交。
尝试获取全局锁。
获得全局锁,意味着可以修改了,然后提交本地事务,释放本地锁。
获得全局锁失败,可以重试, 重试超时以后回滚本地事务释放本地锁 。
只有获得本地锁后才可以尝试获取全局锁,本地锁获取之前,不会去争抢全局锁。
当分布式事务提交或回滚,释放全局锁。这样其它事务就可以获取全局锁了,并提交它们对本地数据的修改。
此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。
2.7.2.2.2. 读隔离控制
在数据库本地事务隔离级别读已提交 (Read Committed) 或以上的基础上,Seata (AT 模式) 的默认全局隔离级别是读未提交 (Read Uncommitted) 。如果应用在特定场景下,必需要求全局的读已提交,目前 Seata 的方式是通过 SELECT FOR UPDATE
语句的代理。
SELECT FOR UPDATE
语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁 (回滚 SELECT FOR UPDATE
语句的本地执行) 并重试。这个过程中,查询是被 block 住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。