分布式事务一致性

分布式事务问题

定义:分库架构下无法使用单机数据库的事务能力,需要在多个服务协同操作下保证数据一致性

解决方案:强一致性协议

一个协调者,多个参与者,协同进行分布式事务处理

2PC(两阶段提交)

流程

  1. 准备阶段:协调者向各个参与者发起询问,说要执行一个事务,各参与者可能回复YES、NO或超时。
  2. 提交阶段:如果所有参与者都回复的是 YES,则事务协调者向所有参与者发起事务提交操作,即Commit操作,所有参与者各自执行事务,然后发送ACK

特点

优点

原理简单,实现方便

缺点

  1. 同步阻塞:在阶段1,锁定资源之后,要等所有节点返回,然后才能一起进入阶段2,不能很好地应对高并发场景
  2. 单点问题:阶段1完成之后,如果在阶段2事务协调者宕机,则所有的参与者接收不到Commit或Rollback指令,将处于“悬而不决”状态
  3. 数据不一致:网络或者协调者出现问题,部分参与者收到commit请求进行提交,剩下的则没有提交

3PC(三阶段提交)

2PC的改进版,其将二阶段提交协议的“提交事务请求”过程一分为二,形成了由CanCommit、PreCommit和do Commit三个阶段组成的事务处理协议

流程

  1. CanCommit:协调者向各个参与者发起询问,说要执行一个事务,各参与者可能回复YES、NO或超时。CanCommit阶段参与者发现超时会回滚
  2. PreCommit:如果协调者收到的都是YES,则向参与者发送PreCommit请求,参与者执行预提交,并反馈给协调者ACK响应(commit或者abort)。如果有一个是NO,则向所有参与者提交abort请求。PreCommit阶段参与者发现超时会提交
  3. doCommit:协调者发送请求,让参与者进行提交或者回滚。如果协调者或网络出现故障,参与者收不到指令会继续进行提交

特点

优点

降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致

缺点

参与者收到PreCommit之后,如果发生网络故障,协调者发送的,可能出现一部分提交一部分回滚的情况,即数据不一致

2PC vs 3PC

Q:引入 PreCommit 的作用是什么?
A:

  1. 明确状态:避免歧义,所有参与者收到 pre-commit 后,进入一个明确的中间状态(已准备提交但尚未提交)。如果此时协调者宕机,参与者就知道协调者已经决定提交了,只是还没发出最终指令
  2. 非阻塞:所有阶段都有超时机制,避免了像 2PC 那样“等死”的情况,即使协调者崩溃,参与者也不会一直阻塞。
特性 2PC(Two-Phase Commit) 3PC(Three-Phase Commit)
阶段数 2(Prepare + Commit) 3(CanCommit + PreCommit + DoCommit)
是否阻塞 是(参与者会阻塞等待协调者) 否(超时后可自行决定)
容错性 低(协调者崩溃会导致阻塞) 较高(通过超时和中间状态降低阻塞)
数据一致性 强一致 强一致(但牺牲一定性能)
复杂度
是否存在协调者单点风险 是(仍需解决)

解决方案:最终一致性

一般的思路是通过消息中间件来实现“最终一致性”。系统A收到用户的转账请求,系统A先自己扣钱,也就是更新DB1;然后通过消息中间件给系统B发送一条加钱的消息,系统B收到此消息,对自己的账号进行加钱,也就是更新DB2。但是这里在系统A操作时,需要更新DB1并发送消息,这里面存在一个技术问题

  • 如果是先发消息后改DB,可能会发生消息发送成功DB修改失败,即DB1扣钱失败DB2加钱成功
  • 如果是先改DB后发消息,可能会发生DB修改成功但是消息发送失败,即DB1扣钱成功DB2加钱失败
    上述两种情况都是不符合预期的,有人说可以将发送mafka消息和更新DB1的操作放到一个事务里面,这样看似是ok的但实际上是有问题的
  • 发送消息失败无法判断是消息中间件没有收到消息,还是返回响应的时候失败
  • 把网络调用放在数据库事务里面,可能会因为网络的延时导致数据库长事务,严重的会阻塞整个数据库

基于业务架构设计实现

  1. 系统A增加一张消息表,系统A不再直接给消息中间件发送消息,而是把消息写入到这张消息表中。把DB1的扣钱操作(表1)和写入消息表(表2)这两个操作放在一个数据库事务里,保证两者的原子性
  2. 系统A准备一个后台程序,源源不断地把消息表中的消息传送给消息中间件。如果失败了,也不断尝试重传。保证消息至少传递一次
  3. 系统B消费消息队列中的消息,做DB2的数据操作
    • 消息丢失问题:如果处理到一半宕机则消息可能会丢失,所以需要消费完再ack,但这样可能会导致重复消费
    • 消息重复问题:增加判重表,记录处理过的消息,这样就能实现幂等。或者业务逻辑可以判重也可以

基于消息队列事务实现

RocketMQ有事务消息的概念。RocketMQ不是提供一个单一的“发送”接又,而是把消息的发送拆成了两个阶段,Prepare阶段(消息预发送)和Confirm阶段(确认发送)。

  1. 系统A调用Prepare接又,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,消息只是暂存在那。
  2. 系统A更新数据库,进行扣钱操作。 步骤3:系统A调用Comfirm接又,确认发送消息。此时消息中间件才会把消息给消费方进行消费
    这里有两种异常场景,
  3. 步骤1、2成功,3失败或者超时
  4. 步骤1成功,步骤2失败或者超时。步骤3不会执行
    这些问题可以通过RocketMQ的机制实现,RocketMQ会定期(默认是1min)扫描所有的预发送但还没有确认的消息,回调给发送方,询问这条消息是要发出去,还是取消。发送方可以自行根据业务状态回复

TCC

事务状态表+调用方重试+接收方幂等

对账

作者

jszero

发布于

2025-05-04

更新于

2025-05-04

许可协议

评论