0%

分布式事务相关概念

前言

事务想必大家并不陌生,至于什么是 ACID,也是老生常谈了。不过暖男为了保证文章的完整性确保所有人都听得懂,我还是得先说说 ACID,然后再来介绍下什么是分布式事务和常见的分布式事务包括 2PC、3PC、TCC、本地消息表、消息事务、最大努力通知。

事务的基本概念

什么是事务?

什么是事务?举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任一个步骤失败,事务将撤销所有已成功的步骤。

明白上述例子,再来看事务的定义:事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

ACID 理论

严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。

ACID理论:

  • 原子性(A):所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
  • 一致性(C):事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
  • 隔离性(I):所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。
  • 持久性(D):所谓的持久性,就是说一旦事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。

总结:通俗意义上事务就是为了使得一些更新操作要么都成功,要么都失败。

Redis事务吐槽

看到 ACID 理论,可能有人会说,不对啊 Redis 的事务不能保证所有操作要么都执行要么都不执行,为什么它也叫事务啊?

我们来看看 Redis 怎么说的:
步骤1:redis 官网解释了为什么不支持回滚,他们说首先如果命令出错那都是语法使用错误,是你们自己编程出错,而且这种情况应该在开发的时候就被检测出来,不应在生产环境中出现。
redis 官网解释事务

步骤2:然后 Redis 就是为了快!不需要提供回滚。
下面还有一段话我就不截图了,就是说就算提供回滚也没用,你这代码都写错了,回滚并不能使你免于编程错误。而且一般这种错也不可能进入到生产环境,所以选择更加简单、快速的方法,我们不支持回滚。

步骤3:你看看这说的好像很有道理,我们不提供回滚,因为我们不需要为你的编程错误买单!但好像哪里不对劲?角度、立场不同,大家自己品。

分布式事务基本概念

分布式事务产生的背景

分布式事物在什么时候会产生?

  • 传统项目(单体应用,多数据源情况下)
  • 分布式项目(soa,微服务,也就是跨服务完成事务情况下)

分布式事务产生的场景

单体项目多数据源

单体项目多数据源

单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信 息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨数据库实例产生分布式事务。

微服务架构

微服务架构

典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的 同时订单微服务请求库存微服务减库存。简言之:跨JVM进程产生分布式事务。

分布式理论(CAP理论&Base理论)

这里不讲解了,自己去了解即可。

2PC(两阶段提交)

2PC简介

2PC(Two-phase commit protocol),中文叫两阶段提交协议。两阶段提交协议是一种强一致性设计。
2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,2PC将事务分成两阶段,两阶段分别是指:准备阶段(Prepare phase)、提交阶段(commit phase)。

2PC处理流程

2PC处理流程

  1. 准备阶段(Prepare phase):事务协调者给每个事务参与者发送Prepare消息,每个参与者在本地执行事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向协调者反馈事务能否处理成功。
  2. 提交阶段(commit phase):事务协调者根据反馈消息,选择回滚事物或提交事物。如果事务协调者接受到事务参与者执行失败或超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Conmmit)消息

2PC存在的问题

  • 同步阻塞:所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
  • 单点问题:协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。列如:在第一阶段已经完成,在阶段二协调者发生故障,所有事务参与者会一直处于阻塞状态,无法完成其它操作。
  • 数据不一致:在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  • 太过保守:任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

2PC实现方案

2PC实现方案有:

  • XA方案:传统数据库实现
  • Seata方案:阿里开源分布式事务框架

3PC(三阶段提交)

简介

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

3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit。

3PC处理流程

3PC处理流程

  • 准备阶段(CanCommit):事务协调者询问事务参与者是否可以完成事务,在此阶段只是询问不做真正操作,防止某些事务参与者不可用情况下导致所有事务参与者都阻塞(不执行事务操作)。
  • 预提交阶段(PreCommit):在准备阶段所有的事务参与者都返回可以执行操作时,事务协调者要求每个事务参与者预提交此操作,并反馈是否可以提交。(执行事务操作,但不提交,只是反馈是否可以成功提交)
  • 提交阶段(DoCommit):事务协调者根据反馈消息,选择回滚事物或提交事物。如果每个事务参与者在预提交阶段返回准备成功,那么进行提交事。否则,进行回滚事物,释放资源。

超时机制:如果在预提交阶段超时,则中断事务;如果提交阶段超时,则事务参与者提交事物。

3PC的优缺点

优点:

  • 降低了阻塞范围,在等待超时后协调者或参与者会中断事务
  • 避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。

缺点:
数据不一致问题依然存在,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

3PC与2PC的区别

增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大。

三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞导致永远锁定资源。

TCC(补偿机制)

简介

TCC是Try、Confirm、Cancel三个词语的缩写。
TCC 要求每个分支事务实现三个操作:预处理(Try),确认(Confirm),撤销(Cancel)。
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务。

TCC处理流程

TCC处理流程图

分为三个阶段:

  • Try阶段:是做业务检查以及资源预留(锁定)。此阶段仅是一个初步操作,需要和后续的Confirm阶段一起才能真正构成一个完整的业务逻辑。
  • Confirm阶段:是做确认提交。Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则 认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。Confirm阶段需要保证幂等性。
  • Cancel阶段:是做资源释放。Try阶段执行错误,取消执行Confirm阶段,调用Cancel阶段把Try阶段的操作撤销(回滚)了。通常情况下,Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。Cancel阶段需要保证幂等性。
  • TM事务管理器
    • TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公 用组件,是为了考虑系统结构和软件复用。
    • TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求 多少次,其结果都相同。

TCC和2PC/3PC很像,不过TCC的事务控制都是业务代码层面的,而2PC/3PC则是资源层面的。

TCC又可以被称为两阶段补偿事务,第一阶段try只是预留资源,第二阶段要明确的告诉服务提供者,这个资源你到底要不要,对应第二阶段的confirm/cancel,用来清除第一阶段的影响,所以叫补偿型事务。

举例模拟TCC流程

前提:假设库存数量本来是50,那么可销售库存也是50。账户余额为50,可用余额也为50。

用户下单,买了1个单价为1元的商品。流程如下:

  • Try阶段:
    • 订单服务:修改订单的状态为支付中
    • 账户服务:账户余额不变,可用余额减1,然后将1这个数字冻结在一个单独的字段里
    • 库存服务:库存数量不变,可销售库存减1,然后将1这个数字冻结在一个单独的字段里
  • Confirm阶段:
    • 订单服务:修改订单的状态为支付完成
    • 账户服务:账户余额变为(当前值减冻结字段的值),可用余额不变(Try阶段减过了),冻结字段清0。
    • 库存服务:库存变为(当前值减冻结字段的值),可销售库存不变(Try阶段减过了),冻结字段清0。
  • cancel阶段
    • 订单服务:修改订单的状态为未支付
    • 账户服务:账户余额不变,可用余额变为(当前值加冻结字段的值),冻结字段清0。
    • 库存服务:库存不变,可销售库存变为(当前值加冻结字段的值),冻结字段清0。

TCC优缺点

优点

因为Try阶段检查并预留了资源,所以confirm阶段一般都可以执行成功。
资源锁定都是在业务代码中完成,不会block住DB,可以做到对db性能无影响。
TCC的实时性较高,所有的DB写操作都集中在confirm中,写操作的结果实时返回(失败时因为定时程序执行时间的关系,略有延迟)。

缺点

从流程分析中可以看到,因为事务状态管理,将产生多次DB操作,这将损耗一定的性能,并使得整个TCC事务时间拉长。

事务涉及方越多,Try、Confirm、Cancel中的代码就越复杂,可复用性就越底(这一点主要是相对最终一致性方案而言的)。另外涉及方越多,这几个阶段的处理时间越长,失败的可能性也越高。

TCC实现方案

TCC实现方案有:

  • Seata
  • tcc-transaction
  • ByteTCC
  • Himly

本地消息表(异步确保)

参考博客:
https://www.cnblogs.com/zhangliwei/p/9984129.html
https://blog.csdn.net/weixin_40533111/article/details/85069536

本地消息表这个方案最初是 eBay 提出的。
eBay 的完整方案:https://queue.acm.org/detail.cfm?id=1394128。

本地消息表处理流程

本地消息表

消息生产方:
需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说它们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。(会有定时任务)

消息消费方:
需要处理这个消息,并完成自己的业务逻辑。
如果本地事务处理成功,则发送消息到消息生产方删除对应消息数据,表明已经处理成功了。
如果处理失败(运行期异常,非业务失败),那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

本地消息表优缺点

优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

消息事务

RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。

第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。

再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。

并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。

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

如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。

可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。

消息事务

最大努力通知型

其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。

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

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

所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。

适用于对时间不敏感的业务,例如短信通知。

总结

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

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

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