分布式事务一致性
分布式事务问题
定义:分库架构下无法使用单机数据库的事务能力,需要在多个服务协同操作下保证数据一致性
解决方案:强一致性协议
一个协调者,多个参与者,协同进行分布式事务处理
2PC(两阶段提交)
流程
- 准备阶段:协调者向各个参与者发起询问,说要执行一个事务,各参与者可能回复YES、NO或超时。
- 提交阶段:如果所有参与者都回复的是 YES,则事务协调者向所有参与者发起事务提交操作,即Commit操作,所有参与者各自执行事务,然后发送ACK
特点
优点
原理简单,实现方便
缺点
- 同步阻塞:在阶段1,锁定资源之后,要等所有节点返回,然后才能一起进入阶段2,不能很好地应对高并发场景
- 单点问题:阶段1完成之后,如果在阶段2事务协调者宕机,则所有的参与者接收不到Commit或Rollback指令,将处于“悬而不决”状态
- 数据不一致:网络或者协调者出现问题,部分参与者收到commit请求进行提交,剩下的则没有提交
3PC(三阶段提交)
2PC的改进版,其将二阶段提交协议的“提交事务请求”过程一分为二,形成了由CanCommit、PreCommit和do Commit三个阶段组成的事务处理协议
流程
- CanCommit:协调者向各个参与者发起询问,说要执行一个事务,各参与者可能回复YES、NO或超时。CanCommit阶段参与者发现超时会回滚
- PreCommit:如果协调者收到的都是YES,则向参与者发送PreCommit请求,参与者执行预提交,并反馈给协调者ACK响应(commit或者abort)。如果有一个是NO,则向所有参与者提交abort请求。PreCommit阶段参与者发现超时会提交
- doCommit:协调者发送请求,让参与者进行提交或者回滚。如果协调者或网络出现故障,参与者收不到指令会继续进行提交
特点
优点
降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致
缺点
参与者收到PreCommit之后,如果发生网络故障,协调者发送的,可能出现一部分提交一部分回滚的情况,即数据不一致
2PC vs 3PC
Q:引入 PreCommit 的作用是什么?
A:
- 明确状态:避免歧义,所有参与者收到 pre-commit 后,进入一个明确的中间状态(已准备提交但尚未提交)。如果此时协调者宕机,参与者就知道协调者已经决定提交了,只是还没发出最终指令
- 非阻塞:所有阶段都有超时机制,避免了像 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的但实际上是有问题的 - 发送消息失败无法判断是消息中间件没有收到消息,还是返回响应的时候失败
- 把网络调用放在数据库事务里面,可能会因为网络的延时导致数据库长事务,严重的会阻塞整个数据库
基于业务架构设计实现
- 系统A增加一张消息表,系统A不再直接给消息中间件发送消息,而是把消息写入到这张消息表中。把DB1的扣钱操作(表1)和写入消息表(表2)这两个操作放在一个数据库事务里,保证两者的原子性
- 系统A准备一个后台程序,源源不断地把消息表中的消息传送给消息中间件。如果失败了,也不断尝试重传。保证消息至少传递一次
- 系统B消费消息队列中的消息,做DB2的数据操作
- 消息丢失问题:如果处理到一半宕机则消息可能会丢失,所以需要消费完再ack,但这样可能会导致重复消费
- 消息重复问题:增加判重表,记录处理过的消息,这样就能实现幂等。或者业务逻辑可以判重也可以
基于消息队列事务实现
RocketMQ有事务消息的概念。RocketMQ不是提供一个单一的“发送”接又,而是把消息的发送拆成了两个阶段,Prepare阶段(消息预发送)和Confirm阶段(确认发送)。
- 系统A调用Prepare接又,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,消息只是暂存在那。
- 系统A更新数据库,进行扣钱操作。 步骤3:系统A调用Comfirm接又,确认发送消息。此时消息中间件才会把消息给消费方进行消费
这里有两种异常场景, - 步骤1、2成功,3失败或者超时
- 步骤1成功,步骤2失败或者超时。步骤3不会执行
这些问题可以通过RocketMQ的机制实现,RocketMQ会定期(默认是1min)扫描所有的预发送但还没有确认的消息,回调给发送方,询问这条消息是要发出去,还是取消。发送方可以自行根据业务状态回复