深入理解分布式事务以及分布式事务的解决方案

1.什么是分布式事务?
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

1.1 要了解分布式事务,我们必须先了解清楚本地事务
本地事务是指传统的单机数据库事务,必须具备ACID原则:
原子性(A
所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。

一致性(C)
事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。

隔离性(I)
所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。数据库保证隔离性包括四种不同的隔离级别:

​ Read Uncommitted(读取未提交内容)

​ Read Committed(读取提交内容)

​ Repeatable Read(可重读)

​ Serializable(可串行化)

什么是幻读,脏读,不可重复读呢?

事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读。

在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。

事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。

持久性(D)
所谓的持久性,就是说一旦事务提交了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。

因为在传统项目中,项目部署基本是单点式:即单个服务器和单个数据库。这种情况下,数据库本身的事务机制就能保证ACID的原则,这样的事务就是本地事务。

概括来讲,单个服务与单个数据库的架构中,产生的事务都是本地事务。

其中原子性和持久性就要靠undo Log和redo Log日志来实现。

1.2 undo Log 和 redo Log日志

1.2.1 undo log主要有两个作用:回滚和多版本控制(MVCC):

MySQL中进行指定数据修改的时候,就在undo Log文件中记录,如果因为某些原因导致事务失败或回滚了,可以用undo Log日志文件进行回滚,undo Log主要存储的是逻辑日志,比如我们要insert一条数据了,那undo Log会记录的一条对应的delete日志。我们要update一条记录时,它会记录一条对应相反的update记录。因为是回滚:所以数据跟操作修改相反就好,这样就能保证一个事务包含多个操作,这些操作要么全部执行,要么全都不执行-------原子性

因为undo log存储着修改之前的数据,相当于一个前版本,MVCC实现的是读写不阻塞,读的时候只要返回前一个版本的数据就行了。

1.2.2 redo Log(重做日志)
是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。
比如MySQL实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性

假如我们有一条sql语句:

update user set name=‘张三’ where id = ‘1367408705977298945’

MySQL执行这条SQL语句,肯定是先把id=1367408705977298945的这条记录查出来,然后将name字段给改掉。这看着没有什么问题,实际上MySQL的基本存储结构是页(记录都存在页里边),所以MySQL是先把这条记录所在的页找到,然后把该页加载到内存中,将对应记录进行修改。

假如在内存中把数据改了,还没来得及落磁盘,而此时的数据库挂了怎么办?显然这次更改就丢了。
如果每个请求都需要将数据立马落磁盘之后,那速度会很慢,MySQL可能也顶不住。所以MySQL是怎么做的呢?

MySQL引入了redo log,内存写完了,然后会写一份redo log,这份redo log记载着这次在某个页上做了什么修改。

*加粗样式在这里插入图片描述
*
其实写redo log的时候,也会有buffer,是先写buffer,再真正落到磁盘中的。至于从buffer什么时候落磁盘,会有配置供我们配置。
在这里插入图片描述

写redo log也是需要写磁盘的,但它的好处就是顺序IO(我们都知道顺序IO比随机IO快非常多)。

所以,redo log的存在为了:当我们修改的时候,写完内存了,但数据还没真正写到磁盘的时候。此时我们的数据库挂了,我们可以根据redo log来对数据进行恢复。因为redo log是顺序IO,所以写入的速度很快,并且redo log记载的是物理变化(页做了xxx修改),文件的体积很小,恢复速度很快。
还有就是redo log 存储的是物理数据的变更,如果我们内存的数据已经刷到了磁盘了,那redo log的数据就无效了。所以redo log不会存储着历史所有数据的变更,文件的内容会被覆盖。

2.分布式事务的产生的原因:

2.1 数据库分库分表
随着业务数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。所以我们对数据库进行了水平拆分,将原单库单表拆分成数据库分片,于是就产生了跨数据库事务问题。
在这里插入图片描述

2.2 应用SOA化
所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。
https://img-blog.csdn.net/20170320083209525?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbWluZV9zb25n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

2.3 以上的分库分表和多服务都会产生数据一致性的问题
在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。在分布式网络环境下,我们无法保障所有服务、数据库都百分百可用,一定会出现部分服务、数据库执行成功,另一部分执行失败的问题。当出现部分业务操作成功、部分业务操作失败时,业务数据就会出现不一致。

3.常见的分布式事务解决方案
3.1 2PC,中文叫二阶段提交。将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段,二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚。
这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)。
接下来看一下两阶段的具体流程。
准备阶段:协调者给每个参与者发送准备命令,等待所有资源的响应之后就进入第二阶段即提交阶段,这里需要注意的是提交阶段不一定是提交事务,也有可能是回滚事务。

如果在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。
在这里插入图片描述
以上的执行过程中如果第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败,如下图:
在这里插入图片描述
第二阶段回滚二种情况是
1.1 执行的是回滚事务操作,那么就要不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。

1.2 执行的是提交事务操作,那么也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功。如果还是解决不了就要人工的介入了。

3.2 3PC是在2PC的基础上增加了CanCommit阶段,是2PC的变种,是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效的解决了协调者单点故障的问题。但是,性能和数据一致性问题没有根本解决。
3PC 包含了三个阶段,分别是准备阶段(CanCommit)、预提交阶段(PreCommit )和提交阶段(DoCommit),就像在2PC的提交阶段增加了预提交阶段和提交阶段,我们从流程图看一下3PC对应2PC的不同之处执行:
在这里插入图片描述
第一阶段:
1、协调者向所有参与者发出包含事务内容的CanCommit请求,询问是否可以提交事务,并等待所有参与者回复。
2、参与者收到CanCommit请求后,如果认为可以执行事务操作,则反馈成功并进入预备状态,否则反馈失败。
第二阶段:
1.1 .如果所有参与者都收到请求并返回成功时
1.1.1 协调者向所有参与者发出PreCommit请求,进入准备阶段。
1.1.2 参与者收到PreCommit请求后,执行事务操作,将Undo和Redo信息记入事务日志中(但不提交事务)。
1.1.3 各参与者向协调者反馈成功或失败响应,并等待最终指令
1.2.有任何一个参与者返回失败时
1.2.1 协调者向所有参与者发出abort请求。
1.2.2 无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务
第三阶段:
1.1 如果所有参与者都收到请求并返回成功时
1.1.1 如果协调者处于工作状态,则向所有参与者发出do Commit请求。
1.1.2 参与者收到do Commit请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
1.1.3 各参与者向协调者反馈成功完成的消息。
1.1.4 协调者收到所有参与者反馈的成功消息后,即完成事务提交。
1.2 有任何一个参与者返回失败时
1.2.1 如果协调者处于工作状态,向所有参与者发出abort请求。
1.2.2 参与者使用阶段1中的Undo信息执行回滚操作,并释放整个事务期间占用的资源。
1.2.3 各参与者向协调者反馈完成的消息。
1.2.4 协调者收到所有参与者反馈的消息后,即完成事务中断。

总结:3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。所以 2PC 和 3PC 都不能保证数据的一致性,因此一般都需要有定时补偿机制。

3.3 TCC分布式事务
TCC的全称是(Try-Confirm-Cancel)

Try 主要是对业务系统做检测及资源预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了

TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。

而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。

在这里插入图片描述

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

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

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

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

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

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

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

3.5 Seata方案
Seata是由阿里中间件团队发起的开源项目 Fescar,后更名为Seata,它是一个是开源的分布式事务框架。
传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作
在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服
务场景下面临的分布式事务问题,它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。
Seata的设计思想如下:
Seata的设计目标其一是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进,并解决2PC方案面临的问题。
Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:
在这里插入图片描述

与 传统2PC 的模型类似,Seata定义了3个组件来协议分布式事务的处理过程:

在这里插入图片描述

Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运
行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
Transaction Manager ™: 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终
向TC发起全局提交或全局回滚的指令。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分
支(本地)事务的提交和回滚。
还拿新用户注册送积分举例Seata的分布式事务过程:

在这里插入图片描述

具体的执行流程如下:

用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
用户服务的 RM 向 TC 注册 分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局
事务的管辖。
用户服务执行分支事务,向用户表插入一条记录。
逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的RM 向 TC 注册分支事
务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
用户服务分支事务执行完毕。
TM 向 TC 发起针对 XID 的全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。


版权声明:本文为wuhuayangs原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。