什么是事务?
通常给出的定义是数据库的执行逻辑单元。这句话感觉跟没说一样。
我们平时用到事务,主要是用到它的四个特性(ACID)。
我去看待问题的时候,通常会去问一些“傻”的问题:那为什么是这4个特性,不是其他的呢?或者为什么是4个,而不是3个或5个呢?
这4个特性被定义出来后,后续出来的数据库需要满足这4个特性,才可以说自己支持了事务。
什么叫分布式事务?
我们知道事务的作用范围是数据库,即一个事务内的数据库操作都是对同一个数据库进行的。
当一个“事务”内的数据库操作是对多个数据库进行的,这个“事务”就叫做分布式事务。
实际应用中通常情况下是以下两种情况:
一个系统的某个操作涉及多个数据源。但又希望这个操作本身满足事务的特性,这种需求就是分布式事务的需求,满足这种需求的事务就叫分布式事务。
同样地,在这个微服务盛行(烂大街)的年代,一个操作可能涉及很多个服务系统,每个服务有自己的数据源。但又希望这个操作本身满足事务的特性,这种也叫做分布式事务。
其实第一说法并不常见,因为按照微服务的单一职责原则来说,一个系统访问多个数据源还要保证事务性,要么你的数据库设计有问题,要么需要拆服务了。
如何实现分布式事务?
本质上有两种思想:
- 引入一个事务协调者
希望达到的效果是:由协调者协调多个事务,要提交大家一起提交,要回滚大家一起回滚。 - TCC
抛开数据库事务束缚,使用业务数据的状态来达到事务的特性。
这两个思想下面进行详细描述。
事务协调者
最早的分布式事务模型是 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing(DTP)模型,也就是大家常说的 X/Open XA 协议,简称XA 协议。
即引入一个事务协调者,将对数据库的提交/回滚转交给协调者进行操作。
这个方案整体看起来还行。
但有一个问题:如果4.1提交失败,4.2提交成功。这个时候,我们怎么办?
4.1提交重试是一个办法,但如果一直重试失败怎么办?
这个时候,4.2已经提交成功,又不可能再回滚了。这个时候就数据不一致了,很严重。
对于应对这种情况,有人提出了2pc和3pc的思想。注意,这里强调一下,2pc/3pc是为了应对提交失败的情况的改进。
2pc
说得简单点,就是提交这个操作分为两步,先去prepare一下,每个数据源反馈一下,提交大概率会成功还是失败。如果成功,那么就都提交,如果有一个说大概率会失败,就回滚。
缺点:
- 性能问题,在执行期间所有数据源都不能提交,资源锁定。
- 单点,协调者成为单点。
- 数据不一致,网络分区后,部分数据源收不到提交请求。
- 网络分区后,由于部分数据源收不到提交/回滚请求,可能导致该数据库资源一直得不到释放的情况,引起资源的严重浪费。
3pc
在2pc的prepare和commit之间又加了一个cancommit阶段,并且加了超时机制。
- 加了一个阶段的好处,将提交成功率的概率再次提升。因为concommit又做了一些undo日志的事情。
- 超时机制的目的是对应应对2pc的第4个缺点。
协调者的超时,即收不到参与者的ack,则认为是没有成功。
参与者的超时,在cancommit和commit之间,一直没有收到commit,则自行提交,避免占用锁。
缺点
2pc的3个缺点仍然存在,其中因引入超时机制,参与者可自行提交。可能导致数据不一致现象更加突出。
XA优点
说一下XA的优点吧,
因为是参与者的操作依赖于本地事务,所以天然具有事务的一些特性。
XA缺点
性能资源问题
中心化问题
数据不一致问题
TCC
核心点在于,不依赖事务管理器,依靠业务逻辑的分解来完成。
说得简单点,XA会hold住各个本地事务不提交,TCC不会,通过业务逻辑分解来实现业务层面的事物特性。
TCC = try + confirm + cancel
即分布式事务参与者需要提供
- try:完成所有的业务校验和大部分的逻辑处理。这个阶段几乎是要完成所有的业务处理的,只是会预留一些状态(表示中间状态)。后面的cc只是很薄的一层,理论上只是改一些状态就可以了。
- confirm:提交。修改try阶段的落地数据状态。理论上是一定要成功的,就算一次不成功,重试也要成功。所以这个阶段一定要保证幂等。
- cancel:取消也叫回滚。删除或修改try阶段的落地数据状态。同样的,理论上也必须保证成功。
分布式事务的发起者
分布式事务的框架实现粗略图。
发起者应用起本地事务,事务的开始调用分布式事务的框架进行开启一个分布式事务(注册两个事务同步器,如果本地事务提交/回滚,则调用分布式事务参与者的对应方法)。
然后发起者程序里,正常调用A和B,如果A和B失败,则需要回滚本地事务。而从触发框架的提交/回滚操作。
分布式事务补偿
虽然说分布式事务的参与者的两个CC方法,需要保证一定成功。
但这个世界不是完美的,一定会出现某个参与者的CC方法失败的情况,这个时候就需要一种分布式事务的补偿机制。
即分布式事务开启时需要记录该事务的状态,以及对应参与者的信息。
当提交成功后,方可删除,如果提交/回滚失败,则需要定时任务来进行重试。
这一步必不可少,公司实现生产中,很多依靠补偿机制来完成数据的一致性保障。
空回滚问题
在实际生产中,会遇到这类问题,作为一个分布式事务的参与者,先收到了cancel请求,然后再收到了try请求。
这种情况发生的概率还不小,这也是tcc的一种常见的异常情况。
- 为什么会出现这种情况?
发起者调用参与者时,由于网络原因或其他原因(积压),try请求一直没到参与者系统中。
然后发起者此时认为参与者超时了,发起了cancel请求。
所以无法保障try一定先于cancel,但可以保证try一定先于confirm(这个留给大家思考下原因)。 - 造成的结果
cancel到了之后,发现没有try,然后直接空处理。
后续try来了后,处理成功,然后这个请求一直没有confirm/cancel了。导致一直处于事务的悬挂状态。 - 解决办法
- 分布式锁
try来了后,加锁,完成后解锁。
cancel来了后,加锁3分钟(如果try没完成会失败),完成后不解锁。
此办法可解决cancel先来了,3分钟内try再来会直接失败掉,解决空回滚的问题。 - 前置事务表(推荐)
同样的道理。
try来了后,插入P,完成后删除。
cancel来了后,插入R(如果是P则失败),完成后不删除。
cancel先来了,插入R,后续try来了后插入P失败,解决空回滚问题。
- 分布式锁
优点
不依赖事务管理器,所以不存在事务资源性能的问题。
缺点
- 使得业务接口变得复杂。
之前一个接口,现在需要提供3个接口。 - 数据不一致
cc的两个接口必须各个业务系统实现幂等和一定成功。如果业务系统打破了,就会造成数据不一致的现象。 - 空回滚与悬挂问题
总结
这里主要说了两种分布式事务的理论:XA协议与TCC。
分析了两种理论的原理以及优缺点。
由于目前公司的分布式事务主要是TCC实现,所以对此比较熟悉。
这里也推荐大家
实际生产中,能不要用分布式事务的话,尽量不要用,会给系统带来太多的复杂性。