一、脏写和脏读
数据隔离性可从读和写两个维度来审视:
-
从写的视角来看,是不能有
脏写
的,即不能让一个事务修改另外一个事务修改过但尚未提交的数据。- 对于成熟的数据库产品来说,脏写这种情况是不允许发生的。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁来实现的。
-
从读的视角来看,通常数据库产品默认隔离级别为
读已提交(Read committed)
,比如Oracle
、SQLServer
;虽然 Mysql 的默认隔离级别为可重复读(Repeatable read)
,但通常来说隔离级别越高,性能损耗越大,而且读已提交能够满足业务绝大部分场景,所以有些公司的 MySQL 也采用了读已提交
的隔离级别。读已提交
的隔离级别解决的是脏读
的问题,脏读
就是一个事务读取到了另外一个事务修改后尚未提交的数据脏读
会有什么问题呢?比如:A 事务读取 B 事务尚未提交的数据,此时如果 B 事务发生错误并执行回滚操作,那么很容易理解 A 事务获取、使用被回滚的数据肯定是会有问题的。
避免脏写和脏读是对数据库相关产品基本且必要的要求,所以 Seata 提供的分布式事务管理能力也要具备避免脏写和脏读的能力
二、锁的设计是引发脏读和脏写的关键
假定您已了解,数据库 XA 协议无论 Phase2 的决议是 commit 还是 rollback,事务性资源的锁都要保持到 Phase2 完成才释放,如下图所示:
而绝大部分情况下全局事务是成功提交的,那么绝大部分情况下,可以省去 Phase2 的资源锁定(数据库锁和数据库连接),如下图所示:
这个设计,给整体性能的提升提供了支撑,原因就在与他极大地减少了分支事务对资源(连接和锁)的锁定时间:
- 在分支事务 Phase1 结束时,本地 DB 连接也得以释放。
- 因分支事务中数据的本地 DB 锁由本地 DB 事务管理,在分支事务 Phase1 结束时释放,这时候其他本地 DB 事务就能读取到最新的数据。
提示:在绝大部分应用在读已提交的隔离级别下工作是没有问题的,本地事务的锁这么早释放,2 阶段没有本地事务锁的保障,是否会出现读未提交的问题呢?实际上,有不少应用场景在读未提交的隔离级别下是没有问题的,而锁在 1 阶段释放掉带来的性能提升也是基于。
相较于 XA 模式 2 个阶段的本地 DB 资源锁定,AT 模式的初衷是减少非必要的 2 阶段本地 DB 资源的锁定,这其中为了保障隔离性,锁在两个阶段肯定是都不能少的(下文补充详情),基于复杂度不会减少只会迁移的的理论模型来推测,AT 模式一定是采用了其他锁方案来替代 2 阶段本地锁
,即由 Seata TC 侧提供的全局锁
,那么 Seata 就需协调本地锁
和全局锁
来保障隔离性,其协作机制如下::
- 一阶段本地事务提交前,需要确保先拿到全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿全局锁有尝试上限(有限次数的重试),超出限制将放弃,并回滚本地事务,释放本地锁。
markdown复制代码分支事务一_开始
|
V 获取 本地锁
|
V 获取 全局锁 分支事务二_开始
| |
V 释放 本地锁 V 获取 本地锁
| |
V 释放 全局锁 V 获取 全局锁
|
V 释放 本地锁
|
V 释放 全局锁
如上所示,一个分布式事务的锁获取流程是这样的
-
先获取到本地锁即可修改本地数据,但不能直接提交本地事务