事务具有 ACID 特性,将应用程序的多个读、写操作捆绑在一起成为一个逻辑操作单元,整个事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试,并不会出现部分失败的情况。而事务的隔离级别为了解决事务执行过程中的并发问题。
读提交隔离
读提交隔离是最基本的事务隔离级别,为了解决以下两种并发问题
- 脏读: 读数据库时,看到未成功提交的数据
- 脏写: 写数据库时,覆盖未成功提交的数据
数据库通常采用行级锁来防止脏写,当事务想修改某个对象时, 必须首先获得该对象的锁。然后一直持有锁直到事务提交(或中止)。数据库通常采用多版本的方式来防止脏读,对于每个待更新的对象,数据库都会维护其旧值和当前持锁事务将要设置的新值两个版本。在事务提交之前,所有其他读操作都读取旧值;仅当写事务提交之后,オ会切换到读取新值。
快照隔离
快照隔离(可重复读)为了解决了不可重复读(读倾斜)这个并发异常现象。如下图所示,Alice 在银行的两个账户中分别存有500美元的存款。转账者事务执行转账交易,事务中包含两个步骤:步骤一向账户1增加100,步骤二将账户2减少100。如果 Alice 进行一个并发的查询事务,那么她在转账者事务开始时的查询结果与转账者事务结束时的查询结果不同。图示的异常对于 Alice 来说,查询结果说明,账户1+账户2的总值只有900,不符合预期。
快照隔离的总体想法是,每个事务都从数据库的一致性快照中读取,保证每个事务都只看到该特定时间点的旧数据。快照级别隔离对于长时间运行的只读査询(如备份和分析)非常有用。
MVCC多版本并发控制在数据库中保留了对象的多个不同的提交版本,在读提交隔离级别下,对每一个不同的查询单独创建一个快照;而快照级别隔离则是使用一个快照来运行整个事务。如下图所示,当事务开始时,首先赋予一个唯一的、单调递增的事务ID(txid
)。每当事 务向数据库写入新内容时,所写的数据都会被标记写入者的事务ID。created_by
字段表示创建该行的 txid
,deleted_by
表示标记删除该行的 txid
串行化隔离
串行化隔离可以解决写倾斜异常。 常见实现方式有严格串行化执行,两阶段加锁以及可串行化的快照隔离。其中严格串行化执行只能用到单核,无法保证可拓展性。
两阶段加锁
近三十年来,最广泛使用的串行化算法,两阶段加锁(two-phase locking, 2PL)。多个事务可以同时读取同一对象,但只要出现任何写操作,则必须加锁以独占访问。因此 2PL不仅在并发写操作之间互斥,读取也会和修改产生互斥。
- 如果事务 A 已经读取了某个对象,此时事务 B 想要写入该对象,那么 B 必须等到 A 提交或中止之オ能继续。以确保 B 不会在 A 执行的过程中间去修改对象。
- 如果事务 A 已经修改了对象,此时事务 B 想要读取该对象,则 B 必须等到 A 提交或中止之后オ能继续。对于 2PL,不会出现读到旧值的情况。
2PL 导致严重的性能下降,原因在于锁的获取和释放本身的开销,但更重要的是其降低了事务的并发性。2PL使用了悲观并发控制的设计原则:如果某些操作可能与其他并发事务发生锁冲突,那么直接放弃,采用等待方式直到绝对安全。类似互斥锁的思想。
可串行化的快照隔离
可串行化的快照隔离(Serializable Snapshot Isolation, SSI)算法提供了完整的可串行性保证,而性能相比于快照隔离损失很小。SSI 基于乐观并发控制的设计原则:如果可能发生潜在冲突,事务会继续执行而不是中止;而当事务提交时,数据库会检査是否确实发生了冲突,如果是的话则回滚重试。SSI 增加了以下判断来防止事务修改其他事务的查询结果。
检测是否读取了过期的MVCC对象。事务提交时,数据库会检査是否存在一些当初被忽略的写操作现在已经完成了提交,如果是则必须中止当前事务。
检测写是否影响了之前的读。事务提交时,数据库会检査是否存在一些提交的写操作影响已读数据,如果是则必须中止当前事务。
更新丢失异常
在快照隔离级别会出现的异常。应用程序从数据库读取某些值,根据应用逻辑做出修改,然后写回新值(read-modify-write 过程)。当有两个事务在同样的数据对象上执行类似操作时,由于隔离性,第二个事务并不会读取第一个事务修改后的值,最终会导致第一个事务的修改值可能会丢失。有以下方式解决这个并发写冲突问题:
- 原子写: 数据库提供原子更新操作,避免应用层代码的 read-modify-write 操作
- 显示加锁: 显示锁定需要更新的对象
- 自动检测更新丢失: 在事务执行过程中,如果检测出更新丢失风险,则回滚当前事务
写倾斜异常
在快照隔离级别会出现的异常。如下图所示,Alice 和 Bob 是两名值班医生,数据库存储两人的值班信息。两人并发进行修改值班信息的事务:查询值班医生人数,如果有足够的值班人员,则将自己的 oncall 字段置为 false。由于快照隔离的特性,两个事务都执行成功了,这是不符合预期的,这样就没有足够的值班人员。写倾斜视可以看做广义的更新丢失问题,即如果两个事务读取相同的一组对象,然后更新其中一部分:不同的事务可能更新不同的对象,则可能发生写倾斜;而不同的事务如果更新的是同一个对象,则可能发生更新丢失。
幻读异常
在一个事务中的写入改变了另一个事务的查询结果的现象为幻读。快照隔离级别可以避免只读査询时的幻读(读倾斜),但是无法避免读写事务的幻读(写倾斜)。
总结
脏读/脏写 | 读倾斜 | 更新丢失 | 幻读 | 写倾斜 | |
---|---|---|---|---|---|
读未提交隔离 | - | - | - | - | - |
读提交隔离 | + | - | - | - | - |
快照隔离 | + | + | - | - | - |
串行化隔离 | + | + | + | + | + |
REFERENCE
- Martin Kleppmann. 数据密集型应用系统设计[M]. 赵军平, 李三平, 吕云松, 等, 译. 中国电力出版社, 2018.
文档信息
- 本文作者:wzx
- 本文链接:https://masterwangzx.com/2022/01/17/transaction-and-isolation/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)