首先什么是 Seata ,摘抄官网的一段话。
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
可以看到提供了很多模式,我们先来看看 AT 模式。
AT模式
AT 模式就是两阶段提交,前面我们提到了两阶段提交有同步阻塞的问题,效率太低了,那 Seata 是怎么解决的呢?
AT 的一阶段直接就把事务提交了,直接释放了本地锁,这么草率直接提交的嘛?当然不是,这里和本地消息表有点类似,就是利用本地事务,执行真正的事务操作中还会插入回滚日志,然后在一个事务中提交。
这回滚日志怎么来的?
通过框架代理 JDBC 的一些类,在执行 SQL 的时候解析 SQL 得到执行前的数据镜像,然后执行 SQL ,再得到执行后的数据镜像,然后把这些数据组装成回滚日志。
再伴随的这个本地事务的提交把回滚日志也插入到数据库的 UNDO_LOG 表中(所以数据库需要有一张UNDO_LOG 表)。
这波操作下来在一阶段就可以没有后顾之忧的提交事务了。
然后一阶段如果成功,那么二阶段可以异步的删除那些回滚日志,如果一阶段失败那么可以通过回滚日志来反向补偿恢复。
这时候有细心的同学想到了,万一中间有人改了这条数据怎么办?你这镜像就不对了啊?
所以说还有个全局锁的概念,在事务提交前需要拿到全局锁(可以理解为对这条数据的锁),然后才能顺利提交本地事务。
如果一直拿不到那就需要回滚本地事务了。
官网的示例很好,我就不自己编了,以下部分内容摘抄自 Seata 官网的示例:
此时有两个事务,分别是 tx1、和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁 。
可以看到 tx2 的修改被阻塞了,之后重试拿到全局锁之后就能提交然后释放本地锁。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。
然后 AT 模式默认全局是读未提交的隔离级别,如果应用在特定场景下,必需要求全局的读已提交 ,可以通过 SELECT FOR UPDATE 语句的代理。
当然前提是你本地事务隔离级别是读已提交及以上。
AT 模式小结
可以看到通过代理来无侵入的得到数据的前后镜像,组装成回滚日志伴随本地事务一起提交,解决了两阶段的同步阻塞问题并且利用全局锁来实现写隔离。
为了总体性能的考虑,默认是读未提交隔离级别,只代理了 SELECT FOR UPDATE 来进行读已提交的隔离。
这其实就是两阶段提交的变体实现。
TCC 模式
没什么花头,就是咱们上面分析的需要搞三个方法, 然后把自定义的分支事务纳入到全局事务的管理中。
我贴一张官网的图应该挺清晰了。
Saga 模式
这个 Saga 是 Seata 提供的长事务解决方案,适用于业务流程多且长的情况下,这种情况如果要实现一般的 TCC 啥的可能得嵌套多个事务了。
并且有些系统无法提供 TCC 这三种接口,比如老项目或者别人公司的,所以就搞了个 Saga 模式,这个 Saga 是在 1987 年 Hector & Kenneth 发表的论⽂中提出的。
那 Saga 如何做呢?来看下这个图。
假设有 N 个操作,直接从 T1 开始就是直接执行提交事务,然后再执行 T2。可以看到就是无锁的直接提交,到 T3 发现执行失败了,然后就进入 Compenstaing 阶段,开始一个一个倒回补偿了。
思想就是一开始蒙着头干,别怂,出了问题咱们再一个一个改回去呗。
可以看到这种情况是不保证事务的隔离性的,并且 Saga 也有 TCC 的一样的注意点,需要空补偿、防悬挂和幂等。
而且极端情况下会因为数据被改变了导致无法回滚的情况。比如第一步给我打了 2 万块钱,我给取出来花了,这时候你回滚,我账上余额已经 0 了。