理论部分来自
Seata
官网:http://seata.io/zh-cn/docs/dev/mode/at-mode.html
一、前提
- 基于支持本地
ACID
事务的关系型数据库。 Java
应用,通过JDBC
访问数据库。
二、整体机制
两阶段提交协议的演变:
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
三、写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1
和 tx2
,分别对 a
表的 m
字段进行更新操作,m
的初始值 1000
。
-
tx1
先开始,开启本地事务,拿到本地锁,更新操作m = 1000 - 100 = 900
。 -
本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
-
tx2
后开始,开启本地事务,拿到本地锁,更新操作m = 900 - 100 = 800
。 -
本地事务提交前,尝试拿该记录的 全局锁 ,
tx1
全局提交前,该记录的全局锁被tx1
持有,tx2
需要重试等待 全局锁
tx1
二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1
的二阶段全局回滚,则 tx1
需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2
仍在等待该数据的 全局锁,同时持有本地锁,则 tx1
的分支回滚会失败。分支的回滚会一直重试,直到 tx2
的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1
的分支回滚最终成功。
因为整个过程 全局锁 在 tx1
结束前一直是被 tx1
持有的,所以不会发生 脏写 的问题。
结论:
写隔离:如果业务表的更新操存在于分布式事务中和本地事务方法中,建议在本地事务方法上使用@GlobalTransaction
或@GlobalLock
注解防止出现数据脏写,其中:
- 属性值
lockRetryInterval
覆盖全局配置client.rm.lock.retryInterval
,校验或占用全局锁重试间隔- 属性值
lockRetryTimes
覆盖全局配置client.rm.lock.retryTimes
,校验或占用全局锁重试次数
四、读隔离
目前数据库事务的隔离级别一共有 4
种,由低到高分别为:
Read uncommitted
:读未提交Read committed
:读已提交Repeatable read
:可重复读Serializable
:序列化
在数据库本地事务隔离级别 读已提交(Read Committed
) 或以上的基础上,Seata
(AT
模式)的默认全局隔离级别是 读未提交(Read Uncommitted
) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE
语句的代理。
SELECT FOR UPDATE
语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE
语句的本地执行)并重试。这个过程中,查询是被 block
住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata
目前的方案并没有对所有 SELECT
语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句
。
结论:
读隔离:如果业务表的更新操存在于分布式事务中,此时本地事务方法中对业务表进行查询操作,建议在本地事务方法上使用@GlobalTransactional+select for update
或@GlobalLock+select for update
注解防止出现数据脏读
注意事项:
使用select for update
时需动态传入参数列表,不可使用拼接好的完整字符串查询语句,会导致获取lockKeys
为空,引起脏读
五、源码解析
在AT
模式下,会使用 Seata
内部数据源代理 DataSourceProxy
,我们从提交过程进行调用跟踪:
5.1 使用 @GlobalTransactional
如果执行方法中带有
@GlobalTransactional
本地事务提交前,先拿到该记录的 全局锁
- 步骤1️⃣:
TM
请求TC
注册事务分支
io.seata.rm.datasource.ConnectionProxy
#getConnection
io.seata.rm.datasource.ConnectionProxy
#doCommit
io.seata.rm.datasource.ConnectionProxy
#processGlobalTransactionCommit
io.seata.rm.datasource.ConnectionProxy
#register
io.seata.rm.AbstractResourceManager
#branchRegister
至此TM
请求TC
注册事务分支完成。
- 步骤2️⃣:
TC
注册分支获取全局锁
io.seata.server.coordinator.AbstractCore
#branchRegister
io.seata.server.transaction.at.ATCore
#branchSessionLock
5.2 使用 @GlobalLock
如果执行方法中带有
@GlobalLock
注解,在提交前会查询 全局锁 是否存在,针对更新或select for update等操作时会重试等待,当超出属性lockRetryInterval*lockRetryTimes
后抛出Global lock wait timeout
异常。
- 属性值
lockRetryInterval
覆盖全局配置client.rm.lock.retryInterval
,校验或占用全局锁重试间隔- 属性值
lockRetryTimes
覆盖全局配置client.rm.lock.retryTimes
,校验或占用全局锁重试次数`
io.seata.rm.datasource.ConnectionProxy
#processLocalCommitWithGlobalLocks
io.seata.rm.datasource.ConnectionProxy
#checkLock
5.3 @GlobalLock 源码解析
io.seata.spring.annotation.GlobalLock
/**
* declare the transaction only execute in single local RM
* but the transaction need to ensure records to update(or select for update) is not in global transaction middle
* stage
*
* use this annotation instead of GlobalTransaction in the situation mentioned above will help performance.
*
* @see io.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary(Object, String, Object) // the scanner for TM, GlobalLock, and TCC mode
* @see io.seata.spring.annotation.GlobalTransactionalInterceptor#handleGlobalLock(MethodInvocation, GlobalLock) // the interceptor of GlobalLock
* @see io.seata.spring.annotation.datasource.SeataAutoDataSourceProxyAdvice#invoke(MethodInvocation) // the interceptor of GlobalLockLogic and AT/XA mode
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface GlobalLock {
/**
* customized global lock retry interval(unit: ms)
* you may use this to override global config of "client.rm.lock.retryInterval"
* note: 0 or negative number will take no effect(which mean fall back to global config)
* @return lock retry interval
*/
int lockRetryInterval() default 0;
/**
* customized global lock retry interval(unit: ms)
* you may use this to override global config of "client.rm.lock.retryInterval"
* note: 0 or negative number will take no effect(which mean fall back to global config)
* @return lock retry interval
*/
@Deprecated
@AliasFor("lockRetryInterval")
int lockRetryInternal() default 0;
/**
* customized global lock retry times
* you may use this to override global config of "client.rm.lock.retryTimes"
* note: negative number will take no effect(which mean fall back to global config)
* @return lock retry times
*/
int lockRetryTimes() default -1;
}
源码注释大概含义:
- 对于某条数据,如果正在 全局事务 中进行更新(或者选择更新)操作,这时某个本地事务需要更新该数据,需要在本地事务方法上使用
@GlobalLock
注解,确保其不会对全局事务中正在操作的数据造成影响(防止出现脏写)。 - 声明事务仅在单个本地
RM
中执行 - 使用
@GlobalLock
注解而不是@GlobalTransaction
将有助于提高性能
- 属性值
lockRetryInterval
覆盖全局配置client.rm.lock.retryInterval
,校验或占用全局锁重试间隔- 属性值
lockRetryTimes
覆盖全局配置client.rm.lock.retryTimes
,校验或占用全局锁重试次数`
5.4 SelectForUpdateExecutor 源码解析
io.seata.rm.datasource.exec.ExecuteTemplate
#execute
io.seata.rm.datasource.exec.SelectForUpdateExecutor
#doExecute
六、下章预告
针对上述理论及源码内容,预计在下个章节进行示例搭建,演示脏读、脏写的产生和对应的修复示例。