数据库的隔离等级

要说关系型数据库的特性,我相信很多人都会想到 ACID 这个词。在 ACID (原子性、一致性、隔离性、持久性) 这四个特性中,最为复杂的是隔离性。在本文中,我们将讨论几种常见的数据库隔离等级,以及它们对数据库应用正确性的影响。


数据库隔离等级概述

所谓数据库的隔离,是指多个事务(transaction)同时发生时,事务之间的相互影响。在理想的情况下,一个事务不应对另一个事务产生副作用。但实践表明,这种数据库的处理速度较慢,若能适当放宽对隔离性的要求,则可以显著增强数据库处理并发事务的能力。

现今通用的数据库隔离等级共分为五级,按隔离性从低到高,分别为:

  1. 未提交读取(read uncommitted)
  2. 已提交读取(read committed)
  3. 可重复读取(repeatable read)
  4. 快照隔离(snapshot isolation)
  5. 可序列化(serializable)


未提交读取

每个数据库的事务通常是由一系列读写操作组成的。数据库的原子性保证了一笔事务要么被完整地执行,要么完全没有被执行(回滚到先前的状态),但是原子性并没有规定事务执行的中间状态是否对其他事务可见。如果事务 A 执行的中间状态对事务 B 是可见的,那么我们称数据库的隔离等级为『未提交读取』,事务 B 读取事务的 A 中间状态称为『脏读』现象。

比如,路人甲在某银行开设了两个账户,分别是支票账户和储蓄账户。根据银行的协议,甲为了维持其 VIP 客户身份,在这家银行存入的资金总额不得低于 50 万元。假设初始条件下,甲在两个账户各拥有 30 万元,现在他想把支票账户中的 20 万元转移到储蓄账户中。

银行后台的数据库有一系列事务在同时运行。其中事务 A 完成转账过程:它将 20 万元从甲的支票账户上扣除,再将 20 万元加到甲的储蓄账户中。而事务 B 则在监控甲的资金流动,判断甲是否符合 VIP 用户的条件。若事务 B 可以读取事务 A 的中间状态,则 B 会发现甲的资金总额只有 40 万元,不再满足 VIP 的条件,而这是不正确的。

从上面的例子可以看出,未提交读取很容易引发数据库应用的错误。一般来说这种隔离等级使用的情况较少。


已提交读取

为了解决上个例子中因『脏读』而出现的错误,我们为数据库的隔离性提出了更高的要求。如果事务 B 无法读取事务 A 的中间状态,只能读取事务 A 在提交(完整执行)之前的值,或者事务 A 提交之后的值,则称数据库的隔离等级为『已提交读取』。

在『已提交读取』这个等级依旧会出现一些问题。比如,假设各地征收利息税的标准并不相同。路人甲向银行提交了一个变更,从 M 市搬到利息税不同的 N 市。在银行的数据库中,事务 A 负责更改甲的住址,而事务 B 则在计算甲的两个账户的利息税。在计算支票账户时,事务 B 看到甲在 M 市,于是按 M 市的税率得出了支票账户应缴的利息税。随后当事务 B 计算储蓄账户时,看到了事务 A 已经提交了住址的变更,于是按 N 市的税率得出了储蓄账户应缴的利息税。于是就出现了两个账户缴税不一致的情况。


可重复读取

如果进一步加强事务间的隔离性,就需要保证事务 B 在多次读取时,读取的内容不应被事务 A 修改。这样的特性通常被描述为『可重复读取』。

不过,可重复读取并不意味着事务 B 在多次读取时,总是能读到相同的数据。这是因为按照标准,可重复读取只是屏蔽了数据修改的结果,而没有对新插入的数据加以限制,因此会出现所谓的『幽灵读取』现象。之所以会有这样鸡肋的设定,很大程度上是由 SQL 数据库的底层实现决定的。

比如,路人甲某日又在该银行开设了第三个账户——股票账户,并存入了 30 万元。此时,银行正在清算资产。在银行的数据库中,事务 A 为甲开设了股票账户并存入了 30 万元。事务 B 在统计银行在本市和本省所拥有的存款总额。在统计本市总额时,事务 B 尚未看到事务 A 的提交,因此只计入了甲 60 万元的存款;而当统计本省总额时,事务 B 看到了事务 A 的提交,因为新增账户并不违背『可重复读取』的要求,所以计入了甲 90 万元的存款。最后会导致的问题是,各市的存款总额不等于全省的存款总额。


快照隔离

在数据库的隔离等级中,『快照隔离』是可以解决事务读取不一致这个问题的。快照隔离保证,每一个事务的读取操作,都会基于某个特定时刻的数据库的状态,因此排除了其他事务的写入产生的干扰。也就是说,除非一个事务自己写入了新值,否则它每次读取的内容必定是相同的。


可序列化

可序列化是数据库隔离的最高等级。它保证,一系列事务执行的最后结果,等价于这些事务按某个次序逐一执行的结果。也就是说,这些并行的事务完全不会对彼此造成影响。

既然都能够排除其他事务的干扰,那快照隔离和可序列化的区别是什么呢?我们可以假设这样一个场景。路人甲通过网上银行同时提交了两个请求,一个请求是将其支票账户中的所有资金转移到储蓄账户,另一个请求则是将储蓄账户中的所有资金转移到支票账户。如果这两个请求分别用事务 A 和事务 B 描述,在可序列化的隔离标准下,事务 A 和事务 B 必定有一个先发生而另一个后发生,所以最终结果要么是两个账户的所有资金都转入了支票账户,要么是都转入了储蓄账户。但是,在快照隔离的标准下还存在另一种可能,那就是事务 A 和事务 B 以相同的初始状态(支票账户 10 万,储蓄账户 50 万)作为数据库状态的参照,因此两个事务执行完的结果是甲的支票账户剩余 50 万元,储蓄账户剩余 10 万元。最后的这种可能性,在可序列化的隔离等级下是不可能发生的。