[分布式系统] ACID理论以及分布式事务 刚性柔性事务二三阶段提交TCC

计算机科学 计算机科学 1768 人阅读 | 0 人回复

事务

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。

https://en.wikipedia.org/wiki/Database_transaction

事务又有几个典型的特性:

https://en.wikipedia.org/wiki/ACID

Atomicity

Main article: Atomicity (database systems) Transactions are often composed of multiple statements. Atomicity guarantees that each transaction is treated as a single "unit", which either succeeds completely or fails completely: if any of the statements constituting a transaction fails to complete, the entire transaction fails and the database is left unchanged. An atomic system must guarantee atomicity in each and every situation, including power failures, errors, and crashes.[5] A guarantee of atomicity prevents updates to the database from occurring only partially, which can cause greater problems than rejecting the whole series outright. As a consequence, the transaction cannot be observed to be in progress by another database client. At one moment in time, it has not yet happened, and at the next, it has already occurred in whole (or nothing happened if the transaction was canceled in progress).

Consistency Main article: Consistency (database systems) Consistency ensures that a transaction can only bring the database from one consistent state to another, preserving database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof. This prevents database corruption by an illegal transaction. Referential integrity guarantees the primary key–foreign key relationship.[6]

Isolation Main article: Isolation (database systems) Transactions are often executed concurrently (e.g., multiple transactions reading and writing to a table at the same time). Isolation ensures that concurrent execution of transactions leaves the database in the same state that would have been obtained if the transactions were executed sequentially. Isolation is the main goal of concurrency control; depending on the isolation level used, the effects of an incomplete transaction might not be visible to other transactions.[7]

Durability Main article: Durability (database systems) Durability guarantees that once a transaction has been committed, it will remain committed even in the case of a system failure (e.g., power outage or crash). This usually means that completed transactions (or their effects) are recorded in non-volatile memory.

毕竟这些东西是母语英语的人创造的,尽可能的多读下原文。

image.png

原子性:操作具有整体性,是不可分割的,要么都成功,要么不成功,如同调用单个方法,要么调用成功,要么调用失败,不存在其他可能。

一致性:事务操作前和操作后,数据的完整性保持一致或满足完整性约束。他表示从一个一致性状态,切换转移到另一个一致性状态,比如银行转账,要么就是转账成功,你的钱少了对方钱多了,要么就是转账失败,双方账号无变动,不能你的账号钱少了,但是对方的钱没多,这样就从一个一致性状态,变成了不一致。

隔离性:个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性:持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

术语 说明
原子 事务中的所有操作都成功,或者没有保留任何操作。
一致 如果数据在事务开始前保持一致,则事务完成后它们将保持一致。
孤立 正在进行的事务的影响将从所有其他事务中隐藏。
耐用 事务完成后,其结果将持续存在,并且会在系统崩溃中幸存下来。

事务隔离级别

事务隔离级别是针对于隔离性的细分:

image.png

读未提交:

两个事务AB并发执行,比如B中先对X+1,然后X+2,然后X-5等等,如果事务A随时都可以读取到X的最新值,那么就是读未提交,因为事务B并没有进行任何的提交操作,但是事务A却总是可以读取到被事务B修改的X的值。

读已提交:

两个事务AB并发执行,比如B中X初始为3,先对X+1,然后X+2,然后X-5等等,事务A完全无法读取到事务B中间过程的值,要么读取到X=3,要么就是事务B结束并提交之后,读取到X+1+2-5=1的这个1,不会读取到中间状态,这就是读已提交,换言之,只能读取到已经提交的事务,初始时X=3可以认为就是一个已经提交的事务,最终的1也是已经提交的事务,其他中间过程的值,读取不到。

这对读已提交,如果一个UPDATE操作发现别人正在更新那就需要等待别人事务结束提交。

可重复读:

针对于读已提交,比如一个事务初始查询X=5;进行了一定的逻辑判断,然后接着另外的事务提交了,这个X的值发生了变化,此时查询到的X=1,前后两次查询读取到的数据是不一样的,这就是不可重复读。

可重复读意味着在同一个事务中,始终能够读取到一样的数据,比如X=1,如果本事务内没有对X进行操作,读取到的永远都是1,如果执行过X+1,那么X就等于2,不存在别的可能。这就是可重复读。

串行化:

不允许事务并发执⾏,强制事务串⾏执⾏。就是在读取的每⼀⾏数据上都加上了锁,读写相互都会阻塞,所以效率很低下。这种隔离级别最⾼,是最安全的,但是性能最低。

隔离级别解决的问题

image.png

脏读:

针对于读未提交隔离级别,两个事务可以互相读取到对方的中间过程状态数据,其实别人根本都还没有提交事务,这就是脏读。

不可重复读:

不可重复读就是重复读隔离级别解决的问题:一个事务前后两次读取到不同的值,就是不可重复读

幻读:

幻读本质上是属于不可重复读的一种情况,不可重复读主要是针对数据的更新(即事务的两次读取结果值不一样),而幻读主要是针对数据的增加或减少(即事务的两次读取结果返回的数量不一样)

隔离级别与可以解决的问题的对应关系如下:

隔离级别 脏读 不可重复读 幻读
READ UNCOMITTED
READ COMMITTED ×
REPEATABLE READ × ×
SERIALIZABLE × × ×

以上是针对数据库中ACID概念的介绍。

根据FLP不可能理论,在异步通信的场景中,因为各个节点之间的延时,是否宕机等不确定因素的存在,所以不存在任何算法能达到强一致性。简而言之在实际的分布式系统中,一致性算法的可靠性是无法保证的,即不存在一个能在异步网络上能够容忍各种故障并保持一致的分布式系统。

所以这个ACID放到分布式事务之上,如果严格论证是无解的,毕竟ACID中的C是强一致性,所以必然不存在绝对的完美的算法可以解决这些问题,但是总会有很多的工程解来完成这个任务。

分布式事务

分布式事务,就是在分布式系统中运行的事务,由多个本地事务组合而成。

在分布式场景下,对事务的处理操作可能来自不同的机器,甚至是来自不同的操作系统。

分布式事务,分布式+事务,所以要面对两个方面的问题,一个是分布式系统本身就有许多问题需要解决,另外一个就是如何像单机数据库那样保障ACID。

既然无法完美、严格的保障强一致性,所以只能追求工程解,而BASE理论,一个延伸自CAP的理论,就是在一致性方面的一种工程解理论。

工程解的特点就是正常情况下可用,有问题基本可用,或者说能够达成目的,但是存在各种各样的问

为什么需要分布式事务

传统单体应用,即使有超出一个以上的服务,数据库也就是同一个,数据库本身已经实现了事务机制,数据库就可以给我们保障,不需要做额外的处理,比如两条更新语句,就可以保障事务执行。

但是随着业务发展,出现了各种分库分表、微服务架构,完全没有办法通过一个单独的数据库来完成事务机制,就必须要有额外的保障来维持事务。

这就是分布式事务必须要解决的问题,如果两个协作的系统,一个数据提交了,一个数据未提交,如果不去做任何事情,这些数据就错乱了。

所以必须要进行分布式事务的处理。

但是分布式事务,相对于传统关系型数据库来说,也存在一些差异,比如最终一致性就是一个很明显的差异,补偿机制实现最终一致性,就是允许出现短时间的数据不一致。

柔性事务与刚性事务

  • 刚性事务,遵循 ACID 原则,具有强一致性。比如,数据库事务。
  • 柔性事务,其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致。

也就是说数据库那种严格的ACID,就是刚性事务,2PC提交,如果提交失败回滚,这就是刚性,没有中间不确定状态,提交成功就是成功,否则就是失败。

诸如补偿机制就是属于柔性事物的范畴,柔性事务关注最终一致性,不管你以什么方法,比如通过消息队列,没消费成功消息还在下次继续消费、记录到数据库中下次继续执行等等,这些都是属于柔性事务。柔性事务遵循的理论就是BASE。

解决方案

image.png

2PC 二阶段提交

两阶段提交协议 (2PC) 是一种原子提交协议 (ACP)

https://en.wikipedia.org/wiki/Two-phase_commit_protocol

用于协调参与分布式原子事务的所有进程是提交还是中止(回滚)事务。

image.png

  1. 协调者发送一个VOTE-REQ消息给所有的参与者
  2. 当参与者接收到VOTE-REQ消息后,它会发送一个包含参与者投票结果的消息(YES或NO)作为响应。如果参与者投的是NO,它会决定ABORT事务并停止运行
  3. 协调者收集来自所有参与者的投票信息。如果所有的消息都是YES,同时协调者投的也是YES,那么协调者就会决定进行COMMIT,并向所有参与者发送COMMIT消息。否则协调者就会决定进行ABORT,并向所有投YES的参与者发送ABORT消息(那些投NO的参与者已经在第2步中决定ABORT了)。之后,对于这两种情况协调者都会停止运行
  4. 每个投YES的参与者等待来自协调者的COMMIT或ABORT消息。收到消息后执行相应动作然后停止运行。

上面只是一个正常情况下成功与失败的简单的交互步骤

超时问题

步骤2中,参与者需要等待来自协调者的VOTE-REQ消息。接收到消息后才能决定投票。所以它投Yes之前都可以单方面地决定进行Abort,因此如果参与者在等待VOTE-REQ消息时超时,它可以简单地决定进行Abort,然后停止运行。

在步骤3中,协调者需要等待来自所有参与者的YES或NO消息。在这个阶段,协调者也不会产生任何决定。而且,也没有任何参与者已经决定要COMMIT。因此协调者可以决定进行ABORT,但是必须要向给它发送YES的每个参与者发送ABORT消息。

在步骤4中,投了YES的参与者p要等待来自协调者的COMMIT或ABORT消息。此时,p处于不确定状态。因此,与前面两种情况下进程可以单方面进行决定不同,在这种情况下参与者必须与其他进程商议决定如何动作。这个商议过程需要通过执行一个terminaion protocol来完成。

简单说就是此时已经无法判断现在系统处于什么状态。

最简单的终止协议如下:

在与协调者的通信恢复之前p始终保持阻塞。之后,协调者通知p对应的决定结果。

协调者肯定支持这样做,因为它没有不确定区间。如果最后所有的故障都修复了的话,p就能与协调者通信,然后就能达到决定状态。

这种协议缺点在于,p可能要经历不必要的阻塞。

比如,假设现在有两个参与者p和q。协调者先给q发送了一个COMMIT或ABORT消息,但是在发送给p之前发生了故障。因此,尽管p是不确定的,但是q不是。如果p可以与q进行通信,那么它就可以从q那得知最终的决定结果。并不需要一直等待着协调者的恢复。

但是这意味着需要参与者之间需要相互知晓,这样它们才能相互直接通信。但是我们前面描述原子性提交问题时,只是说协调者认识所有参与者,参与者认识协调者,但是参与者初始时相互并不知晓。但是这也不是什么大问题,我们可以让协调者在发送VOTE-REQ消息时将参与者信息附加在上面,发给所有参与者。这样在参与者接收到该消息后,相互就都知道了。事实上,在发送该消息之前它们之间也是不需要相互知晓的,因为如果参与者在接收到VOTE-REQ消息前超时了,它可以单方面地决定进行Abort。

下面介绍下协作终止协议:

参与者p如果在不确定区间超时,它会发送一个DECISION-REQ消息给所有其他进程,设为q,问下q是否知道决定结果或者能否单方面地做出决定。

在这个场景中,p是initiator,q是responder。有如下三种情况:

q已经决定进行Commit(或Abort):q简单地发送一个COMMIT(或ABORT)消息给p,然后p进行相应动作 q还未进行投票:q可以单方面地决定进行Abort。然后它发送ABORT消息给p,p会因此决定进行ABORT q已经投了Yes但是还未做决定:q也是处于不确定状态,因此无法帮助p达成决定。

对于该协议来说,如果p可以同某个进程q通信并且上述1或2成立,那么p就可以不经阻塞地达成决定。

另一方面,如果p通信的所有进程都是3成立,那么p就会被阻塞。那么p将会一直阻塞,直到故障修复的出现了某个进程q满足条件1或2为止。至少会有一个这样的进程,即协调者。

关于两阶段提交可以参考这本书,7.4节

Concurrency Control and Recovery in Database Systems

http://sigmod.org/publications/dblp/db/books/dbtext/bernstein87.html

系统描述了数据库系统中的并发控制和恢复机制。

第7章下载:

https://gitee.com/crazybytex/some-documents/blob/master/Concurrency%20Control%20and%20Recovery%20in%20Database%20Systems-Ch7.pdf

image.png

主要问题

同步阻塞问题:

二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。

单点故障问题:

二阶段提交算法属于集中式,一旦协调节点发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦协调者故障,参与者因为等待,而一直锁定事务资源,导致整个系统被阻塞。

数据不一致:

在提交阶段,当协调者向参与者发送 DoCommit 请求之后,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。

2PC的意义

image.png

如上图所示,可以认为是一阶段提交,也就是直接提交,由于分布式系统的特性,网络是不稳定的,系统也可能会宕机,可以百分百的确认,无法保障三个节点数据一致,或者原子执行。

而且,一旦节点3宕机或者网络故障,也无法对节点1和节点2 进行回滚操作,如果采取类似补偿机制那就成了柔性事务的范畴,而不是强一致性。

2PC将这个直接提交的过程,进行拆分,分为准备和确认两个过程,一旦拆分核心就是有了后悔的机会,每个节点都可以后悔,协调者也可以后悔,后悔指的是因为各种原因无法完成任务。

2PC对于分布式系统中一致性的达成实现,具有指导意义,很多东西上面都有它的影子-分阶段处理提交,也就是将一个大的完整的事务处理过程,拆解为多个过程处理。

XA

https://en.wikipedia.org/wiki/X/Open_XA

对于计算中的事务处理,X/Open XA 标准(“扩展架构”的缩写)是 X/Open(后来与 The Open Group 合并)于 1991 年发布的用于分布式事务处理 (DTP) 的规范

XA 的目标是保证跨异构组件执行的“全局事务”的原子性。

交易是一个工作单元,例如将资金从一个人转移到另一个人。分布式事务更新多个数据存储(如数据库、应用程序服务器、消息队列、事务缓存等)为了保证完整性,XA 使用两阶段提交 (2PC) 来确保事务的所有更改生效(提交)或不生效(回滚),即原子方式。

XA 规范描述了资源管理器必须执行哪些操作才能支持事务访问。遵循此规范的资源管理器称为符合 XA 的。

XA 规范在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范提供了支持。

image.png

XA规范主要定义了(全局)事务管理器(TM)和(局部)资源管理器(RM)之间的接口。本地的数据库,比如如mysql在XA中扮演的是RM角色(比如同时写入多个MYSQL,每一个都是一个RM)。

Distributed Transaction Processing:The XA Specification

https://gitee.com/crazybytex/some-documents/blob/master/Distributed%20Transaction%20Processing%20The%20XA%20Specification.pdf

下图来自于上面的pdf链接

image.png

下图中定义了一些交互接口

image.png

MYSQL 中InnoDB支持XA

关于MYSQL中InnoDB对XA的相关文档:https://dev.mysql.com/doc/refman/8.0/en/xa.html

show ENGINES;

image.png

可以看下MySQL XA 事务基本语法

XA {START|BEGIN} xid [JOIN|RESUME] 启动一个XA事务 (xid 必须是一个唯一值; )

XA END xid [SUSPEND [FOR MIGRATE]] 结束一个XA事务

XA PREPARE xid 准备

XA COMMIT xid [ONE PHASE] 提交XA事务

XA ROLLBACK xid 回滚XA事务

XA RECOVER 查看处于PREPARE 阶段的所有XA事务

注意:JOIN 、RESUME、 [FOR MIGRATE] 没有什么实际作用

For XA START, the JOIN and RESUME clauses are recognized but have no effect.

For XA END the SUSPEND [FOR MIGRATE] clause is recognized but has no effect.

MYSQL XA状态变更图

image.png

如上图所示,可以直接一阶段提交(右侧分支);也可以回滚(左侧分支);如果是2PC,那么就是等待其他的RM。

中间件Seata中有对XA的应用,链接如下,可以辅助参考加以理解:

https://seata.io/zh-cn/blog/seata-xa-introduce.html

XA问题

XA的一些问题:

性能(阻塞、响应时间增加); 不是所有资源(RM)都支持XA协议;

XA也是两阶段提交,所以两阶段提交的问题他也基本都是存在的

JTA

JTA 全称 Java Transaction API,是 X/OPEN CAE 规范中分布式事务 XA 规范在 Java 中的映射,是 Java 中使用事务的标准 API,同时支持单机事务与分布式事务。

小结

简单说XA是一种实现标准,定义了相关接口,对于相对应的业务场景,确定好角色,实现了相关的接口功能即可,就可以按照XA协议的运行逻辑进行事务保障。

3PC 三阶段提交

https://en.wikipedia.org/wiki/Three-phase_commit_protocol

3PC也是一致性协议的一种,在2PC的基础上做了一些改进,于是有了3PC。

3PC是把2PC的阶段二(事务提交)一分为二,形成了由CanCommit、PreCommit、DoCommit三个阶段组成。

维基百科原文:

A two-phase commit protocol cannot dependably recover from a failure of both the coordinator and a cohort member during the Commit phase. If only the coordinator had failed, and no cohort members had received a commit message, it could safely be inferred that no commit had happened. If, however, both the coordinator and a cohort member failed, it is possible that the failed cohort member was the first to be notified, and had actually done the commit. Even if a new coordinator is selected, it cannot confidently proceed with the operation until it has received an agreement from all cohort members, and hence must block until all cohort members respond.

The three-phase commit protocol eliminates this problem by introducing the Prepared to commit state. If the coordinator fails before sending preCommit messages, the cohort will unanimously agree that the operation was aborted. The coordinator will not send out a doCommit message until all cohort members have ACKed that they are Prepared to commit. This eliminates the possibility that any cohort member actually completed the transaction before all cohort members were aware of the decision to do so (an ambiguity that necessitated indefinite blocking in the two-phase commit protocol).

两阶段提交协议,在提交阶段,无法可靠地从协调器和队列成员的故障中恢复。如果只有协调器失败,并且没有队列成员收到提交消息,则可以安全地推断没有发生提交。但是,如果协调器和队列成员都失败了,则失败的队列成员可能是第一个收到通知的人,并且实际上已经完成了提交。即使选择了新协调员,在收到所有队列成员的同意之前,它也无法自信地继续操作。

三阶段提交协议通过引入“准备提交”状态来消除此问题。如果协调器在发送提交前消息之前失败,则队列将一致同意操作已中止。

在所有队列成员都确认他们已准备好提交之前,协调器不会发送 doCommit 消息。这消除了任何队列成员在所有队列成员知道这样做的决定之前实际完成事务的可能性(这种模糊性需要在两阶段提交协议中无限期阻止)。

简单说拆分出来的一个阶段,可以让参与者明确的知道接下来要进行的下一步动作,尽管这还都没开始执行

最普通的形式如下:

一路绿灯,顺利提交

image.png

步骤

image.png

详细说明

阶段一:CanCommit

1)事务询问:事务协调者向各参与者发送包含事务内容的CanCommit请求,询问是否可以提交事务。并开始等待各参与者响应。

2)参与者反馈事务询问响应给协调者:各参与者收到协调者的CanCommit请求后,如果自身能够执行事务则返回yes,否则返回no。

阶段二:PerCommit

会有两种情况:直接进入preCommit阶段执行事务预提交或者不进入preCommit,直接中断事务

执行事务预提交

1)发送预提交请求:协调者向各参与者发送预提交请求。

2)事务预提交:参与者接收到perCommit请求后,会执行事务操作。

3)各参与者向协调者反应事务执行的响应ack:如果参与者成功执行了事务操作,就会反馈给协调者ack,等待commit(提交)或者abort(中止)。

中断事务

如果任何一个事务参与者向协调者返回No,或者等待超时后,协调者无法接收到所有参与者的反馈响应,那么就会事务中断。

1)发送中断请求:协调者向各参与者发送中断请求。

2)中断事务:无论参与者是否收到协调者的中止请求,在超时后仍然后会中断事务。

阶段三:DoCommit

存在两种情况:直接进入doCommit阶段提交事务或者不进入doCommit第三阶段直接中断事务

执行提交:

1)发送提交请求:协调者接收到所有参与者的ack响应,将会从预提交转换为提交状态,并向所有参与者发送doCommit请求。

2)事务提交:参与者接收到doCommit请求后,会正式执行事务操作,最终提交事务释放资源占用。

3)反馈事务提交结果:参与者完成事务提交后向协调者发送ack响应。

4)完成事务:协调者收到所有参与者反馈的ack后,完成事务。

中断事务

如果任何一个事务参与者向协调者返回No,或者等待超时后,协调者无法接收到所有参与者的反馈响应,那么就会事务中断。

1)发送中断请求:协调者向所有参与者发送abort请求。

2)事务回滚:参与者接收到abort请求后,回滚后释放占用资源。

3)反馈事务回滚结果:所有参与者在完成回滚后,向协调者发送ack。

4)中断事务:协调者接收到所有参与者反馈的ack,完成事务中断。

注意:

一旦所有参与者完成二阶段准备提交,当进入第三阶段后,协调者出现故障,或参与者与协调者出现网络问题,在超时后,参与者都会进行事务提交。只有第二步preCommit都是返回ACK的情况下才算是此处说的进入第三阶段,如果preCommit消息的响应超时或者存在NO,是不会进入第三阶段的。

TCC

2PC 和 3PC 这两种方法,有两个共同的缺点,一是都需要锁定资源,降低系统性能;二是,没有解决数据不一致的问题。因此,便有了通过分布式消息来确保事务最终一致性的方案。

最早是2007年下面这篇文章中提出,最初使用的三个词是Tentative Operations, Confirmation,and Cancellation

Life Beyond Distributed Transactions: An apostate’s opinion

https://www.semanticscholar.org/paper/Life-beyond-Distributed-Transactions:-an-Apostate's-Helland/c20baa16cb57ff4979569871d15294fa720bbc23#:~:text=Life%20beyond%20Distributed%20Transactions%3A%20an%20Apostate%27s%20Opinion.%20Many,use%20of%20platforms%20providing%20guarantees%20of%20global%20serializability.

https://gitee.com/crazybytex/some-documents/blob/master/Life_beyond_Distributed_Transactions_an_Apostates.pdf

https://dl.acm.org/doi/10.1145/3012426.3025012

第三个和前面两个不一样:

This is an updated and abbreviated version of a paper by thesame name first published in CIDR (Conference on InnovativeDatabase Research) 2007

关于TCC三个词的原始解释:

Performing Tentative Business Operations To reach an agreement across entities,one entity asks another to acceptuncertainty. One entity sends anothera request that may be canceled later. This is called a tentative operation. At the end of this step, one entity agrees to abide by the wishes of the other

Tentative Operations, Confirmation,and Cancellation Essential to a tentative operation is the right to cancel.

If the requesting entity decides not to move forward, it issues a cancellation operation.

If it decides to move ahead, it issues a confirmation operation

正式以Try-Confirm-Cancel作为名称的是Atomikos公司,并且还注册了TCC商标。

国内最早可查引进TCC概念,应该是阿里程立2008年在 软件开发2.0大会 上分享主题《大规模SOA系统中的分布事务处理》中。

TCC 的本质是一种补偿型事务,它的核心思想就是每个操作都要注册一个确认和补偿(撤销回滚)操作。

该模型要求应用的每个服务提供 try、confirm、cancel 三个接口,通过对资源的预留(提供中间态,如账户状态、冻结金额等,比如库存可以单独的字段写明扣减了2个库存,但是库存字段的值先不修改),尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源。

TCC模型完全交由业务实现,每个子业务都需要实现Try-Confirm-Cancel三个接口,对业务侵入大,资源锁定交由业务方。

  1. Try:尝试执行业务,完成所有业务检查(一致性),预留必要的业务资源(准隔离性)。
  2. Confirm:确认执行业务,不再做业务检查。只使用Try阶段预留的业务资源,Confirm操作满足幂等性。
  3. Cancel:取消执行业务释放Try阶段预留业务资源。

image.png

基础形式如下图举例所示:

image.png

注意点

  1. 保持幂等性,如果try之后,第二阶段需要comfirm或者cancel时,出现网络故障,如果会重复发送comfirm后者cancel需要保障是幂等的,对于confirm和cancel都需要幂等
  2. 空回滚,如果try时,网络故障部分节点根本没收到try,但是却收到了cancel操作,需要保障不会出问题
  3. 如果网络异常导致try比cancel请求更晚到达,通过空回滚可以保障cancel不出问题,但是后续的try也不能被继续执行,confirm也是如此,但是如果没有try可能没有相关的业务数据,confirm不会对系统产生危害

以上三点是为了防止网络问题给系统带来危害。

2PC 对比

TCC与2PC的思想别无二致,都是将一个整体事务,分拆为不同的阶段步骤进行确认然后提交,可以认为是2PC思想的延伸,但是与2PC又有很大不同,这很容易理解,思想相似,具体的事务可以有很大区别。

差异 tcc 2pc
处理层 业务层处理全靠开发人员写业务代码 资源层次处理开发人员无感知
资源 tcc借助于try阶段对数据锁定是一种柔性的锁定比如记录库存-1,只是更新数据不存在资源锁 全局锁定资源所有参与者阻塞
事物类型 柔性事务 刚性事务

SAGA

1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas,讲述的是如何处理Long lived transactions (LLTs)(长活事务)。Saga是一个长活事务可被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。

https://dl.acm.org/doi/10.1145/38713.38742

https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf

此类知识点,如需简单了解本文即可,深入学习,必读论文。

还有一些译文版:

https://github.com/mltds/sagas-report

简单说:

在分布式事务场景下,我们把一个Saga分布式事务看做是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务。

正常执行事务,如果出现了问题就执行相关的补偿事务回滚。

Saga的组成

  • 每个Saga由一系列sub-transaction Ti组成
  • 每个Ti都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果

可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

  • T1 , T2 , T3 , ..., Tn
  • T1 , T2 , ..., Tj , Cj ,..., C2 , C1 ,其中0 < j < n

Saga定义了两种恢复策略:

  • backward recovery,向后恢复,补偿所有已完成的事务,如果任一子事务失败。即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。
  • forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,执行顺序是类似于这样的:T1 , T2 , ..., Tj (失败), Tj (重试),..., Tn ,其中j是发生错误的sub-transaction。该情况下不需要Ci。

显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。

理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机,网络可能会失败,甚至数据中心也可能会停电。在这种情况下我们能做些什么? 可能就是最常见又最笨拙的诸如补数据、修数据一类的~

对于ACID的保证:

Saga对于ACID的保证和TCC一样:

  • 原子性(Atomicity):正常情况下保证。
  • 一致性(Consistency),在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的。
  • 隔离性(Isolation),在某个时间点,A事务能够读到B事务部分提交的结果。
  • 持久性(Durability),和本地事务一样,只要commit则数据被持久。

(b)At the outer level full atomicity is not provided. That is, sagas may view the partial results of other sagas.

本地消息表

本地消息表也是基于补偿的一种方案。

简单说就是把要做的事情,记录下来,然后通过记录的这个消息表,进行重试或者撤销的补偿

消息生产方,需要额外建一个消息表,并「记录消息发送状态」。

消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。 如果消息发送失败,会进行重试发送。

消息表可以通过MQ或者异步调用等形式通知消费方(被调用方)消息消费方,需要「处理」这个「消息」,并完成自己的业务逻辑。

因为业务处理逻辑和消息表在一个事务里,只要处理成功,那么消息表就会记录下来,就可以进行后续的业务逻辑;

如果最开始处理失败,也就不需要有后续的处理。

消息事务

消息事务的原理是将两个事务 「通过消息中间件进行异步解耦」,和上述的本地消息表有点类似,但是是通过消息中间件的机制去做的,其本质就是'将本地消息表封装到了消息中间件中'。

就是旧瓶装新酒。

执行流程:

  • 发送prepare消息到消息中间件

  • 发送成功后,执行本地事务

    • 如果事务执行成功,则commit,消息中间件将消息下发至消费端
    • 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
  • 消费端接收到消息进行消费,如果消费失败,则不断重试

这种方案也是实现了 「最终一致性」 ,对比本地消息表实现方案,不需要再建消息表,「不再依赖本地数据库事务」 了,所以这种方案更适用于高并发的场景。目前市面上实现该方案的 「只有阿里的 RocketMQ」

小结

柔性事务的本质是最终一致性,也就是BASE理论的应用,后续所有的相关的大抵也都是补偿机制,这也是柔性的必然性,最终一致性强调了最终,也就弱化了同步的概念,所以都将会是异步、补偿、重试、撤销这类的词汇上发展,这是分布式系统的特性决定的。

https://seata.io/zh-cn/index.html作为近年来兴起的分布式事务中间件,已经得到了广泛的应用,如有必要可以参考它的设计与理念。

common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-248-1-1.html

关注下面的标签,发现更多相似文章

文章被以下专栏收录:

    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
海康摄像头接入 wvp-GB28181-pro平台测试验
[md]### 简介 开箱即用的28181协议视频平台 `https://github.c