Lihang Liu's Homepage

本地事务的隔离性是如何实现的

如果没有并发的存在,数据库所有事务总是串行执行的,那么也就不会有临界资源竞争的情况出现,但现实情况是不可能没有并发的存在。

为了保证并发的正确性,需要通过对数据库资源加锁。本文将首先介绍数据库中的锁,之后会介绍各个隔离级别及其实现方式。

  • 写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为 X-Lock):如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。
  • 读锁(Read Lock,也叫作共享锁,Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务加了读锁,允许直接将其升级为写锁,然后写入数据。
  • 范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。

特别注意:写锁禁止其他事务施加读锁,而不是禁止事务读取数据。

兼容性

只有读锁和读锁是兼容的,写锁不兼容任何其他锁。

Write Lock (X) Read Lock (S)
Write Lock (X) 不兼容 不兼容
Read Lock (S) 不兼容 兼容

事务隔离级别

事务的隔离性是通过锁的机制来实现的,事务的隔离性越高,并发吞吐量越低,为了让开发人员能够在吞吐量以及隔离性之前取的较好的平衡点,数据库提供了多种隔离性级别。

从本质上来说,事务在不同隔离级别下的不同表现,来源于不同隔离级别采取的加锁机制的不同。

下表是事务的 4 个隔离级别,隔离性从上到下依次递减:

隔离级别 特征 可能存在的问题
Serializability,可串行性 多个事务并发执行的效果,和串行执行的效果一致。
Repeatable read,可重复读 可重复度保证一个事务读取到的数据,在整个事务执行过程中不会改变。 幻读
Read committed,读已提交 不允许一个事务读取到其他事务提交的数据。 幻读、不可重复读
Read uncommitted,读未提交 允许一个事务读取到其他事务未提交的数据。 幻读、不可重复读、脏读

Serializability, 可串行性

对事务所涉及到的数据加读锁、写锁、范围锁即可实现 Serializability 所要求的隔离性。

Repeatable Read, 可重复读

可重复读 对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁。这就意味着 可重复读 可能会出现幻读(Phantom reads, 可参考下文针对幻读问题的讨论)的问题,比如说:

事务 T1 在两次查询取得的数据将会不同,这是因为事务 T1 并没有加范围锁,而 T2 在两次查询之间插入了一行新的记录,这是允许的,因为 可重复读 仅仅只会对数据加读锁和写锁。

Read Committed, 读已提交

读已提交 对事务所涉及的数据将会加写锁和读锁,写锁在被施加后会一直持续到事务结束为止,但是读锁在查询操作结束后将会立即得到释放。

读已提交 除了幻读问题之外,还存在着 不可重复读 (Non-repeatable reads, 可参考下文对不可重复读问题的讨论)的问题,举个例子:

事务 T1 将会读到 T2 对数据的修改,这是因为在 读已提交 隔离级别下,事务不会对数据加贯穿整个事务生命周期的读锁,在 T1 事务第一次查询结束之后,该行记录的写锁就被释放了,T2 也因此能够对改行数据进行写操作。

Read Uncommitted, 读未提交

读未提交 对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。

读未提交 除了有幻读、不可重复读问题之外,还可能面临脏读的问题(Dirty reads,可参考下文对脏读问题的解释),举例如下:

读未提交 完全不加读锁,因此即便 T2 对行数据加了写锁,T1 也能读到 T2 对行数据的修改。

这里要特别注意,写锁会阻塞任何加写锁或读锁的操作,并不阻塞单纯的读取行为。 读未提交 在查询时不加读锁,因此不会被阻塞。

假如在 读已提交 的隔离级别下,因为在查询前会先加读锁,而 T2 此时已对该行数据加了写锁,T1 事务的第二次查询将会被阻塞。

综上所述:

  1. 事务的隔离性是通过锁的机制来实现的。
  2. 事务隔离性的不同本质上是锁机制的不同。

隔离性问题

Phantom Reads, 幻读

幻读是指,当一个事务 A 使用相同的查询条件进行两次范围查询时,在两次查询之间有其他事务插入或者删除了数据,导致事务 A 前后两次查询到的数据不一致。

From Wikipedia:

phantom read occurs when a transaction retrieves a set of rows twice and new rows are inserted into or removed from that set by another transaction that is committed in between.

Non-repeatable Reads, 不可重复读

不可重复读是指,一个事务 A 对某一行查询了两次,因为其他事务在两次查询之间对这一行数据进行了修改,导致事务 A 的两次查询结果不一致。

From Wikipedia:

non-repeatable read occurs when a transaction retrieves a row twice and that row is updated by another transaction that is committed in between.

Drity Reads, 脏读

脏读是指,一个事务读到了另一个事务未提交的修改。

From Wikipedia:

dirty read (aka uncommitted dependency) occurs when a transaction retrieves a row that has been updated by another transaction that is not yet committed.

Dirty Write, 脏写

脏写是指,一个事务覆盖了另一个事务未提交的更新:

假设事务既不加读锁也不加写锁,那么 T1 对行数据的更新将会被 T2 对该行数据的写操作所覆盖。

不过,即便是 Read Uncommitted 隔离级别下,也不会出现这种情况。脏写意味着事务的原子性都被破坏了,所以一般不把它纳入隔离性相关问题的讨论范围内。

总结

下表总结了各个事务隔离级别下可能遇到的问题:

Phantom Reads Non-repeatable Reads Dirty Reads
Serializability 不可能 不可能 不可能
Repeatable Read 可能 不可能 不可能
Read Committed 可能 可能 不可能
Read Uncommitted 可能 可能 可能

下表总结了各个事务隔离级别下对锁的使用情况:

Read Locks Write Locks Range Locks
Serializability 加读锁并持有到事务结束 加写锁并持有到事务结束 加范围锁并持有到事务结束
Repeatable Read 加读锁并持有到事务结束 加写锁并持有到事务结束 不加范围锁
Read Committed 加读锁,查询结束后立即释放 加写锁并持有到事务结束 不加范围锁
Read Uncommitted 不加读锁 加写锁并持有到事务结束 不加范围锁

Reference

  1. 本地事务 | 凤凰架构
  2. 《MySQL 技术内幕——InnoDB 存储引擎》
  3. Isolation (database systems) - Wikipedia