以 InnoDB 为例——深入理解事务隔离级别是如何实现的

784 阅读6分钟

一、概述

隔离性保证了每个事务各自读、写的数据互相独立,不会彼此影响。在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间可见的,哪些是不可见的。越高级别的隔离可以保证数据的一致性,但执行并发也就越低;同样较低级别的隔离可以执行更高的并发,但却会产生幻读、不可能重复读、脏读等各种可能的问题。

虽然隔离性问题属于数据库理论的基础知识,但大多数人可能只是将它们当作数据库的某种固有属性,而忽视了不同的隔离级别的本质,实际上就是各种锁在不同加锁时间上组合而产生的结果。所以下面我将以MySQL InnoDB存储引擎为例,从锁开始,逐一解释四种隔离级别是如何产生的。

二、InnoDB 的锁

无论何时,只要有请求需要在同一时刻修改数据,就会产生并发控制的问题。而解决并发问题最常用的方法,就是加锁同步。MySQL InnoDB 提供了以下几种锁,来应对不同情况下的并发问题。

2.1 共享锁与排他锁

  1. 共享锁(Shard Lock)

简称 S 锁,又叫读锁(Read Lock),事务在读取一条记录时,需要先获取该记录的 S 锁。之所以被称为共享锁,是因为多个事务在同一时刻可以同时读取同一个资源而互不干扰。

  1. 排他锁(eXclusive Lock)

简称 X 锁,又叫写锁(Write Lock),事务要改动一条记录时,需要先取该记录的 X 锁。之所以被称为排他锁,是因为当一个事务获取到某资源的 X 锁时,会阻塞其他事务获取该资源的 X 锁和 S 锁,直到它释放 X 锁为止。

2.2 表锁、行锁和间隙锁

  1. 表锁(Table Lock)

表锁是 MySQL 中最基本的锁策略,也是开销最小的锁策略。它会锁定整张表。

  1. 行锁(Row Lock)

行锁可以最大程序地支持并发处理,但同时也会造成最大的锁开销。它只会通过索引上的索引项,来锁定某一行的数据,这也就是为什么使用行锁的提前是执行计划使用了索引。

  1. 间隙锁(Next-Key Lock)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

三、四种隔离级别与锁的关系

前面提到过,不同的隔离级别的本质,实际上就是各种锁在不同加锁时间上组合产生的结果,而与此有关的锁有三种,在 InnoDB中,即写锁(排他锁)、读锁(共享锁)和范围锁(间隙锁)。

3.1 可串行化(Serializable)

Serializable 是最高的隔离级别,它通过强制事务串行执行,避免了低事务级别可能产生的幻读、不可重复读和脏读等问题。

对事务所有读、写的数据全都加上读锁、写锁和范围锁即可实现可串行化隔离级别。

3.2 可重复读(Repeateable Read)

MySQL InnoDB 的默认隔离级别,该级别保证了在同一具事务中多次读取同样的记录的结果是一致的。

对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不加范围锁,即可实现可重复读隔离级别。相对于可串行化弱化的地方在于会产生幻读问题(Phantom Read)。

所谓幻读问题,是指在事务执行过程中,两个完全相同的范围查找得到了不同的结果集。因为在可重复读隔离级别下,没有范围锁来禁止在该范围内插入新的数据,所以当事务 T1 查询某一范围的同时,事务 T2 插入了一条在该范围内的数据,就会导致 T1 出现两次查询的结果集不一致的现象,也即产生了幻读问题。

3.3 读已提交(Read Committed)

一个事务从开始直到提交之前,所做的任何修改,对其他事务都是不可见的,同理,一个事务开始时,只能“看见”其他已经提交过了的事务所做的修改。

对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放,即可实现读已提交隔离级别。读已提交比可重复读弱化的地方在于出现不可重复读(Non-Repeatable Reads)问题。

所谓不可重复读问题,是指在事务执行过程中,对同一行数据的两次查询得到了不同的结果。这是由于在读已提交隔离级别下,事务 T1 在查询操作完成后,会立即释放读锁,这就给了事务 T2 修改数据的可趁之机(因为它可以获取写锁了),如果此时 T2 修改了数据并提交,T1 再次进行查询时,就会出现两次查询的结果不一致的现象,也即产生了不可重复读问题。

3.4 读未提交(Read Uncommited)

在此级别下,事务中的修改,即使没有提交,对其他事务也都是可见的。

对事务涉及的数据只加写锁,会一起持续到事务结束,但完全不加读锁,即可实现读未提交隔离级别。读未提交比可重复读弱化的地方在于脏读(Dirty Read)问题。

所谓脏读问题,指在事务执行过程中,一个事务读取到了另一个事务未提交的数据。因为在读未提交隔离级别下,全程没有加读锁,当事务 T1 修改数据完成后,即使没有提交将写锁释放,T2 却也能够看到 T1 未提交的数据(写锁禁止其他事务施加读锁,而不是禁止其他事务读取数据),也即产生了脏读问题。