分布式事务
在了解一个东西之前,我们需要先搞清楚它的定义(以下是AI分布式事务的解读)
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。
对于分布式事务而言几乎满足不了 ACID,其实对于单机事务而言大部分情况下也没有满足 ACID,不然怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应用上的分布式事务了
下面开始介绍常见的分布式事务解决方案(采取循序渐进的方式)
2PC
二阶段提交(Two-Phase Commit),是一个非常经典的强一致、中心化的原子提交协议。目前,绝大多数关系型数据库都采用二阶段提交协议来完成分布式事务处理(例如mysql的XA协议)。因此二阶段提交协议也被广泛运用到分布式系统中。
顾名思义,算法流程就是分为两个阶段提交某一操作,其分为准备阶段、提交阶段。为了更好描述算法过程,为此定义了两种角色:事务管理者(TM),资源管理者(RM)
阶段一:准备阶段
在准备阶段,全局事务管理器向每个资源管理器发送准备消息,用于确认本地事务操作成功与否。
阶段二:提交阶段
在提交阶段,若全局事务管理器收到了所有资源管理器回复的成功消息,则向每个资源管理器发送提交消息,否则发送回滚消息。资源管理器根据接收到的消息对本地事务进行提交或回滚。
- 返回y 的情况(提交事务)
- 返回n 的情况(回滚事务)
2PC 优缺点分析:
-
优点:原理简单、容易实现。并且基本只在 db 做文章,所以对代码的入侵小
-
缺点:
- 同步阻塞-每个参与者都需要等待其他参与者完成后,才能继续下一阶段,也就是说事务操作逻辑都是处于阻塞状态,极大限制了分布式系统性能
- 单点问题-事务管理者在2PC中,太过重要,当TM宕机,整个集群将不可用。更可怕的是,TM在第二阶段之前宕机,那么所有参与者将一直锁定准备阶段的事务资源
- 太过保守 任何一个节点故障,都会导致整个事务协调失败,换句话说没有完善的容错机制
XA 规范
既然说到 2PC 了那么也简单的提一下 XA 规范,XA是由X/Open组织提出的分布式事务的规范,XA规范主要定义了(全局)事务管理器(TM)和(局部)资源管理器(RM)之间的接口。本地的数据库如mysql在XA中扮演的是RM角色
简单的说就是要先定义一个全局唯一的 XID,然后告知每个事务分支要进行的操作
可以看到图中执行了两个操作,分别是改名字和插入日志,等于先注册下要做的事情,通过XA START XID 和 XA END来包裹要执行的 SQL
对应2PC 的一阶段,其实就是这个PREPARE命令
然后根据准备的情况来选择执行提交事务命令还是回滚事务命令
MySQL 的XA 基本上就是这么个流程
3PC
三段提交(3PC)是对两段提交(2PC)的一种升级优化,3PC在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前,各参与者节点的状态都一致。同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC的单点故障问题
3PC 的三个阶段分别是CanCommit、PreCommit、DoCommit
CanCommit:TM向所有RM发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。
PreCommit:TM向所有RM发送PreCommit命令,询问是否可以进行事务的预提交操作,RM接收到PreCommit请求后,如RM成功的执行了事务操作,则返回Yes响应,进入最终commit阶段。一旦RM中有向TM发送了No响应,或因网络造成超时,TM没有接到RM的响应,TM向所有RM发送abort请求,RM接受abort命令执行事务的中断。
DoCommit: 在前两个阶段中所有RM的响应反馈均是YES后,TM向RM发送DoCommit命令正式提交事务,如TM没有接收到RM发送的ACK响应,会向所有RM发送abort请求命令,执行事务的中断
3PC 多了一个阶段其实就是在执行事务之前来确认RM是否正常,防止个别RM不正常的情况下,其他RM都执行了事务,锁定资源。出发点是好的,但是绝大部分情况下肯定是正常的,所以每次都多了一个交互阶段就很不划算
从个人的观点出发,我觉得 3PC 的引入并没什么实际突破,而且性能更差了。。。
以至于我搜罗了好久,压根没找到实际的3PC 的落地示例,反倒是2PC,确实有不少的落地示例(所以可以理解为3PC是纯理论)
TCC
不知道大家注意到没,不管是 2PC 还是 3PC 都是依赖于数据库的事务提交和回滚,但是我们在实际开发的过程中,还有一些其他的资源,比如上传一张照片或者发送某个消息等等。所以说事务的提交和回滚就得提升到业务层面而不是数据库层面了,而 TCC 就是一种业务层面或者是应用层的两阶段提交。
TCC 分为指代 Try、Confirm、Cancel ,也就是业务层面需要写对应的三个方法,主要用于跨数据库、跨服务的业务操作的数据一致性问题
Try 指的是预留,即资源的预留和锁定,注意是预留。 Confirm 指的是确认操作,这一步其实就是真正的执行了。 Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了
其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。
比如说一个事务要执行A、B三个操作,那么先对两个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作
可以看到流程还是很简单的,难点在于业务上的定义,对于每一个操作你都需要定义三个动作分别对应Try - Confirm - Cancel。因此 TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作(代码工作量大)
虽说对业务有侵入,但是 TCC 没有资源的阻塞,每一个方法都是直接提交事务的,如果出错是通过业务层面的 Cancel 来进行补偿,所以也称补偿性事务方法。
这里有人说那要是所有人 Try 都成功了,都执行 Comfirm 了,但是个别 Confirm 失败了怎么办?
这时候只能是不停地重试调失败了的 Confirm 直到成功为止,如果真的不行只能记录下来,到时候人工介入了
本地消息表
本地消息就是利用了本地事务,会在数据库中存放一直本地事务消息表,在进行本地事务操作中加入了本地消息的插入,即将业务的执行和将消息放入消息表中的操作放在同一个事务中提交
这样本地事务执行成功的话,消息肯定也插入成功,然后再调用其他服务,如果调用成功就修改这条本地消息的状态。
如果失败也不要紧,会有一个后台线程定时扫描,发现这些状态的消息,会一直调用相应的服务,一般会设置重试的次数,如果一直不行则特殊记录,待人工介入处理。
可以看到还是很简单的,这是一种最大努力通知思想
消息事务
RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。
第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可
事务消息也可以算最大努力的思想
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入预警/丢弃,其实这也算最大努力
参考文献: