分布式事务是什么
本文的分布式事务指的是DTM下的分布式事务。
分布式事务有两类,这里指的是跨数据库、跨服务的分布式事务。
分布式事务指事务的发起者、资源及资源管理器和事务协调者分别位于分布式系统的不同节点之上。
CAP理论
- C(一致性):指分布式节点中数据一致。
- A(可用性):指无论何时向分布式系统发送请求都能响应。
- P(分区容错性):发生网络分区时会产生脑裂问题,分区容错性就是解决脑裂问题。
在CAP理论中,C、A、P只能满足其中的一种或两种,无法同时满足三种情况。
BASE理论
- BA(基本可用):极端情况下牺牲部分可用性,只需满足基本可用即可,但不等于完全不可用。
- S(软状态):允许数据存在中间态,即允许数据传输存在延迟。
- E(最终一致性):无需强一致性,在一段时间后可以满足一致性即可。
无法保证强一致性
分布式事务中一致性最强的是XA模式(见下面事务模式-XA),XA是采用两个阶段执行事务的:准备阶段(prepare)和提交/回滚阶段(commit/rollback)。
可是开始事务操作以及提交/回滚阶段并不能保证所有RM(资源管理者,可以理解成数据库实例)都能同步进行,所以不能保证强一致性。
如上图所示,隔离级别为可重复读,这里查询数据的时机是:微服务2中的RM事务已提交,微服务1中的RM未提交。此时查询到的结果为A+B+30,数据不一致。
如果隔离级别为序列化,也会存在不一致的情况,如下图所示:
上图中隔离级别为序列化,查询时机为:微服务1中RM开启事务,微服务2中RM为开启事务,返回结果为A+B-30,数据不一致。
理论上的强一致性
理论上可以在读已提交隔离级别实现强一致性,需要为读加上写锁,即select for update的形式。
这样子,读的数据要么都是事务启动前的,要么都是事务提交后的,不会出现一个读取到的是事务启动前的数据、一个读取到的是事务提交后的数据的情况。
但是这样子对性能影响很大。
最终一致性
分布式事务一致性不能保证的情况发生在分布式事务进行当中。当事务完成后是可以保证一致性的,这里的一致性还包括数据库完整性。
以下是各个分布式事务模式中,一致性强度的分类,一致性由强到弱分别是:
XA事务>TCC>二阶段消息>SAGA
其中,XA事务是通过commit提交事务的,而TCC是在业务层通过confirm提交事务的,理论上来说XA的不一致性时间要比TCC的短。
异常与子事务屏障
NPC问题
NPC是Network Delay、Process Pause、Clock Drift的简写,是分布式系统会遇到的重大挑战,其中对分布式系统影响最大的是NP
- Network Delay:由于网络环境复杂,不可避免的会出现网络延迟的问题。
- Process Pause:由GC机制等各种情况导致的进程暂停,特别是云服务中可以利用进程暂停将一台主机迁移至另一台主机,进程暂停时间可能会长达数十分钟。
- Clock Drift:计算机中计时会存在误差,在分布式系统中就会导致每个节点之间时间不一致的问题。为了同一时间,一般会使用NTP服务统一各节点的时间,但这样会导致节点的时间突然向前或先后偏移。
异常分类
以分布式事务TCC为例,分布式系统的两大难题:
- 悬挂:如果发生网络乱序,导致comfirm/cancel操作先执行,try操作后执行,那么事务将永远无法完成。解决方法:在执行try操作时判断事务是否执行过comfirm/cancel,如果执行过则拒绝执行try操作,直接返回。
- 空补偿:try执行失败触发cancel补偿,但是由于网络乱序,导致cancel请求比try请求先到达,从而导致先执行cancel操作,这就是空补偿。解决方法:在执行cancel时判断事务是否执行过try,如果没有则拒绝执行cancel操作,直接返回。
此外,发生请求失败,重试过程中也会遇到重复请求的情况,如果请求不幂等,会导致异常。
- 幂等:无论请求多少次,请求的结果都是相同的,不会对原有数据进行改变。
因为空补偿、悬挂、重复请求都跟NP有关,我们把他们统称为子事务乱序问题。在业务处理中,需要小心处理好这三种问题,否则会出现错误数据。
大众方案的问题
大众对异常解决方案是:允许空补偿和防悬挂。
- 允许空补偿:因为补偿请求之前肯定有事务执行请求,因此可以允许空补偿,但是要标记空补偿。
- 防悬挂:在执行事务前,要判断这个事务是否补偿过,如果补偿过要拒绝执行事务,避免悬挂。
这种处理方法在极端场景下会出问题:假如执行事务和进行补偿的是两个节点,执行事务的节点在执行过程中发送进程暂停(NPC中的P),补偿操作的节点正常执行。导致补偿操作比执行事务操作先完成,这又导致了空悬挂问题。
PS:幂等控制如果也采用“先查再改”,也是一样很容易出现类似的问题。解决这一类问题的关键点是要利用唯一索引,“以改代查”来避免竞态条件。
DTM的幂等控制原理是,插入一条有关请求的唯一主键,如果插入失败直接返回成功,不用执行后续业务流程,否则将执行业务流程。
子事务屏障
dtm的一项技术,在dtm层面解决异常问题,具体效果如下图所示:
子事务屏障会管理TCC、SAGA、事务消息等,也可以扩展到其他领域
原理
子事务屏障技术的原理是,在本地数据库,建立分支操作状态表dtm_barrier,唯一键为全局事务id-分支id-分支操作(try|confirm|cancel)
具体操作如下:
- 开启本地事务
- 对于任意操作(try/comfirm/cancle):在表中插入唯一键,如果插入失败则提交事务返回成功(常见的幂等控制法)
- 对于补偿操作(cancle):在表中插入try的键,如果插入成功,提交事务返回成功
- 调用业务逻辑,如果执行成功则提交事务返回成功,否则回滚事务并返回失败
这样可以满足空补偿、悬挂和幂等问题:
- 解决空补偿问题:在执行cancle时,会在表中插入cancle的唯一键后插入try的唯一键,如果插入try成功就代表没有执行try,就不执行补偿了,从而解决空补偿问题。
- 解决悬挂问题:在执行try时,会在表中插入try的唯一键。如果cancle在try前执行,表中就存在了try的唯一键,那try就不会被执行。
- 解决幂等问题:由于每个操作都会在表中插入唯一键,就保证真正执行只会执行一次,保证了操作的幂等性。
竞争分析
如果try和cancle在同一时刻执行,那么会有一个先开启事务,没开启事务的要等到前一个事务执行完成在开启事务。
- try先cancle后:正常执行
- cancle先try后:可能会有空补偿和悬挂问题,由于子事务屏障的存在,不会发生空补偿和悬挂问题
- 发生宕机:会进行重试
最终成功
最终成功指的是,允许操作暂时性失败,但是通过重试等操作,最终使操作成功。
最终成功的情况
最终成功的情况包括以下方面
- 二阶段消息中的分支操作
- SAGA中的回滚操作
- TCC的Confirm和Cancel操作
业务中需要保证以上的操作能正常执行,在业务逻辑上是可以最终成功的。
为什么要最终成功
在SAGA中,回滚假如不能最终成功,那么会陷入一个很尴尬的局面:该事物既无法提交,又无法回滚,使得事务不能正常进行。
分布式事务无法解决这类问题,所以要在业务代码中保证回滚的最终成功。
应用如何设计
😰没看懂
- 二阶段消息的分支操作是最终成功的,因为二阶段消息不支持回滚。如果您需要回滚,那么请采用其他事务模式
- SAGA事务如果有些正向操作是无法回滚的,那么您可以用普通非并发的SAGA,将可回滚的分支Ri放在前面,不可回滚的分支Ni放在后面。如果分支Ri正向操作失败,则会回滚Ri,一旦到了Ni,保证Ni的正向操作最终成功,这样也能够保证SAGA事务的正确运行
- TCC事务的Confirm/Cancel是最终成功的,一般的设计是在TCC的Try阶段预留资源,检查约束条件,然后在Confirm阶段修改数据,在Cancel阶段释放预留的资源。经过精心的设计,能够在业务逻辑上保证Confirm/Cancel的最终成功
注意点
在实际的业务应用中,可能会出现某些应用bug,导致要求最终成功的操作,一直无法成功,导致数据一直无法达到最终一致。建议开发者对全局事务表进行监控,发现重试超过3次的事务,发出报警,由运维人员找开发手动处理,参见dtm的运维
XA
是什么
一种分布式事务的规范,主要定义了两个角色:TM(事务管理器)和RM(资源管理器)。
一个XA事务包含两个阶段:
- 准备阶段(prepare):当RM完成本地事务时,向TM发送prepare命令
- 提交阶段(commit/rollback)当所有RM都准备好了,则进行commit;反之则rollback。
本地数据库如何支持XA
-- 第一阶段 准备
XA start '4fPqCNTYeSG' -- 开启一个 xa 事务
UPDATE `user_account` SET `balance`=balance + 30,`update_time`='2021-06-09 11:50:42.438' WHERE user_id = '1'
XA end '4fPqCNTYeSG'
XA prepare '4fPqCNTYeSG' -- 此调用之前,连接断开,那么事务会自动回滚
-- 当所有的参与者完成了prepare,就进入第二阶段 提交
xa commit '4fPqCNTYeSG'
好处与坏处
- 简单易理解
- 开发较容易,回滚之类的操作,由底层数据库自动完成
- 对资源进行了长时间的锁定,并发度低,不适合高并发的业务
SAGA
SAGA分布式事务模式的精髓是:将一个长事务拆分成多个短事务,由SAGA协调器负责协调。如果所有短事务完成,则全局事务完成,反之则由当前异常的短事务开始反向按顺序进行补偿操作。
SAGA的事务操作是服务端编排的。
拆分为子事务
有一个银行转账的分布式事务:A向B转账30元
SAGA事务会拆成一下事务:
- A转出事务:A账户中-30元
- A转出补偿事务:在执行过A转出事务的数据库中将A账户+30元
- B转入事务:B账户中+30元
- B转入补偿事务:在执行过B转入事务的数据库中将A账户-30元
这里将一个全局事务拆分为两个短事务:A转出事务和B转入事务,成功执行事务顺序为:执行A转出事务 -> 执行B转入事务
如果A转出事务执行失败,那么整个事务执行流程应该是:A转出事务(失败) -> A转出补偿事务
如果B转入事务执行失败,那么整个事务执行流程应该是:A转出事务 -> B转入事务(失败) -> B转入补偿事务 -> A转出补偿事务
时序图如下,其中A转出事务为TransOut,B转入事务为TransIn:
补偿要考虑的情况
在短事务执行过程中,会有三种情况:1. 执行成功 2. 执行失败 3. 还在执行中,无法判断成功或失败,在事务补偿中,这三种情况都需要去考虑。
DTM提供了子事务屏障技术,自动处理上述三种情况,开发人员只需要编写好针对1的补偿操作情况即可,相关工作大幅简化,详细原理,参见下面的异常章节。
失败的分支是否需要补偿?
dtm 常被问到的一个问题是,TransIn返回失败,那么这个时候是否还需要调用TransIn的补偿操作?DTM 的做法是,统一进行一次调用,这种的设计考虑点如下:
- XA, TCC 等事务模式是必须要的,SAGA 为了保持简单和统一,设计为总是调用补偿
- DTM 支持单服务多数据源,可能出现数据源1成功,数据源2失败,这种情况下,需要确保补偿被调用,数据源1的补偿被执行
- DTM 提供的子事务屏障,自动处理了补偿操作中的各种情况,用户只需要执行与正向操作完全相反的补偿即可
异常
偶发失败
在偶发的网络抖动、机器宕机、进程Crash的情况下,会偶发请求失败的场景,解决方法就是重试。
DTM中应该支持幂等的接口都支持幂等,所以在DTM中重试是安全的。
故障宕机
DTM在重试方面做了指数退避算法,如果遇见了故障宕机情况,那么指数退避可以避免大量请求不断发往故障应用,避免雪崩。
网络乱序
分布式系统中,网络延时是难以避免的,所以会发生一些乱序的情况,例如转账的例子中,可能发生服务器先收到撤销转账的请求,再收到转账请求。这类的问题是分布式事务中的一个重点难点问题,详情参考:异常与子事务屏障
二阶段消息
假设有三个微服务:本地应用A、微服务B、协调者C。
二阶段消息主要有三个流程:Prepare、Submit和QueryPrepared。
- 本地应用A会发起Prepare请求给协调者C,预留事务资源和QueryPrepared接口。之后本地应用A开启并执行事务,协调者什么也不做。
- 本地应用A执行完事务后,向协调者C发起Submit请求。协调者C收到请求后,将事务同步到微服务B。
- 如果本地应用A超过一段时间未发起Submit,协调者C就会调用QueryPrepared接口。如果返回事务执行成功,则同步事务到微服务B。否则事务失败。
回查原理
在回查时,本地应用A的事务状态会有三种情况:执行中、事务执行成功、事务执行失败。
回查(QueryPrepared)原理:本地应用A在事务执行时会在数据库插入一条唯一主键gid(全局事务id)、原因为committed的数据。在回查的时候,会在数据库insert ignore一条主键为gid、原因为rollback的数据(由于insert带X锁,如果事务还在执行,sql会被堵塞,知道上一条事务结束才会被执行)。最后在查询数据库主键为gid的数据,查看事务执行结果。
为什么要在查询前insert?
如果事务在执行中,我们直接查询主键为gid的数据是查不到的,同样,如果事务发送回滚的话,查询gid也是查不到的。
如果只select,一般业界的做法是:重复轮询直到查出结果,如果超过两分钟还查不到数据,就认为事务回滚了。
但是如果机器出现P(Process Pause)情况,协调者认为事务回滚了,可是事务在P恢复时成功提交了,这又变成了悬挂问题。同时,长事务等情况的出现会导致轮询请求造成服务器不必要的压力。
易用性
采用新架构处理一致性问题,仅需要:
定义好本地业务逻辑,指定下一步处理的服务即可 定义QueryPrepared处理服务,复制粘贴例子代码即可。
TCC
TCC与XA类似,只不过TCC的阶段实在业务层上执行,XA阶段在数据库层上执行。在设计上,TCC主要用于处理一致性要求较高、需要较多灵活性的短事务。
TCC的事务操作是客户端编排的。
TCC是Try、Confirm、Cancel三个词语的缩写,是TCC事务过程中的三个阶段:
- Try:预留必须的业务资源,进行业务的一致性检查操作,尝试执行。
- Confirm:如果所有的try都成功了,执行业务,不做任何业务检查,只使用到try阶段预留的业务资源。
- Cancel:如果try执行失败了,执行补偿操作。即释放try预留的业务资源。
由于TCC实在业务层面上执行的分布式事务,这三个阶段的业务逻辑都是自定义的,这使得TCC变得更佳灵活,开发者主要保证业务逻辑符合对应阶段的要求即可。
时序图如下:
失败回滚
如果任意try失败了,TCC会将全局事务中的所有子事务回滚。由于DTM的子事务屏障,开发者无需考虑回滚遇到的情况,只需要编写好回滚业务代码,执行回滚操作即可。
更好的一致性
由于TCC中有prepare阶段,因此操作更佳灵活,可以更好的保障一致性。下面还是以银行转账A转B30为例子。
由于SAGA只有一个提交,进行A余额扣除30的操作是真实扣除的。假如在进行B转账操作时失败,虽然会触发失败回滚,但是回滚期间A的余额确实扣除了30,知道回滚结束后A的余额扣除的30才得到返回。
使用TCC事务,可以在数据库中增加一个冻结余额字段:
- 在准备阶段(Prepare):检查A的余额是否不少于30,检查完成后在冻结余额中增加30。
- 在提交阶段(Confirm):A的冻结余额中减少30,同时余额减少30。
- 在补偿阶段(Cancle):A的冻结余额减少30。
通过增加一个冻结字段,使得转账业务的一致性得以保证,在转账中不会出现转账失败,A的余额又被扣除的情况。
只适合短事务
由于TCC事务实在应用层上进行的,且编排是由客户端编排的。一旦发送进程崩溃等情况,会导致编排内容丢失,这种情况只能回滚(对比SAGA,同样的情况SAGA可以重试,因此SAGA适合长事务)。
DTM
DTM中的分布式事务
DTM中的ACID
- 原子性:严格遵守原子性
- 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽
- 隔离性:严格遵守事务间不互相影响,事务运行时的可见性可以适当安全放宽
- 持久性:严格遵守持久性