老大:来,你搞一搞分布式事务吧

我:......,啥是事务?

我:先从理论学起吧

我不懂什么是事务

如果事务都不懂,就更不用说分布式事务了,于是我马上开始学习了。

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。

事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。

换成比较容易理解的话就是,就是一组操作比如增删改查四个操作要么都成功,要么都失败,不存结果不一致的状态。

我不懂什么是分布式事务

终于弄明白什么是事务了,又来了分布式事务。为什么需要分布式事务呢?

事务更多指的是单机版、单数据库的概念。分布式事务 指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上 。

换成比较容易理解的话,就是多个事务之间再保持事务的特性,也就是多个事务之间保证结果的一致性。

XA规范

有了分布式事务的场景,就会有解决该问题的方式规范,XA规范就是解决分布式事务的规范,具体描述见维基百科解释:

XA规范提供了一种重要思想:

1、引入全局事务的控制节点,事务的协调者

2、多个本地事务划分多阶段提交(也就是下面讲的2PC,3PC)

我不懂分布式方案

有了规范就会有落地方案,下面介绍基于XA规范的几个实现协议。

首先介绍两阶段提交( Two-phase Commit )和三阶段提交( Three-phase Commit )

2PC( Two-phase Commit )

两阶段提交,顾名思义就是要分两步提交。

这里第一阶段称为准备或者投票阶段。引入一个负责协调各个本地资源管理器的事务管理器,

本地资源管理器一般是由数据库实现,事务管理器在第一阶段的时候询问各个资源管理器是否都就绪,并执行完除提交事务外所有事情,然后把结果返回给事务协调者。

如果收到每个资源的回复都是 成功,则在第二阶段提交事务,如果其中任意一个资源的回复是 失败, 则回滚事务。

这里的实现方式和我们平常开黑玩游戏时差不多,当我们组队时,队长会让大家准备,让队员上完厕所吃饱饭,如果所有队员都准备好,那就开始游戏,如果有任一一个队员没有吃饱,没有确认准备好,就不会开始游戏。

但是这种协议也会存在一些问题,如下:

同步阻塞,这是2PC最大的问题, 严格的2PC执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态

解决方案:引入引入超时机制,如果长时间没有收到响应,执行特定的动作。

协调者单点故障,协调者在2PC中是最重要的角色,同时也意味着如果他出问题,整个过程就GG了

解决方案:单点故障的常规方案就引入副本然后当主节点挂掉后,重新选主,就像组队游戏中,如果队员都准备好后,队长长时间蹲厕所不开始游戏,游戏程序一般就会踢掉队长,其他组员切换成队长身份。

数据不一致,虽然解决了上面几个问题,但是由于分布式系统存在很多网络抖动和调用失败场景还是会有数据不一致的情况,下面分为协调者、参与者、网络等故障来详细分析一下:

1、协调者发送准备命令前挂掉

这种相当于事务直接没有开始,没有啥太大影响

2、协调者发送准备命令后挂掉

这种情况,如果参与者没有超时机制,就会造成资源锁定

3、协调者发送提交命令前挂掉

这种情况和上一种情况类似,也会造成资源锁定

4、协调者发送提交命令后挂掉

这种情况很可能是能够成功执行分布式事务的,因为已经到了提交阶段说明其他参与者都已经准备好,如果失败就不断重试

5、协调者发送回滚命令前挂掉

这种情况和2、3是类似的,由于参与者收不到执行操作的命令,如果没有超时会一直阻塞并占据着资源

6、协调者发送回滚命令后挂掉

这种情况和4差不多,也是很大概率是能够成功执行回滚事务的,如果没有成功,由于已经形成了决议,所以只能不断重试

7、协调者发送准备命令后,部分参与者挂掉

这种情况协调者有超时机制,直接判定成失败,然后通知所有参与者回滚

8、协调者发送准备命令后挂掉,且部分参与者挂掉

这种情况重新选举协调者后,发现还在第一阶段,由于没有收到挂掉参与者的响应,所以判定失败,通知其他参与者执行回滚

9、协调者发送提交或回滚命令后挂掉,且收到消息的参与者挂掉

这种情况重新选举协调者后,没有收到消息的参与者没有执行事务,但是协调者无法确定收到消息的参与者执行第二阶段的提交或回滚到底是否成功,就会出现事务不一致的情况

3PC( Three-phase Commit )

从上面介绍的相关内容也可以大体知道2PC的缺点和解决方式,于是就有了下面的解决协议,三阶段提交

从百科可以看到3PC的引入主要就是为了解决上面我们说的2PC的缺点,咋就能解决呢?

1、3PC是非阻塞协议

好的,就是为了解决了资源占用问题,主要也就是引入了参与者超时机制

2、 第一阶段与第二阶段之间插入了一个准备阶段

解决了在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者回滚的“不确定状态”,也就是为了保证最后提交阶段之前所有参与节点状态一致

3PC 把2PC第一阶段再次拆分为2个阶段,多了一个阶段其实就是在执行事务之前来确认参与者是否正常,防止个别参与者不正常的情况下,其他参与者都执行了事务锁定资源。

他的大概步骤其实可以按照参与者4个状态来划分

0、初始状态,此阶段事务发起者触发全局事务,参与者切换本地状态为开始状态,并把自己注册到协调者中。

1、可提交或状态等待,此阶段协调者发送命令到每个注册过来的参与者,让他们更改状态为可提交状态。

2、预提交状态,此阶段协调者收到参与者确认可以提交并进入状态,然后协调者向他们发送预提交消息,参与者锁定资源,并更改状态为预提交状态。同时 协调者也进入预提交状态。

3、提交状态,此阶段协调者根据参与者预提交的结果执行提交或回滚操作,然后释放资源。

通过这种方式可以解决一些2PC状态不一致问题。JBoss上大佬的总结:

大概意思是,通过引入预提交阶段,协调者能够确定参与者提交前的状态,同时参与者也能够推断其他参与者状态

协调者正常的情况下,可以根据参与者状态切换的结果来决定是执行还是回滚。多出的一个预提交阶段就是为了统一状态。

参与者如果没有收到协调者消息,会默认执行提交,虽然可能会导致数据不一致。

协调者挂掉重新选举后,会根据参与者和原主节点状态确定是执行还是回滚。

新协调者来的时候发现自己是可提交状态并且参与者为可提交和回滚状态,说明经过投票回滚的,此时新协调者执行回滚命令

新协调者来的时候发现自己是预提交并且参与者处于预提交和提交状态,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令

可以看到3PC由于多引入了一个阶段,性能会比较低,而且其实也没有解决数据一致性问题,多了一个阶段的效果也不能保证效果一定要比2PC要好,所以一般还是很少用。

TCC(Try-Confirm-Cancel)

2PC/3PC 模式基于 支持本地 ACID 事务关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式从业务层面处理,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中,可以不依赖本地数据库,当然实现上可以依赖,更多的场景还是两者结合。

TCC更多的是让业务来实现两阶段提交的思想,对业务侵入性大

Try阶段定义为执行资源的锁定,这个阶段我认为比较难实现,常规的思路是

转账场景时可能需要把尝试账户余额是否足够,然后减去转账金额并把金额存入到临时字段,做到锁定金额

缓存场景可能就需要使用分布式锁,锁定住要操作的缓存值,或者取出某个缓存到另一个缓存

上传下载场景可能需要把文件存到服务器临时目录

Confirm阶段定义为执行try阶段锁定的资源,也就是说基于try的成功,可以继续操作,比如执行真正的转账、缓存操作、上传下载等。

Cancel阶段定义为释放Try预留的资源,也就是说由于Try的失败,需要作出相应的补偿操作或者恢复环境,比如删除掉转账时的临时字段、释放掉锁、清理临时文件等。

TCC模式实现难度还是蛮大的,需要考虑很多异常场景,还要考虑资源如何锁定和释放,但是由于不会阻塞资源,应用方面也更广,据说还是有很多公司热衷于这种补偿型的事务实现方式

还有就是这里所说的TCC更多是一种思想,实际实现可能还是需要根据具体业务来做相应的调整,方法是死的,人是活的。

SAGA

理论基础(点击查看原论文):Hector & Kenneth 发表论文Sagas (1987)

Saga模式提供的是长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

适用场景:

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

Saga主要思想是依赖于状态机转换,长事务拆分成多个短事务,依次执行短事务

如果某个短事务失败,则按照前面执行顺序的逆序执行补偿事务

这种模式还少使用的,实现也是比较复杂,同时流程很长,当遇到类似场景时还是需要仔细考虑是否有必要去实现分布式事务呢?

本地消息表

执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。

然后再去调用下一个服务,如果成功了,消息表的消息状态可以直接改成已成功。

如果调用失败,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。

一般也会有重试次数限制,超出后执行回滚或者通知人工介入。

可见本地消息表也会出现数据不一致的情况,尽量保证最终一致性。

消息队列

此方案的意思是通过支持事务的消息队列来实现分布式事务。

主要流程:

  1. 生产者发送半事务消息到MQ
  2. 生产者收到MQ成功接收到之后,去执行本地事务,但是事务还没有提交。
  3. 生产者会根据事务的执行结果来决定发送提交或者回滚到消息
  4. 生产者需要提供一个查询事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 MQ 会通过查询接口获得发送方事务执行结果。
  5. 如果是失败结果的消息,MQ直接丢弃,也就不会影响到消费者
  6. 如果是成功结果的消息,消费者消费半事务消息,然后再去消费普通消息

该方案与本地消息不同点是去掉了本地消息表,本地事务和MQ事务绑定在一起。目前市面上实现该方案的只有阿里的 RocketMq

最大努力通知

这种方式请进行最大努力自行学习吧

我不懂怎么实现

学了这么多方案,自己实现还是很有难度。

常见的解决方案的实现框架有: byteTCC 、华为 ServiceComb 实现的DTM(华为cloud官网可见)、阿里seata(收费版为GTS)、腾讯DTF

目前开源最火的还是seata,支持模式多、官网文档详细,这里就不一一介绍了

关于seata的文章非常多,下篇文章也打算以seata框架实践分布式事务。

那seata是不是就完美了呢?当然不是,以后可能改进的几点

1、不支持控制台,没有可视化界面,验证全靠打印和连接数据库

2、seata-server高可用不支持Raft协议,事务信息完全依赖于DB、redis等

3、undoLog占用空间过大尤其是前后置镜像一个大JSON字段,数据量大时可能会入库慢,可能需要进行压缩

4、只能通过异常回滚,不支持类似Spring的Rollback-Only标志位回滚

5、全局锁的粒度是不是有点大,分支事务是否有必要上报状态到TC

找到一份seata开源作者 jimin slievrly 的分享视频一起学习

如果需要视频中PPT学习,公众号内回复seata即可

我懂了

本文按照完全没接触过事务的学习流程进行书写,脑图如下:

左边是基础,右边是方案,如果你也在学习分布式事务相关知识,可以参考。

本文是系列第一篇,后面计划一篇为seata实战,一篇为seata原理和如何设计一个通用分布式事务框架。感谢阅读,欢迎关注。


你呀不牛
1 声望0 粉丝