如何选择分布式事务解决方案?

方案设计没有绝美,只有合适和高效。               ------微微一笑

引言

上一篇介绍了分布式事务的基础理论认识,这章我们重点针对业界常用分布式解决方案作比较,深入理解在实际工程经验中如何合理选择对应的分布式事务解决方案。

阅读此文,你将收获:

进入正题之前,我们先用一个生活的例子说明一个简单的事务是怎样形成的,就行这章图中,正如完成一场田径比赛,我们复盘下需要做哪些人和事的准备。

角色扮演:

  1. 裁判员:负责安排起跑前准备动作的合规性+发令枪;
  2. 运动员:准备就位+完成起跑和冲刺

注意事项:

  1. 就位:远动员起跑前复合规定要求;
  2. 开跑:正式在赛道上狂飙

我们将这个简单的生活场景抽离出一个2PC事务模型:

  • 裁判员(TM): 事务管理者(Transaction Manager)
  • 运动员(RM): 资源管理器(Resource Manager),也即事务参与者,可理解为一个数据库实例

常用分布式事务解决方案

1、二阶段提交(2PC)

二阶段提交(Two-Phase Commit,2PC)是一种分布式事务协议,用于确保多个参与者在分布式环境中的事务操作的一致性。它是一种典型的协调者-参与者模型,包括一个协调者和多个参与者。用一段简单伪代码描述基本的两阶段提交(Two-Phase Commit)过程

diff 复制代码
-- 创建事务
START TRANSACTION;

-- 第一阶段:准备阶段
-- 检查所有参与者是否准备就绪
-- 如果有一个参与者未准备就绪,则回滚事务
-- 否则,进入第二阶段
PREPARE TRANSACTION;

-- 第二阶段:提交阶段
-- 提交所有参与者的事务
-- 如果有一个参与者提交失败,则回滚事务
-- 否则,确认事务
COMMIT;

成功情况:

失败情况:

流程说明:

1. 准备阶段(Prepare phase):

事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)

2.提交阶段(commit phase):

如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。

注意:必须在最后阶段释放锁资源。

从完整的提交过程来看,二阶段提交具有以下特点:

优点:

  • 使用简单: 2PC易于理解和实现。它只有两个明确定义的阶段:准备阶段和提交阶段。
  • 一致性: 2PC的设计目标之一是保证事务的一致性。在正常情况下,如果一个参与者已经准备好,那么整个事务将被提交,否则将回滚。

缺点:

  • 阻塞: 在第一阶段,协调者会向所有参与者发送准备请求,并等待它们的响应。如果有参与者无法响应或发生超时,协调者可能会一直等待,导致阻塞。
  • 单点故障: 如果协调者在第二阶段发生故障,可能会导致整个事务的无法完成。这使得2PC对于单点故障敏感。
  • 性能: 由于需要等待所有参与者的响应,2PC可能在性能上表现不佳,尤其是在分布式环境中。
  • 不适用于所有场景: 2PC对于一些特殊情况,如网络分区或参与者故障的情况下可能无法提供理想的性能和可用性。

典型的二阶段方案有(后续展开说明):分布式Seata方案

2、三阶段提交(3PC)

三阶段提交的基本思想是在传统的二阶段提交协议的基础上引入一个准备阶段,以降低阻塞时间和减轻单点故障的影响。流程如下图(图片来源于:苏三说技术):

三阶段流程:

1. CanCommit(准备阶段): 协调者向所有参与者发送准备请求,要求它们准备好提交。如果所有参与者都准备好,它们将返回"可以提交"(Yes)的消息;否则,返回"不可以提交"(No)的消息。

2. PreCommit(准备提交阶段): 如果所有参与者都回复"可以提交",协调者将发送准备提交请求,要求它们提交事务。如果任何参与者在这个阶段发生故障或者无法提交,它们会通知协调者,然后协调者会向所有参与者发送中止请求。

**3. DoCommit(提交阶段):**如果在准备提交阶段没有发生故障,协调者将发送正式的提交请求,要求所有参与者提交事务。参与者在收到这个请求后进行实际的提交操作。

对于三阶段提交,相对于二阶段做了一些改进:

  • 减少阻塞: 3PC引入了第三阶段,即"准备"阶段,可以在第一和第二阶段之间减少阻塞的时间。在3PC中,如果参与者在准备阶段无法完成准备,它可以通知协调者,从而避免长时间的阻塞。

  • 降低单点故障的影响: 3PC通过引入准备阶段来降低协调者的单点故障的影响。即使协调者在第一或第二阶段故障,参与者可以通过准备阶段的信息来决定是否提交事务。

  • 异步通信: 3PC采用了异步通信的方式,减少了同步等待的时间,提高了系统的响应性能。

仍然存在的一些问题:

  • 性能: 尽管3PC在某些情况下降低了阻塞时间,但在性能上仍然可能不如一些其他分布式事务协议。

  • 不解决网络分区问题: 3PC仍然无法完全解决网络分区的问题,当分区发生时,可能会导致无法达成一致性。

小结:二阶段提交和三阶段提交的比较

特征 二阶段提交 (2PC) 三阶段提交 (3PC)
参与者状态 参与者在第一阶段提交前不能释放资源 参与者在第二阶段前不能释放资源
超时处理 存在阻塞问题,可能导致永久阻塞 在超时情况下可以更好地处理
单点故障 协调者故障可能导致事务无法完成 在第三阶段引入的准备阶段有助于降低单点故障的影响
同步/异步 同步协议,需要等待所有参与者响应 异步协议,减少了同步等待的时间
一致性级别 强一致性 不同实现可能达到弱一致性或中等一致性
性能 通常比3PC性能更好,因为只有两个阶段 可能因为引入第三阶段而导致性能略有降低

3、TCC(事务补偿)方案

TCC (全英文 "Try, Confirm, Cancel"),它表示一种分布式事务的处理模式。这种模式旨在解决分布式事务的一致性和隔离性的问题。在这种模式下,事务分为三个阶段:尝试(Try)、确认(Confirm)、取消(Cancel)。

**1. Try阶段:**用于业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能真正构成一个完整的业务逻辑。

**2. Confirm阶段:**做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引 入重试机制或人工处理。

**3. Cancel阶段:**是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。

成功情况(Try-Confirm):

失败情况(Try-Cancel):

TCC(Try, Confirm, Cancel)事务模型的一些特点:

  • 三阶段处理: TCC事务由三个连续的阶段组成,分别是尝试(Try)、确认(Confirm)和取消(Cancel)。这有助于在分布式环境中更好地管理事务的一致性。
  • 自定义业务逻辑: TCC允许开发者在每个阶段定义自己的业务逻辑。在尝试阶段,业务逻辑尝试执行操作;在确认阶段,确认执行操作;在取消阶段,执行撤销操作。
  • 无锁设计: TCC通常采用无锁设计,通过在每个阶段引入补偿操作,从而减少了对全局锁的需求,提高了并发性能。
  • 弱隔离性: TCC相对于传统的两阶段提交(2PC)可能在隔离性上更为灵活,但一般来说,它通常表现为弱隔离性,需要业务层面的逻辑来处理潜在的并发问题。
  • 业务补偿: TCC事务模型通过在取消阶段引入业务补偿操作来处理在尝试阶段发生失败时的情况。这种机制使得系统能够更好地应对异常状况。
  • 适用于高并发: 由于采用了无锁设计和自定义业务逻辑的方式,TCC通常适用于高并发的分布式系统场景。

4、可靠消息最终一致性

可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方达到最终事务一致性。 通常利用消息中间件完成,如下图:

流程也很简单:

  1. 事务发起方(消息生产方)将消息发给消息中间件
  2. 事务参与方(消息消费者)从消息中间件接收消息

注意:事务发起方、消息中间件和事务参与方之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题

但是可靠消息最终一致性方案要解决以下几个问题:

1.本地事务与发送消息的原子性问题

这是实现可靠消息最终一致性方案的关键问题。事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。

1.1. 先发送消息,再操作数据库:

arduino 复制代码
begin transaction;
  //1.发送MQ
  //2.本地数据库操作
commit transation;

这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败。

1.2. 先进行数据库操作,再发送消息:

arduino 复制代码
begin transaction;
  //1.本地数据库操作
  //2.发送MQ
commit transation;

这种情况下,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是网络超时,数据库会执行回滚,但MQ其实已经正常发送了,同样会导致数据不一致。

2、事务参与方接收消息的可靠性

事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。

3、消息重复消费的问题

由于网络传输容易出现丢包,阻塞等问题,若某一个消费节点超时但是消费成功,此时消息中间件会自动重复投递此消息,就导致了消息的重 复消费。此处涉及幂等性的问题(这里不多说)。

那如何解决这些问题?这里介绍两种常用的方案。

方案一、本地消息表方案

用户表和消息表通过本地事务保证操作原子性

还是以上一节注册用户送积分的业务场景,介绍本地事务消息表方案。 交互流程如下:

(1)用户注册

arduino 复制代码
begin transaction;
  //1.新增用户
  //2.存储积分消息日志
commit transation;

用户服务在本地事务执行.这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。

(2)定时任务扫描日志表

如何保证将消息发送给消息队列呢?

  • 启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件;
  • 在消息中间件反馈发送成功后删除该消息日志;
  • 没有成功,等待定时任务下一周期重试。

(3)消费消息

如何保证消费者一定能消费到消息呢?

  • 这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。

  • 积分服务接收到"增加积分"消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复 投递此消息。

  • 由于消息会重复投递,积分服务的"增加积分"功能需要实现幂等性。

方案二、 RocketMQ事务消息方案

关于RocketMQ 事务消息设计,我们关注以下几个核心点:

  1. 为了解决 Producer 端的消息发送与本地事务执行的原子性问题

  2. RocketMQ 的设计中 Broker 与 Producer 端的双向通信 能力,使得 broker 天生可以作为一个事务协调者存在;

  3. RocketMQ 本身提供的存储机制为事务消息提供了持久化能力(内部封装本地消息表);

执行流程如下:

为方便理解我们还以注册送积分的例子来描述 整个流程。 Producer即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。

  1. Producer (MQ发送方)发送事务消息至MQ Server;MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的;
  2. MQ Server回应消息发送成功;
  3. Producer 执行本地事务,即:添加用户操作;
  4. 消息投递

若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将"增加积分消息" 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;

若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除"增加积分消息" 。

MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里程序执行正常则自动回应ack。

5、事务回查

如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。

以上由RocketMQ实现主干流程,可以看到,用户只需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。

总结

在选择分布式事务解决方案时,综合考虑业务场景的差异是至关重要的。本文主要是对业界常用分布式事务解决方案的探索和理解,并针对不同场景下如何合理选择给出了建议。

二阶段提交 (2PC):

  • 优点:
    • 简单可靠:适用于对一致性要求高、事务较为简单的场景。
  • 缺点:
    • 阻塞风险:在电商支付场景中,由于用户付款可能涉及多个账户,2PC可能导致长时间的阻塞,影响用户体验。
  • 案例:
    • 在电商系统中,当用户下单并支付时,使用2PC确保订单生成和支付操作的一致性。

三阶段提交 (3PC):

  • 优点:
    • 减少阻塞:适用于对性能要求相对高、对一致性要求仍然较高的场景。
  • 缺点:
    • 网络分区问题:在金融交易场景中,可能无法完全解决网络分区导致的一致性问题。
  • 案例:
    • 在金融结算系统中,使用3PC确保资金划转的一致性,降低阻塞时间。

TCC (Try, Confirm, Cancel):

  • 优点:
    • 灵活适应业务:适用于业务逻辑较为复杂、需要灵活性的场景。
  • 缺点:
    • 实现复杂:在电商系统中,如果涉及库存、优惠等复杂业务,TCC的实现可能相对复杂。
  • 案例:
    • 在电商促销活动中,使用TCC确保用户下单后的库存扣减、优惠券使用等操作的一致性。

可靠消息最终一致性方案:

  • 优点:
    • 高可用性:适用于对可用性要求较高、实时性要求相对较低的场景。
  • 缺点:
    • 系统复杂性:在金融领域中,引入消息队列可能增加系统的复杂性。
  • 案例:
    • 在银行交易系统中,使用可靠消息最终一致性确保跨银行转账等操作的一致性,提高系统的可用性。

业界选择建议:

  • 电商支付场景:
    • 在电商支付中,可能选择使用2PC,因为一致性要求高,而且对阻塞时间的容忍度相对较大。
  • 金融结算场景:
    • 在金融结算中,可能选择使用3PC,以减少阻塞时间,同时保持较高的一致性。
  • 电商促销场景:
    • 在电商促销活动中,可能选择使用TCC,以满足复杂的业务逻辑和灵活性的需求。
  • 银行交易场景:
    • 在银行交易系统中,可能选择使用可靠消息最终一致性,以保障高可用性和容错性。

结尾

感谢阅读到最后,文章内容主要来源于近期学习笔记,结合自身的理解做了整理。如果您觉得有帮助,点赞、转发加关注,一起努力不迷路!!!

相关推荐
初晴~1 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
有一个好名字1 小时前
zookeeper分布式锁模拟12306买票
分布式·zookeeper·云原生
yukai080085 小时前
【最后203篇系列】002 - 两个小坑(容器时间错误和kafka模块报错
分布式·kafka
老猿讲编程6 小时前
OMG DDS 规范漫谈:分布式数据交互的演进之路
分布式·dds
C++忠实粉丝6 小时前
服务端高并发分布式结构演进之路
分布式
洛神灬殇7 小时前
彻底认识和理解探索分布式网络编程中的SSL安全通信机制
网络·分布式·ssl
龙哥·三年风水8 小时前
workman服务端开发模式-应用开发-vue-element-admin封装websocket
分布式·websocket·vue
李洋-蛟龙腾飞公司11 小时前
HarmonyOS Next 应用元服务开发-分布式数据对象迁移数据文件资产迁移
分布式·华为·harmonyos
技术路上的苦行僧13 小时前
分布式专题(10)之ShardingSphere分库分表实战指南
分布式·shardingsphere·分库分表
GitCode官方14 小时前
GitCode 光引计划投稿 | GoIoT:开源分布式物联网开发平台
分布式·开源·gitcode