Spring Cloud 分布式

你必须了解的分布式事务解决方案

2021-08-16  本文已影响0人  靓仔聊编程

前言

上一篇文章《就这?分布式 ID 发号器实战》之后,我朋友辉哥在后台留言让靓仔聊聊分布式事务,既然辉哥都开口了,那必须得满足啊,安排!

温馨提示:文章很干,请多喝水

img

什么是分布式事务

什么是事务想必大多数朋友应该都很清楚了,不清楚的可以看前面的文章《就这?一篇文章让你读懂 Spring 事务》。

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

简单来说,就是一个大的操作由 N 个小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。比如存在一个订单的微服务,一个库存的微服务,当订单完成需要同步减少库存,这时候就要在事务上确保完整和一致。

相关理论

关于事务的特性(ACID)隔离级别这里就不再重复介绍了,可以看前面的文章,这里着重介绍下两个新的知识:CAP 理论BASE 理论。

CAP 理论

CAP 是一个已经被证实的理论,在分布式系统中最多只能同时满足这三项中的两项,而分区容错性是分布式系统必须满足的,所以在分布式系统中常见的组合就是 CP 和 AP

BASE 理论

常见解决方案

1、两阶段提交

两阶段提交(Two-phaseCommit),简称为 2PC,两阶段提交是一种强一致性设计,它引入一个事务协调者的角色来协调管理各个参与者(也可称之为各本地资源)的提交和回滚。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。

img

2PC 存在的问题:

2PC 的优势在于对业务没有侵入,可以利用数据库自身机制来进行事务的提交和回滚。

常见的基于 2PC 的具体落地方案有:JTA(XA 规范) 和 Seata( AT 模式)。

2、三阶段提交

三阶段提交(Three-phase commit),简称为 3PC,是 2PC 的改进版本。同时在协调者和参与者都引入了超时机制,还在 2PC 中的准备阶段和提交阶段中间增加了一个预提交阶段。

img

相对于 2PC,3PC 主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行 commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了 commit 操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。而且 3PC 整体的交互过程更长,性能也会有所下降。

3PC 目前似乎只存在于理论,还没有具体落地方案。

3、TCC

2PC 和 3PC 都是依赖于数据库的事务提交和回滚,但是有时候很多业务并不仅仅只涉及到数据库,可能还会发送短息、消息等等,而 TCC 就是属于业务层面或者说是应用层面的分布式事务。

TCC 方案分为Try-Confirm-Cancel三个阶段,属于补偿性分布式事务。

img

有的朋友可能会问了,Try 成功了会执行 Confirm,失败了会执行 Cancel,那 Confirm 阶段失败了怎么办?这时候只能设置重试机制,不断重试调失败的 Confirm,直到成功为止,真有怎么也不成功的,就只能人工介入了。

TCC 需要根据每个场景和业务逻辑来设计相应的操作,所以很大程度增加了业务代码的复杂度,对业务有很大的侵入。

虽说对业务有侵入,但是 TCC 没有资源的阻塞,每一个方法都是直接提交事务的,如果出错是通过业务层面的 Cancel 来进行补偿,所以也称补偿性事务方法。

TCC 要注意的几个问题:

4、本地消息表

本地消息表分布式事务解决方案是国外的 eBay 提出的一套方案。其实就是利用了各系统本地的事务来实现分布式事务,在数据库中存放一张事务消息表,在执行业务操作的时候, 将业务的执行和将消息放入消息表中的操作放在同一个事务中。

本地事务执行成功之后再调用其他服务,如果成功了就将消息表里的消息状态改为成功,如果失败了,则由定时任务去读取本地事务表中未成功的消息,再去调用相应的服务,成功后再次修改状态。

img

这里也要设置重试机制,一旦有实在不成功的,还需人工介入。这里要注意的是,也要保证对应服务的方法幂等性。

可以看出,本地消息表实现比较简单,是一种最大努力通知思想,实现的是最终一致性,容忍了数据暂时不一致的情况。

缺点是严重依赖数据库。

5、可靠消息最终一致性方案

在上面的本地消息表方案中,生产者需要额外创建消息表,还需要对本地消息表进行轮询,业务负担较重。阿里开源的 RocketMQ 4.3 之后的版本正式支持事务消息,该事务消息本质上是把本地消息表放到 RocketMQ 上,解决生产端的消息发送与本地事务执行的原子性问题。

服务 A,先给 Broker (消息中间件) 发送一个 Half Message(半消息),其实这个半消息已发送到 Broker 端,但是此消息的状态被标记为"不能投递",消费者还看不到,处于这种状态下的消息称为半消息。

发送完 半消息后,服务A 执行业务操作(本地事务),再根据操作结果:如果成功,则向 Broker 发送 一个 Commit 命令,这时半消息就变成了可以被消费者消息;如果失败,则发送一个 RollBack 命令,该消息则会被删除。

如果是 Commit 那么服务 B 就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。

如果 RocketMQ 没有收到服务 A 确认状态的消息,那么半消息 RocketMQ 会自动定时轮询回调你的接口,询问这个处理的处理情况。借助这点,服务A实现一个回调,根据实际处理结果 Commit 或者 Rollback,加强一致性判断。

img

在服务 B 执行的过程中也可能会失败,这时也是需要重试,一直执行不成功也需要人工介入,同时也需要保证服务 B 方法的幂等性。

6、最大努力通知

最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。

就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了

事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。

最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。

总结

其实分布式事务解决方案还有很多,但是各自还是会存在很多问题,极端情况下也都需要人工去处理,而且大大提高了流程的复杂度,会带来很多额外的开销。

所以谨记,在真实的开发过程中,能不使用分布式事务就不要使用!

后面会给大家带来分布式事务的实战,没点关注的可以点个关注,防止走丢了。

END

往期推荐

就这?分布式 ID 发号器实战

略懂设计模式之工厂模式

就这?Spring 事务失效场景及解决方案

就这?一篇文章让你读懂 Spring 事务

SpringBoot+Redis 实现消息订阅发布

更多精彩推荐,请关注公众号【靓仔聊编程】

上一篇 下一篇

猜你喜欢

热点阅读