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

详细解析数据库事务隔离性的实现原理,包括锁机制和各种隔离级别的工作方式

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

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

#

  • 写锁(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