隔离级别

事务隔离级别

在前面章节中,我们介绍了事务的 ACID 特性,即原子性、一致性、隔离性、持久性;但是多个事务同时执行的时候,就可能出现脏读,不可重复读,幻读等现象:

  • 脏读:一个客户端读取到另一个客户端尚未提交的写入。读已提交或更强的隔离级别可以防止脏读。
  • 脏写:一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏写。
  • 读取偏差(不可重复读):在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。譬如 B 事务读到了 A 事务已经提交的数据,即 B 事务在 A 事务提交之前和提交之后读取到的数据内容不一致(AB 事务操作的是同一条数据);快照隔离经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用多版本并发控制(MVCC)来实现。
  • 更新丢失:两个客户端同时执行读取-修改-写入序列。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(SELECT FOR UPDATE)。
  • 写偏差:一个事务读取一些东西,根据它所看到的值作出决定,并将决定写入数据库。但是,写入的时候,决定的前提不再是真实的。只有可序列化的隔离才能防止这种异常。
  • 幻读:事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。譬如 B 事务读到了 A 事务已经提交的数据,即 A 事务执行插入操作,B 事务在 A 事务前后读到的数据数量不一致。快照隔离可以防止直接的幻像读取,但是写入歪斜环境中的幻影需要特殊处理,例如索引范围锁定。

为了解决这些问题,就有了隔离级别的概念,SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化/可序列化(serializable)。

  • Read Uncommitted(读未提交): 一个事务还没提交时,它做的变更就能被别的事务看到,读取尚未提交的数据,哪个问题都不能解决;
  • Read Committed(读已提交):一个事务提交之后,它做的变更才会被其他事务看到,读取已经提交的数据,可以解决脏读;这也是 Oracle 默认级别。
  • Repeatable Read(可重复读):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,可以解决脏读和不可重复读;这是 MySQL 的默认级别
  • Serializable(串行化):顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。可以解决脏读、不可重复读和虚读—相当于锁表。

其中,这些不同的隔离级别可以分为弱隔离级别与可序列化两大类,弱隔离级别可以防止这些异常情况,但是让应用程序开发人员手动处理其他应用程序(例如,使用显式锁定)。只有可序列化的隔离才能防范所有这些问题,不过隔离得越严实,效率就会越低,我们在实际应用中也很少使用可序列化级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。事务的并行度随着隔离级别的增加而衰减,除了并发度最低的可序列化,其他隔离级别都伴随着对一致性的权衡和牺牲。

事务隔离的实现方案

事务隔离级别有两种常见的实现方案,即锁实现和 MVCC 实现;其中除了 Infomix 等少数数据库,大部分关系型数据库均采用 MVCC 实现。下表是基于锁实现的隔离级别对照表。

隔离级别 脏读脏写 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可序列化 不可能 不可能 不可能

而基于 MVCC 实现的隔离级别对照表与基于锁的也是有所区别:

隔离级别 脏读脏写 不可重复读 幻读 写偏序
读未提交 无需实现 无需实现 无需实现 无需实现
读已提交 不可能 可能 可能 可能
可重复读 不可能 不可能 不可能 可能
可序列化 不可能 不可能 不可能 不可能

通过 MVCC 实现的隔离级别实际上只有 SI(快照隔离)和 SSI(可序列化快照隔离)这 2 种。SI 和 SSI 与 ANSI 的 4 种隔离级别并不能完全对照。其中的读未提交,与读已提交在 MVCC 的实现中性能并无差别,可以忽略不计。因此 SI 可以对应为读已提交和可重复读这 2 种隔离级别。实际上,即使是幻读,在 SI 隔离级别中也是不会出现的。由于快照并发控制并不能真正意义上保证事务是可串行化的,所以事务间的并发操作依旧有可能引发数据异常现象。但这里的异常不同于之前提到的脏读、丢失更新的异常,而是一种业务数据间逻辑语义层面的异常,也可以说是由于未能满足数据间的语义约束而产生的异常。这被称之为写偏序(Write skew),它的检测可依据并发事务间读写依赖的多版本可串行化图(The multiversion serialization graph)来实现,即 SSI 隔离级别。