在微服务遍地开花,分库分表屡见不鲜的环境下,你有没有想过在这样的分布式环境中事务的机制该如何得到保证?
事务以及分布式事务
在讨论分布式事务之前,我们先一起回顾一下什么是事务?事务又有什么特性?
事务是用户定义的一系列对数据库的操作,这些操作是一个逻辑执行单元,要么全部执行成功,要么全部执行失败。
事务具有以下特性
-
原子性 A
可以理解为一个事务内的所有操作要么都执行,要么都不执行
-
一致性 C
事务是从一个一致性状态到另一个一致性状态,不会停留在中间状态
-
隔离性 I
多个并发事务之间相互隔离,一个事务内部的数据对于其他事务来说是隔离的
-
持久性 D
一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响
了解什么是事务,以及事务的特性以后;我们再来看看什么是分布式事务?
分布式事务是指涉及到多个数据库或者服务之间的事务;通俗点来讲就是将单个数据库的事务概念扩展到了多个数据库(服务)的维度。
这样的场景可能已经遍布在我们的生活中,你可能没有发现?
例如:你在电商平台购物的时侯,在进行下单时,可能会涉及到下单服务,库存服务,会员服务等多个服务。
还有你在享受微信支付宝支付带来的便捷的时候,它们跟银行是怎么交互的,事务是如何得到保证的?
XA协议
在了解到分布式事务的场景后,我们先了解一下XA协议。
XA协议是由X/Open 组织提出 Distributed Transaction Processing(DTP)模型的规范。(即:分布式事务处理规范)
这个规范它定义了如下核心角色:
-
AP 应用程序
-
TM 全局事务管理器
全局事务管理器负责管理全局事务状态与参与的资源,协同资源一起提交或回滚
-
RM 资源管理器
资源管理器则负责具体的资源操作
XA规范:总之一句话:
就X/Open DTP 定义的 事务协调者与数据库之间的接口规范(即接口函数),事务协调者用它来通知数据库事务的开始、结束以及提交、回滚等。
XA 接口函数由数据库厂商提供。
以下的函数使事务管理器 TM 可以对资源管理器 RM 进行的操作:
1)xa_open,xa_close:建立和关闭与资源管理器的连接。
2)xa_start,xa_end:开始和结束一个本地事务。
3)xa_prepare,xa_commit,xa_rollback:预提交、提交和回滚一个本地事务。
4)xa_recover:回滚一个已进行预提交的事务。
5)ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
6)ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册。
XA协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现,接下来我们分别来介绍下这两种实现方式的原理。
2PC
我们先来聊聊两阶段提交,顾名思义它的提交分为两个阶段:准备阶段,提交阶段(提交或回滚)。
两阶段提交流程大致如下:
-
1.准备阶段协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了。
-
2.同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务)。
-
3.假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令。
-
4.参与者处理完以后给协调者发送响应,然后等待所有事务都提交成功之后,返回事务执行成功。
成功情况示例:
2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的;而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
2PC两阶段提交的问题:
-
事务协调者单点故障:事务协调者出现故障,事务会失败。
-
阻塞资源:占用数据库连接,性能较低。
-
数据不一致:第二阶段有的提交成功,有的提交失败会造成数据不一致。
3PC
3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit。
3PC 的出现是为了解决 2PC 的一些问题,3PC在2PC基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。
总结一下, 3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。
所以 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。
3PC 鲜有具体的实现,可以认为 3PC 只是纯的理论上的东西,而且可以看到相比于 2PC 它是做了一些努力但是效果甚微,所以只做了解即可。
TCC
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!
TCC 指的是Try - Confirm - Cancel。
Try 指的是预留,即资源的预留和锁定,注意是预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。
比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。
这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。
本地消息表
本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。
本地消息表就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
消息事务
RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。
第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
最大努力型事务
实际我们在对接第三方接口的时候,很难要求接口的提供方按照我们的要求去开发接口。
例如在对接银行,或者微信支付宝的交易接口;对于交易结果;上游一般会通过通知的方式将结果送达,但是通知一般也有频率和次数的限制.
如果在对应的时间窗口内没有接到通知或者正确的处理通知,我们就无法保证自己系统与三方系统的数据一致;为此可以下游可以主动发起查询,来尽最大努力保证数据的一致;类似的场景便是最大努力性事务的场景。
- 本文链接: https://www.sunce.wang/archives/分布式事务解决方案
- 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!