探索分布式事务解决方案:八种方案解析
前面已经学习了分布式事务的基础理论CAP 理论和 BASE 理论,以理论为基础,针对不同的分布式场景业界常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知等方案,**以下 总结8 种常见的解决方案,取名 八奇技**。帮助大家在实际的分布式系统中更好地运用事务。。
1.2PC
二阶段提交协议(Two-phase commit protocol),简称 2PC。2PC是将整个事务流程分为两个阶段:
- 1.准备阶段(Prepare phase)
- 2.提交阶段(commitphase)
2是指两个阶段,P是指准备阶段,C是指提交阶段
在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议,如下图:
- 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入据文件)
- 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。
注意:必须在最后阶段释放锁资源
下图展示了2PC的两个阶段,分成功和失败两个情况说明:
- 成功情况:
图片
- 异常情况:
图片
2PC优缺点:
优点
- 简单直观:逻辑清晰,易于理解和实现。
- 原子性保证:能够保证跨多个分布式节点的事务的原子性。
缺点:
- 同步阻塞:因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差,在高并发场景下不适用
- 单点故障问题,如果协调者在第二阶段崩溃,参与者可能会无限期地等待指令,因为它们不知道应该提交还是回滚。这使得整个系统容易受到单点故障的影响
- 数据不一致问题,如果在第二阶段中协调者向某些参与者发送了提交指令,而其他参与者因为网络问题没有收到指令,那么这些没有收到指令的参与者可能会选择回滚,导致数据不一致
2.3PC
3PC,即Three-Phase Commit,是一种分布式事务协议,用于在分布式系统中确保多个参与者之间的事务操作的一致性和可靠性。它是在两阶段提交(2PC)协议的基础上发展而来,解决了2PC协议可能出现的悬挂事务问题。
3PC协议将提交操作分为三个阶段,分别是准备阶段、提交准备阶段和提交阶段,每个阶段都有对应的操作和协议。
准备阶段(CanCommit):
- 协调者:向所有参与者发送CanCommit准备请求,询问它们是否可以提交事务。
- 参与者:执行本地事务,检查是否能够执行,如果可以执行则返回可以提交,否则返回不可以提交。
提交准备阶段(PreCommit):
- 协调者: 根据参与者的反馈情况决定是否进行提交准备
- 如果所有参与者都返回“可以提交”,协调者向所有参与者发送提交请求,告知它们可以进行提交准备。
- 如果有任何参与者返回“不可以提交”或者超时未响应,则协调者向所有参与者发送中止请求,取消事务。
提交阶段(DoCommit/DoAbort):
- 如果协调者 接收到所有参与者的确认提交消息,则向所有参与者发送最终的提交请求,提交事务。
- 如果协调者接收到任何参与者的中止请求,或者在提交准备阶段超时未收到所有参与者的响应,则向所有参与者发送中止请求,取消事务
3PC协议相对于2PC协议的改进在于增加了一个准备阶段,使得参与者在准备阶段就能够知道是否可以提交事务,从而避免了悬挂事务问题。然而,3PC协议仍然存在着协调者单点故障、消息丢失等问题,因此在实际应用中并不常见,一般更多地使用2PC、Saga等分布式事务解决方案
3.TCC
TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。Try操作业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。
- 分支事务成功情况:
图片
- 分支事务失败的情况:
图片
TCC分为三个阶段
- Try 阶段:是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能真正构成一个完整的业务逻辑。
- Confirm 阶段:是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。。
- Cancel 阶段:是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理
TCC需要注意三种异常处理
空回滚
在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。
出现原因:是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
解决思路是
关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。
幂等
TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。
解决思路 在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。
悬挂
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
出现原因: 在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。
解决思路:如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。
TCC优缺点:
TCC的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
4. 分布式补偿事务(Saga)
Saga是一种长事务的解决方案,它将一个大的分布式事务拆分成多个较小的本地事务,并通过异步消息传递来串联这些本地事务。每个本地事务执行成功后,会发送消息触发下一个事务的执行。如果某个本地事务失败,Saga会执行一系列补偿操作,保持数据的一致性。
分布式补偿事务(Saga) 优缺点
优点
- 灵活性: 允许每个小事务独立管理,提高了系统的灵活性。
- 减少资源锁定: 不需要持续占用资源,提高了系统的并发能力。
- 容错性: 通过定义补偿操作来处理失败,增强了系统的容错能力。
- 适用于微服务架构: 可以跨服务边界管理事务,每个服务都可以独立处理自己的事务和补偿逻辑。
缺点
- 复杂性: 实现Saga需要定义每个小事务的补偿操作,增加了系统的复杂性。
- 数据一致性: 不能提供即时一致性保证,只能保证最终一致性。
- 补偿操作的难度: 在某些情况下,补偿操作可能很难实现,特别是当事务有副作用时。
- 测试和调试: 涉及多个服务和补偿逻辑,测试和调试可能会更加困难。
在选择使用Saga模式时,需要仔细考虑业务场景是否适合最终一致性,以及是否能够有效地实现和管理补偿逻辑。对于需要高度一致性保证的场景,可能需要考虑其他事务管理机制。Saga模式在适当的情况下可以为分布式系统带来灵活性和容错性,但需要慎重考虑其复杂性和实现难度。
5. 可靠消息最终一致性
可靠消息最终一致性方案:是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
此方案是利用消息中间件完成,如下图:
图片
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。
可靠消息最终一致性方案要解决以下几个问题
1. 本地事务与消息发送的原子性问题
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。 先来尝试下这种操作,先发送消息,再操作数据库:
begin transaction;
//1.发送MQ
//2.数据库操作
commit transation;
这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败立马想到第二种方案,先进行数据库操作,再发送消息:
begin transaction;
//1.数据库操作
//2.发送MQ
commit transation;
这种情况下貌似没有问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数据库回滚,但MQ其实已经正常发送了,同样会导致不一致。
2. 事务参与方接收消息的可靠性
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
3. 消息重复消费的问题
由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性
6. 本地消息表方案
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明: 共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
图片
交互流程
- 用户注册 用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致)
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。
- 定时任务扫描日志
思考:如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
- 消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复 投递此消息。由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性
7.最大努力通知原则
最大努力通知也是一种基于消息的分布式事务解决方案,但它不保证 100% 的消息传递成功。它的工作原理是:
- 在本地事务执行成功后,系统会尝试通知其他的参与者或服务。
- 通知操作会尽最大努力去执行,但如果失败,系统不会无限重试。
- 该方案通常结合人工干预,例如,如果通知失败,系统可能会记录日志、发送报警、或者提供管理界面供操作人员手动处理。思考:最大努力通知与可靠消息一致性有什么不同?
解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
- 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
两者的业务应用场景不同
- 可靠消息一致性:关注的是交易过程的事务一致,以异步的方式完成交易。
- 最大努力通知:关注的是交易后的通知事务,即将交易结果可靠的通知出去。
技术解决方向不同
- 可靠消息一致性:要解决消息从发出到接收的一致性,即消息发出并且被接收到。
- 最大努力通知:无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
8. 分布式锁
在某些业务场景,使用分布式锁是确保多个分布式节点不会同时操作同一资源的有效方法。这一机制可以通过使用像Redis、ZooKeeper等分布式协调服务来实现
应用场景: 在电商秒杀活动中,为了防止超卖现象,需要确保同一时间只有一个请求能够对库存数量进行修改。这时,可以使用Redis作为分布式锁的后端存储,以确保秒杀活动的进行顺利和公平。
推荐场景: 当需要协调多个节点对共享资源进行访问控制时,分布式锁是一个非常有效的解决方案。例如,在分布式系统中,多个节点需要同时对同一资源进行读取或更新操作时,为了保证数据的一致性和避免竞态条件,可以使用分布式锁来进行并发控制。