如果没有并发的存在,数据库所有事务总是串行执行的,那么也就不会有临界资源竞争的情况出现,但现实情况是不可能没有并发的存在。
为了保证并发的正确性,需要通过对数据库资源加锁。本文将首先介绍数据库中的锁,之后会介绍各个隔离级别及其实现方式。
锁
- 写锁(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 事务的第二次查询将会被阻塞。
综上所述:
- 事务的隔离性是通过锁的机制来实现的。
- 事务隔离性的不同本质上是锁机制的不同。
隔离性问题
Phantom Reads, 幻读
幻读是指,当一个事务 A 使用相同的查询条件进行两次范围查询时,在两次查询之间有其他事务插入或者删除了数据,导致事务 A 前后两次查询到的数据不一致。
From Wikipedia:
A 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:
A 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:
A 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
- 本地事务 | 凤凰架构
- 《MySQL 技术内幕——InnoDB 存储引擎》
- Isolation (database systems) - Wikipedia