分布式事务

https://zhuanlan.zhihu.com/p/183753774

https://blog.csdn.net/eluanshi12/article/details/84528393  TCC分布式事务的实现原理(补偿机制)

https://destiny1020.blog.csdn.net/article/details/92432059  分布式事务-TCC

1. 2pc

 

 

二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)提交两个阶段。

缺点:

1.2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。

  单点故障问题: 事务协调者挂了。 解决方案集群。通过选举。

  总体而言效率低: 同步阻塞的

  极端条件下存在数据不一致的风险:    协调者给某一个参与者发送了 提交/或者回滚命令,之后协调者和当前参与者就都挂了。 新的协调者通过选举产生了。 刚刚的参与者也好了。这时,协调者并不知道这个

参与者的命令有没有执行成功。协调者不知道自己是应该执行提交命令还是回滚命令。这时就容易参数数据不一致问题。

    1:每个参与者自身的状态只有自己和协调者知道. 新的协调者选举成功后。他得不到参与者自身的状态信息(他不知道以前的协调者有没有发参与者发送过 提交或者回滚的命令)  。 

      解决方案: 协调者可以在发送提

交或者回滚命令的时候,记录在日志表中。新的协调者来了。可以通过日志知道以前的协调者有没有给参与者发送过命令。

    2. 致命问题。  就算1的问题解决了。 协调者 不会知道参与者的命令是否执行成功了。 所以 在和极端的情况下。会出现数据不一致问题。

2,2PC 适用于数据库层面的分布式事务场景,而我们业务需求有时候不仅仅关乎数据库,也有可能是上传一张图片或者发送一条短信(同步非阻塞的设计更适于发短信场景)

3. 首先 2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。

在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!

3PC

3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态

3PC 包含了三个阶段,分别是准备阶段预提交阶段提交阶段

看起来是把 2PC 的提交阶段变成了预提交阶段和提交阶段,但是 3PC 的准备阶段协调者只是询问参与者的自身状况,比如你现在还好吗?负载重不重?这类的。

而预提交阶段就是和 2PC 的准备阶段一样,除了事务的提交该做的都做了。

提交阶段和 2PC 的一样,让我们来看一下图。

1. 首先准备阶段的变更成不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。

2.而预提交阶段的引入起到了一个统一状态的作用,它像一道栅栏,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。

3.参与者引入了超时机制,参与者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了(那如果应该回滚的场景出现了。这里数据就会不一致),因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。

缺点:

1.但是多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。

2.然而超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。

3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。(协调者不知道该如果做(是提交呢?还是回滚呢?))

1,3PC 就是通过引入预提交阶段来使得参与者之间的状态得到统一。新协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了(都经历过了准备阶段),所以此时执行的就是提交命令(这也只能让协调者知道该如果做,但不能保证这样做一定对的)

  1.1 大部分情况下直接执行提交命令是没有问题的。如果参与者没有执行事务成功。3pc 直接执行了提交事务就是错的。这里就应该回滚事务。

让我们总结一下, 3PC 相对于 2PC 做了一定的改进:

1.引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。

2. 3PC 的准备阶段是没有事务的。 所以准备阶段出问题了,不会造成阻塞。

 

3.TCC

1. 核心思想 是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。分为三个阶段:

    • Try 阶段:主要是对业务系统做检测(一致性)及资源预留(准隔离性)
    • Confirm 阶段:主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。(Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试,一直不成功就人工介入。
      )
    • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。(Cancel 操作满足幂等性)

2. 如果你要实现一个TCC分布式事务,首先你的业务的主流程以及各个接口提供的业务含义,不是说直接完成那个业务操作,而是完成一个Try的操作。

这个操作,一般都是锁定某个资源,设置一个预备的状态,冻结部分数据,等等,大概都是这类操作。

3. try 其实就是一种试探,看看个个服务处理数据是否ok,  try 要失败了,就走回滚操作(业务回滚)。 try 如果成功了。 默认 confirm 就一定会成功。 如果在执行confirm 时有个服务失败了。就会一直重试。保证其他服务都正常的执行confirm。 如果当前服务就一直不能confrm 成功。就通过记录的日志进行补偿操作(定时任务补偿/人工补偿)。

4. TCC 3步都要记录日志,增加 事务状态控制表(可以解决下面三种情况)。这样我们在处理 try  confirm cancael 的时候我们可以通过日志数据的唯一性加状态来来处理一些 特殊的场景 或进行数据的补偿。

   4.0  事务状态控制表 字段 1.主事务ID 2.分支事务ID 3.分支事务状态 (INIT(I) - 初始化CONFIRMED© - 已提交ROLLBACKED® - 已回滚)

   4.1  幂等处理  因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口。

      解决方案
记录每个分支事务的执行状态。在执行前状态,如果已执行,那就不再执行;否则,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记录每个分支事务的执行状态。

  4.2  空回滚  当没有调用参与方Try方法的情况下,就调用了三阶段的Cancel方法。 

    解决方案:
需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

  4.3  资源悬挂   当没有调用参与方Try方法的情况下,就调用了三阶段的Cancel方法,后面try 恢复了。有执行了try 进行了资源预留,所以造成了资源悬挂。

      解决方案
二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

5. TCC 框架  ByteTCC、himly、tcc-transaction ,seata

 

4. 最大努力通知型事务

  “最大努力通知型事务”是为解决跨网络(不同公司间的调用),跨服务的柔性事务的另一种解决方案,它是基于方法回调。(也是一思想,这种思想在其他分布式解决中也会用到 比如本地消息表)适用于对时间不敏感的业务,例如短信通知。。

1.业务活动的调用方(通知发起方),在完成本地业务活动处理后,调用业务被调用方。这个过程允许服务调用失败,
2.如果失败了,服务调用方能够通过重试尽力实现双方的数据一致性。此处的的重试就体现出了–最大努力 的特点。
3.然后是业务的被调用方。业务被调用方会暴露一个业务结果接收接口(或者叫回调接口也可以)给业务调用方,对收到的通知消息进行必要的校验,校验通过后执行本地业务,从而使业务达到闭环
4.同时,由于调用方对被调用方的通知次数是有限的,即我们不可能无限制的通知,因此业务活动调用方需要提供一个业务查询补偿接口供被调用方使用,被调用方会根据定时策略,向业务活动的调用方发起查询操作,从而对丢失的业务消息达到补偿的目的。
5.整个过程中,业务被调用方暴露给调用方的通知接收接口 以及 业务调用方提供给被调用方进行查询操作的接口均需要实现幂等,这样才能保证数据完整性不会被破坏,从而实现最终一致性。

  前提条件:

1. 调用方和被调用方 最好都留有调用日志记录表。调用方通过调用日志记录表来实现循环通知被调用方

2.调用方需要提供一个业务查询补偿接口供被调用方使用。
3.业务被调用方会暴露一个业务结果接收接口(或者叫回调接口也可以)给业务调用方,对收到的通知消息进行必要的校验,校验通过后执行本地业务,从而使业务达到闭环.

4. 回调接口都要幂等

 

5 本地消息表(适用于自己公司微服务之间的调用)

本地消息表其实就是利用了 各系统本地的事务来实现分布式事务

本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。

然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。

如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。

这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。

可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。

 6. 基于RocketMQ的事务消息方案

 

 

 

 

 

 

 

总结

可以看出 2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。

而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。

本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。

posted @   好记性不如烂笔头=>  阅读(108)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示