1、AT 模式的前提:
1、基于支持本地 ACID 事务的关系型数据库;
2、Java 应用,通过 JDBC 访问数据库;
2、整体机制是两阶段提交协议的演变:
-
一阶段:“业务数据”和“回滚日志”在一个本地事务中提交,释放本地锁(相当于数据库行锁)和连接资源;(本地事务,就已经在数据库持久化了)
-
二阶段:
-
如果没有异常:提交异步化,非常快速地完成;(正常情况,那就提交了,同步一下TC Server的状态,删除回滚日志)
-
如果有异常:回滚通过一阶段的回滚日志进行反向补偿;(通过undo_log表中rollback_info字段);
-
3、多线程模式数据一致性
3.1写隔离
- 一阶段本地事务提交前,需要确保先拿到
全局锁
(多线程时只有一个线程优先获得全局锁)。 - 拿不到
全局锁
,不能提交本地事务。 - 拿
全局锁
的尝试被限制在一定范围内(源码中默认尝试10次,如果10次还没拿到全局锁就放弃),超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
- 两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
- tx1 先开始,开启本地事务,拿到本地锁
- 更新操作 m = 1000 - 100 = 900。
- 本地事务提交前,先拿到该记录的
全局锁
,拿到后本地提交并释放本地锁(本地锁,相当于其数据库行锁,其他线程能对其他数据进行读写)。 - tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。
- 本地事务提交前,尝试拿该记录的
全局锁
,tx1 全局提交前
,该记录的全局锁一直会被 tx1 持有
,tx2 需要重试等待全局锁
。
- tx1 二阶段全局提交(global commit),释放
全局锁
。tx2 拿到全局锁
提交本地事务。 - 如果 tx1 的二阶段全局回滚,则 tx1需要重新获取该数据的本地锁,进行反向补偿(数据回滚)的更新操作,实现分支的回滚。
- 此时,如果 tx2 仍在等待该数据的
全局锁
,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁
等锁超时,放弃全局锁
并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
3.2、读隔离
-
在数据库本地事务隔离级别
读已提交(Read Committed) 或以上的基础上
,Seata(AT 模式)的默认全局隔离级别是读未提交(Read Uncommitted)
。- mysql 默认是可重复读(Repeatable Read:在Read Committed基础上重复读)
- oracle,sql server 读已提交(Read Committed)
-
如果应用在特定场景下,必需要求全局的
读已提交
,目前 Seata 的方式是通过 SELECTFOR UPDATE
语句的代理。
- SELECT FOR UPDATE 语句的执行会申请
全局锁
- for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。
- 如果
全局锁
被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。 - 这个过程中,查询是被
block
住的,直到全局锁
拿到,即读取的相关数据是已提交
的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
如果某个表操作做了分布式事物,那么在查询的时候需要做防止脏读的处理(SELECT FOR UPDATE
),因为查询的数据可能会被回滚,引起脏读