大家好,我是老三,上次发文的时候还是上次发文的时候,这篇文章分享分布式事务,看完要是你们不懂,那一定是不明白。
事务大家应该都知道,事务将一组操作纳入到一个不可分割的执行单元,这个执行单元里的操作都成功时才能提交成功。
简单地说,事务提供一种要么不做,要么全做机制。
我们先简单了解一下事务的四大特性:

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会出现部分成功部分失败的情况。。
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
在单体架构时代,所有的业务只用一个数据库。

单体架构时代事务的是实现很简单,我们操作的是同一个数据库,利用数据库本身提供的事务机制支持就可以了。
例如我们比较熟悉的MySQL数据库:
insert、update、deltete操作,回滚的时候做相反的delete、update、insert操作来恢复数据。redolog被称作重做日志,是物理日志,事务提交的时候,必须先将事务的所有日志写入redo log持久化,到事务的提交操作才算完成。
详细了解建议阅读《MySQL技术内幕 InnoDB存储引擎》7.2节。
随着业务发展,单体架构顶不住了,慢慢进入分布式时代——SOA或者粒度更细的微服务。
当然伴随而来的就是分库分表。
垂直拆分大库,例如原始大库拆分成订单库、商品库、支付库。水平分库,来减轻单个数据库的压力。
不管是怎么分库的,最后的结果就是我们一个操作可能要横跨多个数据库。
数据库本身的事务机制只能保证它自己这个库的事务,但是没法保证到其它的库。我们要保证跨多个库的操作还具备事务的特性,就不得不上分布式事务了。
在前面 分布式必备理论基础:CAP和BASE 里,讲了分布式的理论基础——CAP和BASE,这里就不再多讲。
我们只需要知道,BASE理论是对CAP中AP的一个延申,在没法保证强一致性的前提下,尽可能达到最终的一致性。
我们的分布式事务通常也做不到本地事务那么强的一致性,一般都是对一致性(Consistency)适当做了一些放宽,只需要达到最终的一致性。
XA是一个分布式事务协议,由Tuxedo提出。
在这个协议里,有三个角色:
XA规范主要定义了 事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口。

XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。
两阶段提交的思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作还是中止操作。
两阶段提交的两个阶段:第一阶段:准备阶段,第二阶段:提交阶段

准备阶段 Prepares
协调者向所有参与者询问是否可以执行提交操作,所有参与者执行事务,将结果返回给协调者。

提交阶段 commit

两阶段提交优点:尽量保证了数据的强一致,但不是100%一致
两阶段提交同样有一些缺点:
刚性事务,事务执⾏过程中需要将所需资源全部锁定,会比较影响性能。
三阶段提交(3PC)是二阶段提交(2PC)的一种改进版本 ,为解决两阶段提交协议的单点故障和同步阻塞问题。上边提到两阶段提交,当协调者崩溃时,参与者不能做出最后的选择,就会一直保持阻塞锁定资源。
2PC 中只有协调者有超时机制,3PC 在协调者和参与者中都引入了超时机制,协调者出现故障后,参与者就不会一直阻塞。而且在第一阶段和第二阶段中又插入了一个预提交阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
三阶段提交的三个阶段:CanCommit,PreCommit,DoCommit三个阶段

准备阶段 CanCommit
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
预提交阶段 PreCommit
协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务
参与者执行完操作之后返回ACK响应,同时开始等待最终指令。
提交阶段 DoCommit
协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务
ACK响应,则提交事务ACK响应或者超时,则中断事务协调者收到所有参与者的ACK响应,完成事务。

可以看出,三阶段提交解决的只是两阶段提交中 单体故障和同步阻塞的问题,因为加入了超时机制,这里的超时的机制作用于 预提交阶段 和 提交阶段。如果等待 预提交请求 超时,参与者直接回到准备阶段之前。如果等到提交请求超时,那参与者就会提交事务了。
无论是2PC还是3PC都不能保证分布式系统中的数据100%一致
TCC Try-Confirm-Cancel 的简称,是两阶段提交的一个变种,针对每个操作,都需要有一个其对应的确认和取消操作,当操作成功时调用确认操作,当操作失败时调用取消操作,类似于二阶段提交,只不过是这里的提交和回滚是针对业务上的,所以基于TCC实现的分布式事务也可以看做是对业务的一种补偿机制。
TCC的三阶段:
在Try阶段,是对业务系统进行检查及资源预览,比如订单和库存操作,需要检查库存剩余数量是否够用,并进行预留,预留操作的话就是新建一个可用库存数量字段,Try阶段操作是对这个可用库存数量进行操作。
例如下单减库存的操作:

执行流程:
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
但是,使用TCC,原本一个方法,现在却需要三个方法来支持,可以看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以还需要考虑接口的幂等性。
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。
例如,可以在订单库新增一个消息表,将新增订单和新增消息放到一个事务里完成,然后通过轮询的方式去查询消息表,将消息推送到MQ,库存系统去消费MQ。

执行流程:
订单服务中的消息有可能由于业务问题会一直重复发送,所以为了避免这种情况可以记录一下发送次数,当达到次数限制之后报警,人工接入处理;库存服务需要保证幂等,避免同一条消息被多次消费造成数据不一致。
本地消息表这种方案实现了最终一致性,需要在业务系统里增加消息表,业务逻辑中多一次插入的DB操作,所以性能会有损耗,而且最终一致性的间隔主要有定时任务的间隔时间决定。
消息事务的原理是将两个事务通过消息中间件进行异步解耦。
订单服务执行自己的本地事务,并发送MQ消息,库存服务接收消息,执行自己的本地事务,乍一看,好像跟本地消息表的实现方案类似,只是省去 了对本地消息表的操作和轮询发送MQ的操作,但实际上两种方案的实现是不一样的。
消息事务一定要保证业务操作与消息发送的一致性,如果业务操作成功,这条消息也一定投递成功。

执行流程:
消息事务依赖于消息中间件的事务消息,例如我们熟悉的RocketMQ就支持事务消息(半消息),也就是只有收到发送方确定才会正常投递的消息。
这种方案也是实现了最终一致性,对比本地消息表实现方案,不需要再建消息表,对性能的损耗和业务的入侵更小。
最大努力通知相比实现会简单一些,适用于一些最终一致性要求较低的业务,比如支付通知,短信通知这种业务。
以支付通知为例,业务系统调用支付平台进行支付,支付平台进行支付,进行操作支付之后支付平台会去同步通知业务系统支付操作是否成功,如果不成功,会一直异步重试,但是会有一个最大通知次数,如果超过这个次数后还是通知失败,就不再通知,业务系统自行调用支付平台提供一个查询接口,供业务系统进行查询支付操作是否成功

执行流程:
Saga事务,核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
和本地事务undo log有点像,出问题了,逆向操作来挽救。
Sega简介:
Sega的执行顺序:
Saga两种恢复策略
举个例子,假设用户下订单,花50块钱购买了10瓶可乐,则有这么一些短事务和回滚操作:
T1=下订单 => T2=用户扣50块钱 => T3=用户加10瓶可乐= > T4=库存减10瓶可乐
C1=取消订单 => C2= 给用户加50块钱 => C3 =用户减10朵玫瑰 = > C4=库存加10朵玫瑰

看了这么些事务的方案,介绍了相关的原理,但是这些原理怎么落地呢?各种各样的坑怎么处理呢?
—— 人生苦短,我用开源。
阿里巴巴开源了一套开源分布式事务解决方案——Seata。Seata可能并不称之为完美,但对代码入侵性非常小,基本环境搭建完成的话,使用的时候在只需要方法上添加一个注解@GlobalTransactional就可以开启全局事务。
Seata 也是从两段提交演变而来的一种分布式事务解决方案,提供了 AT、TCC、SAGA 和 XA 等事务模式,我们来看一下AT模式。
Seata 中主要有这么几种角色:
TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
我们看一下Seata大概的一个工作流程:

执行流程:
关于Seata的使用,和更详细的原理,这里挖个坑,以后有时间再细讲。
上边简单介绍了 2PC、3PC、TCC、本地消息表、最大努力通知、MQ、Sega、Seata 这8种分布式事务解决方案,但不管我们选哪一种方案,我们可以看到真要落地要考虑的点都很多,一个不慎,可能踩坑。
即使是看起来很省心的Seata,我之前的项目花了不少w买了它的商业化版本GTS,但是支持方仍然列出了一些“禁忌”,像长事务、大量数据、热点数据、异步调用等等,都可能会出现问题。
所以在项目中应用分布式事务要谨慎再谨慎,除非真的有一致性要求比较强的场景,能不用就尽量不用。

如果觉得文章有帮助, 点赞、关注、收藏 素质三连,抱拳!
参考:
[1]. 再有人问你分布式事务,把这篇扔给他
[2]. 让我们聊一聊分布式事务
[3]. 分布式事务,这一篇就够了
[4].从分布式事务解决到Seata使用,一梭子给你整明白了
[5]. 看了 5种分布式事务方案,我司最终选择了 Seata,真香!
[6]. 后端程序员必备:分布式事务基础篇