本文来说下关于分布式事务的几个问题,分布式事务是分布式场景下一个老大难的问题。
文章目录
概述
不知道你是否遇到过这样的情况,去小卖铺买东西,付了钱,但是店主因为处理了一些其他事,居然忘记你付了钱,又叫你重新付。又或者在网上购物明明已经扣款,但是却告诉我没有发生交易。这一系列情况都是因为没有事务导致的。这说明了事务在生活中的一些重要性。有了事务,你去小卖铺买东西,那就是一手交钱一手交货。有了事务,你去网上购物,扣款即产生订单交易。
事务的具体定义
事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种要么什么都不做,要么做全套(All or Nothing)机制。
数据库本地事务
ACID特性
说到数据库事务就不得不说,数据库事务中的四大特性,ACID:
A:原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
就像你买东西要么交钱收货一起都执行,要么要是发不出货,就退钱。
C:一致性(Consistency)
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
I:隔离性(Isolation)
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
打个比方,你买东西这个事情,是不影响其他人的。
D:持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。
InnoDB实现原理
InnoDB是mysql的一个存储引擎,大部分人对mysql都比较熟悉,这里简单介绍一下数据库事务实现的一些基本原理,在本地事务中,服务和资源在事务的包裹下可以看做是一体的:
我们的本地事务由资源管理器进行管理:
而事务的ACID是通过InnoDB日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过redo log(重做日志)来实现,原子性和一致性通过Undo log来实现。UndoLog的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。和Undo Log相反,RedoLog记录的是新数据的备份。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。
什么是分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器「分别位于不同的分布式系统的不同节点之上」。
一个大的操作由N多的小的操作共同完成。而这些小的操作又分布在不同的服务上。针对于这些操作,「要么全部成功执行,要么全部不执行」。
分布式事务情况一
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,如下图:
分布式事务情况二
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况。
上面两种分布式事务表现形式以第一种居多。
为什么会有分布式事务
举个例子:
转账是最经典的分布式事务场景,假设用户 A 使用银行 app 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后增加用户 B 账户中的余额。
如果其中某个步骤失败,此时就有可能会出现 2 种「异常」情况:
- 用户 A 的账户扣款成功,用户 B 账户余额增加失败
- 用户 A 账户扣款失败,用户 B 账户余额增加成功。
对于银行系统来说,以上 2 种情况都是「不允许发生」,此时就需要事务来保证转账操作的成功。
在「单体应用」中,我们只需要贴上@Transactional注解就可以开启事务来保证整个操作的「原子性」。
但是看似以上简单的操作,在实际的应用架构中,不可能是单体的服务,我们会把这一系列操作交给「N个服务」去完成,也就是拆分成为「分布式微服务架构」。
比如下订单服务,扣库存服务等等,必须要「保证不同服务状态结果的一致性」,于是就出现了分布式事务。
分布式事务的基础
分布式事务是随着互联网高速发展应运而生的,这是一个必然的我们之前说过数据库的ACID四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论:
CAP定理
在一个分布式系统中,以下三点特性无法同时满足,「鱼与熊掌不可兼得」
- 一致性(C):在分布式系统中的所有数据备份,「在同一时刻是否拥有同样的值」。(等同于所有节点访问同一份最新的数据副本)
- 可用性(A):在集群中一部分节点「故障」后,集群整体「是否还能响应」客户端的读写请求。(对数据更新具备高可用性)
- 分区容错性(P):即使出现「单个组件无法可用,操作依然可以完成」。
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用「至多只能同时支持上面的两个属性」。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
BASE理论
在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢?
前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:
- 「Basically Available(基本可用)」
- 「Soft state(软状态)」
- 「Eventually consistent(最终一致性)」
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
分布式事务协议
两阶段提交(2PC)
熟悉mysql的同学对两阶段提交应该颇为熟悉,mysql的事务就是通过「日志系统」来完成两阶段提交的。
两阶段协议可以用于单机集中式系统,由事务管理器协调多个资源管理器;也可以用于分布式系统,「由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交」。
这个协议有「两个角色」,A节点是事务的协调者,B和C是事务的参与者。事务的提交分成两个阶段。
第一个阶段是「投票阶段」
- 协调者首先将命令「写入日志」
- 「发一个prepare命令」给B和C节点这两个参与者
- B和C收到消息后,根据自己的实际情况,「判断自己的实际情况是否可以提交」
- 将处理结果「记录到日志」系统
- 将结果「返回」给协调者
第二个阶段是「决定阶段」
当A节点收到B和C参与者所有的确认消息后
- 「判断」所有协调者「是否都可以提交」
- 参与者收到协调者发起的命令,「执行命令」
- 将执行命令及结果「写入日志」
- 「返回结果」给协调者
可能会存在哪些问题
「单点故障」:一旦事务管理器出现故障,整个系统不可用
「数据不一致」:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
「响应时间较长」:整个消息链路是串行的,要等待响应结果,不适合高并发的场景
「不确定性」:当事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。
三阶段提交(3PC)
三阶段提交又称3PC,相对于2PC来说增加了CanCommit阶段和超时机制。如果段时间内没有收到协调者的commit请求,那么就会自动进行commit,解决了2PC单点故障的问题。
但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的?
第一阶段:「CanCommit阶段」
这个阶段所做的事很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。
- 如果都返回yes,则进入第二阶段
- 有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求
第二阶段:「PreCommit阶段」
此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
第三阶段:「DoCommit阶段」
在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。
分布式事务解决方案
有了上面的理论基础后,这里介绍开始介绍几种常见的分布式事务的解决方案。
在说方案之前,首先你一定要明确你是否真的需要分布式事务?
上面说过出现分布式事务的两个原因,其中有个原因是因为微服务过多。我见过太多团队一个人维护几个微服务,太多团队过度设计,搞得所有人疲劳不堪,而微服务过多就会引出分布式事务,这个时候我不会建议你去采用下面任何一种方案,而是请把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。分布式事务可以不适用尽量不要使用。
如果你确定需要引入分布式事务可以看看下面几种常见的方案。
全局事务(DTP模型)
一套分布式事务标准,使用了两阶段提交来保证分布式事务的完整性。
全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色:
AP:Application 应用系统 它就是我们开发的业务系统,在我们开发的过程中,可以使用资源管理器提供的事务接口来实现分布式事务。
TM:Transaction Manager 事务管理器
- 分布式事务的实现由事务管理器来完成,它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。
- 事务管理器还管理着所有的资源管理器,通过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。
- DTP只是一套实现分布式事务的规范,并没有定义具体如何实现分布式事务,TM可以采用2PC、3PC、Paxos等协议实现分布式事务。
RM:Resource Manager 资源管理器
- 能够提供数据服务的对象都可以是资源管理器,比如:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。
- 资源管理器能够提供单数据库的事务能力,它们通过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。
- XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。
- DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。
问题
- 有没有基于DTP模型的分布式事务中间件?
- DTP模型有啥优缺点?
补偿事务(TCC)
TCC其实就是采用的补偿机制,其核心思想是:「针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作」。它分为三个阶段:
「Try,Confirm,Cancel」
Try阶段主要是对「业务系统做检测及资源预留」,其主要分为两个阶段
- Confirm 阶段主要是对「业务系统做确认提交」,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,「预留资源释放」。
比如下一个订单减一个库存:
执行流程:
Try阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,
- 如果Try阶段「执行成功」,执行Confirm阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量
- 如果Try阶段「执行失败」,执行Cancel阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量
TCC 事务机制相比于上面介绍的2PC,解决了其几个缺点
1.「解决了协调者单点」,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
2.「同步阻塞」:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
3.「数据一致性」,有了补偿机制之后,由业务活动管理器控制一致性
总之,TCC 就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的「增加」了业务代码的「复杂度」,因此,这种模式并不能很好地被复用。
本地消息表
执行流程:
消息生产方,需要额外建一个消息表,并「记录消息发送状态」。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要「处理」这个「消息」,并完成自己的业务逻辑。
- 如果是「业务上面的失败」,可以给生产方「发送一个业务补偿消息」,通知生产方进行回滚等操作。
- 此时如果本地事务处理成功,表明已经处理成功了
- 如果处理失败,那么就会重试执行。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
消息事务
消息事务的原理是将两个事务「通过消息中间件进行异步解耦」,和上述的本地消息表有点类似,但是通过消息中间件的机制去做的,其本质就是’将本地消息表封装到了消息中间件中’
执行流程:
1 发送prepare消息到消息中间件
2 发送成功后,执行本地事务
- 如果事务执行成功,则commit,消息中间件将消息下发至消费端
- 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
3 消费端接收到消息进行消费,如果消费失败,则不断重试
这种方案也是实现了「最终一致性」,对比本地消息表实现方案,不需要再建消息表,「不再依赖本地数据库事务」了,所以这种方案更适用于高并发的场景。目前市面上实现该方案的「只有阿里的 RocketMQ」。
最大努力通知
最大努力通知的方案实现比较简单,适用于一些最终一致性要求较低的业务。
执行流程:
- 系统 A 本地事务执行完之后,发送个消息到 MQ;
- 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。
Saga事务模型
Saga事务模型又叫做长时间运行的事务。其核心思想是「将长事务拆分为多个本地短事务」,由Saga事务协调器协调,如果正常结束那就正常完成,如果「某个步骤失败,则根据相反顺序一次调用补偿操作」。
Seata框架中一个分布式事务包含3种角色:
「Transaction Coordinator (TC)」:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。「Transaction Manager ™」:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。「Resource Manager (RM)」:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
seata框架「为每一个RM维护了一张UNDO_LOG表」,其中保存了每一次本地事务的回滚数据。
具体流程:
1.首先TM 向 TC 申请「开启一个全局事务」,全局事务「创建」成功并生成一个「全局唯一的 XID」。
2.XID 在微服务调用链路的上下文中传播。
3.RM 开始执行这个分支事务,RM首先解析这条SQL语句,「生成对应的UNDO_LOG记录」。下面是一条UNDO_LOG中的记录,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。
4.RM在同一个本地事务中「执行业务SQL和UNDO_LOG数据的插入」。在提交这个本地事务前,RM会向TC「申请关于这条记录的全局锁」。
如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。
6.RM在事务提交前,「申请到了相关记录的全局锁」,然后直接提交本地事务,并向TC「汇报本地事务执行成功」。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。
7.TC根据所有的分支事务执行结果,向RM「下发提交或回滚」命令。
RM如果「收到TC的提交命令」,首先「立即释放」相关记录的全局「锁」,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。
RM如果「收到TC的回滚命令」,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,
- 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
- 如果相同,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。
分布式事务的几个核心问题
常见面试题1:分布式事务产生的背景?
单体架构下,多个不同的业务逻辑使用的都是同一个数据源,单一事务管理器情况下,不存在事务问题。
而在分布式或者微服务架构中,每个服务都有自己的数据源,使用不同事务管理器,如果两个服务执行成功之后出现了异常,A 服务的事务会回滚,但是 B 服务的事务不会回滚,分布式事务就出现了。
画外音:单体架构偶尔也会存在多数据源事务管理,解决方案通常采用 jta+ atominc。
常见面试题2:分布式事务方案通常有哪些?
一般分为 6 种。
- 2PC:强一致性;
- 3PC:相对于 2PC 引入超时机制;
- TCC:业务层面的分布式事务,Try - Confirm - Cancel;
- 本地消息表:利用各系统本地事务实现分布式事务;
- 消息事务:以 RocketMQ 为代表,通过中间件实现;
- 最大努力通知:柔性事务思想。
常见面试题3:什么是 BASE 理论?
先回答3个概念:
- 基本可用(Basically Available);
- 软状态(Soft State);
- 最终一致性(Eventually Consistent)。
BA(Basically Available):鼓励通过架构设计,把可能影响全平台的严重问题,转化为只影响平台中的一部分数据或者功能的非严重问题。
软状态:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
最终一致性:指数据在多个副本之间能否保持一致的特性,也是分布式事务要解决的终极问题。
常见面试题4:最终一致性分为哪几种?
5种:
- 因果一致性(Causal consistency);
- 读己之所写(Read your writes);
- 会话一致性(Session consistency);
- 单调读一致性(Monotonic read consistency);
- 单调写一致性(Monotonic write consistency)。
常见面试题5:Seata和LCN有何区别?
Seata 是阿里设计用来专门解决分布式事务的框架,未来可能会成为主流。
Seata 和 LCN 的思想相近,只不过 LCN 中采用的是假关闭,两者的区别是出错时,LCN 会发生死锁,而 Seata 不会,但 Seata 会脏读。
TCC、Seata 等分布式事务方案如何落地实践?每种方案分别更适合什么业务?如何设计高可用、高并发的分布式事务架构?
本文小结
本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解。还是那句话,能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。上面对解决方案只是一些简单介绍,如果真正的想要落地,其实每种方案需要思考的地方都非常多,复杂度都比较大,所以最后再次提醒一定要判断好是否使用分布式事务。
分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择,但是我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作,「代码量上去了,业务复杂了,性能下跌了」。
所以,当我们真实开发的过程中,能不使用分布式事务就不使用。