关键要点
- 仅从 ACID 或非 ACID 角度考虑问题是不够的,你应知道你的数据库支持何种事务隔离级别。
- 一些数据库宣称自己具有“最终一致性”,但却可能对重复查询返回不一致的结果。
- 相比于你所寻求的数据库,一些数据库提供更高的事务隔离级别。
- 脏读可导致同一记录得到两个版本,或是完全地丢失一条记录。
- 在同一事务中多次重新运行同一查询后,可能会出现幻读。
最近 MongoDB 登上了 Reddit 的头条,因为 MongoDB 的核心开发者 David Glasser 痛苦地认识到 MongoDB 默认会执行脏读。
在本文中,我们将解释什么是事务隔离级别和脏读,并给出一些广受欢迎的数据库是如何实现它们的。
ANSI SQL 给出了四种标准的事务隔离级别:可序列化 (Serializable)、可重复读 (Repeatable reads)、提交读 (Read committed) 和未提交读 (Read uncommitted)。
许多数据库缺省是提交读的,这保证了在事务运行期间用户看不到转变中的数据。提交读的实现通过在读取时暂时性地获取锁,并持有写入锁直至事务提交。
如果在一个事务中需要多次重复同一读取,并想要“合理地确定”所有的读取总是会得到同样的结果,这要在整个过程期间持有读取锁。在使用可重复读事务隔离级别时,上述操作是自动完成的。
我们这里所说的“合理地确定”可重复读,是因为存在“幻读”(phantom reads)的可能性。当执行使用了 WHERE 语句的查询时,类似于“WHERE Status=1”,就有可能发生幻读。虽然所涉及的行将被锁上,但是这并不能阻止匹配 WHERE 条件的新行被添加进来。“幻”(phantom)一词指在查询第二次执行时所出现的行。
为确保在同一事务中的两次读取会返回同样的数据,可使用可序列化事务隔离级别。可序列化使用了“范围锁”,避免了匹配 WHERE 条件的新行添加到一个开放的事务中。
一般情况下,由于锁竞争的存在,事务隔离级别越高,性能越差。因此为了改进读取性能,一些数据库还支持未提交读。该事务隔离级别将无视锁的存在(事实上其在 SQL Server 中被称为“NOLOCK”),因此该级别下可执行脏读。
脏读所存在的问题
在探讨脏读问题之前,你必须要理解表并非是真实存在于数据库中的,表只是一个逻辑结构。事实上你的数据是按一个或多个索引进行存储的。主索引在大多数数据库中被称为“聚束索引”或“堆”(该术语在各 NoSQL 数据库中各不相同)。因而当执行插入操作时,需要在每个索引中插入一行。当执行更新操作时,数据库引擎仅需访问指到被改变列的索引。但更新操作常常必须要在每个索引上执行两个操作,即从旧的位置删除并在新的位置插入。
在下图中,你可看见一个普通的表,还有表中 IX_Customer_State 和 PK_Customer 对象更新操作的执行计划。鉴于表的 FullName 列并未改变,所以可以跳过 IX_Customer_FullName 索引。
(点击放大图像)
注意在SQL Server 中,PK 前缀指代主键,通常也是用于聚束索引的键。IX 用于指代非聚束索引。其它的数据具有它们自己的命名规范。
解决了上述问题,让我们看一下脏读导致不一致数据的多种途径。
未提交读问题易于理解。在事务被完全提交之前,如果无视写入锁的存在,使用“未提交读”的SELECT 语句就可以就看到新插入或更新的行。如果这些转变操作这时被回滚,从逻辑上说,SELECT 操作将返回并不存在的数据。
如果数据在更新操作过程中被移动了,这就产生了双重读取。例如,你正在读取所有的客户记录的状态。如果在你读取“California”记录和读取“Texas”记录之间,上面所说的更新语句被执行了,你就能看见“客户1253”记录两次。一次是旧值,一次是新值。
记录丢失发生的方式相同。如果我们提取“客户1253”记录并将其从“Texas”记录移动到“Alaska”记录,并再次使用状态去选择数据,你可能会完全地丢失该记录。这就是发生在David Glasser 的MongoDB 数据库中的事情。由于在更新操作期间读取了索引,查询丢失了记录。
脏读也会妨碍到排序操作,该问题的出现取决于数据库的设计方式及特定的执行计划。例如,脏读可能发生于执行计划对所有候选数据行采集指针信息时,如果在其后一行数据被更新了,但实际上执行引擎还是会使用已被采集的指针信息从原始位置拷贝数据。
快照隔离,或被称为“行级版本控制”
为在避免脏读问题的同时提供好的性能,许多数据库支持快照隔离语义。运行于快照隔离状态下,当前的事务不能看到任何先于其启动的其它事务的结果。
快照隔离的实现是通过做被改变行的临时拷贝,而非仅依靠于锁机制,因此它也常被称为“行级版本控制”。
很多支持快照隔离语义的数据库在被请求使用“提交读”事务隔离时,会自动使用快照隔离。
SQL Server 中的事务隔离级别
SQL Server 支持所有四种 ANSI SQL 事务隔离级别,外加一种显式的快照隔离级别。提交读可能也使用快照语义,这取决于数据库中 READ_COMMITTED_SNAPSHOT 选项的配置方式。
在开关该选项前,你的数据库需要做充分的测试。虽然提交读可以提升读取性能,但它也同时降低了写入性能。尤其是 tempdb 被部署在慢速磁盘上时,因为这存储了行的旧版本。
在 SELECT 语句中可以使用臭名昭著的 NOLOCK 指示符。NOLOCK 的作用等同于将事务运行设置为未提交读。这在 SQL Server 2000 及更早期的版本中被大量地使用,因为那时并没有提供行级版本控制。尽管现在不再必要或不建议这样做,但是该习惯仍然保留着。
更多信息参见“设置事务隔离级别 (Transact-SQL) ”.
PostgreSQL 中的事务隔离级别
虽然官方宣称 PostgreSQL 支持所有四种 ANSI 事务隔离级别,但事实上 PostgreSQL 中只有三种事务隔离级别。每当查询请求“未提交读”时,PostgreSQL 就默默地将其升级为“提交读”。因此 PostgreSQL 不允许脏读。
当你选取“未提交读”级别时,事实上你得到了“提交读”,在 PostgreSQL 对可重复读的实现中,脏读是不可能发生的,因此实际的事务隔离级别可能比你所选取的要更加严格。这是被 SQL 标准所允许的,因为四种事务隔离级别仅定义了事务中一定不能发生的现象,它们并未定义应该发生哪种现象。
PostgreSQL 并未显式地提供快照隔离。当然快照隔离是在使用提交读时自动发生的。这是因为 PostgreSQL 的设计从一开始就考虑了多版本并发控制。
在9.1 版本之前,PostgreSQL 不提供可序列化事务,会将它们静默降级为可重复读。但当前所有仍在支持的PostgreSQL 版本中都不再有这个限制了。
更多的信息参见PostgreSQL 官方文档的 13.2 节,“ 事务隔离”.
MySQL 中的事务隔离级别
InnoDB 默认为可重复读,但是提供所有四种 ANSI SQL 事务隔离级别。提交读使用快照隔离语义。
更多 InnoDB 相关的信息,参见 MySQL 官方文档的 15.3.2.1 节“ 事务隔离等级”
事务在使用 MyISAM 存储引擎时是完全不被支持的,这里使用了表一级的单一读写锁(虽然在某些情况下,插入操作是可以绕过锁的。)
Oracle 中的事务隔离等级
Oracle 只支持三种事务隔离级别,即提交读、可序列化和只读。在 Oracle 中,提交读是默认的,它使用快照语义。
类似于 PostgreSQL,Oracle 并不提供未提交读,永不允许脏读。
可重复读并不在 Oracle 的支持列表中。如果你需要在 Oracle 中具有该行为,你的事务隔离级别需要被设置为可序列化。
只读是 Oracle 所独有的事务隔离级别。但是对此并没有很好的文档,手册中只有如下描述:
只读事务只能看见那些在事务开始阶段就被提交的改变,不允许 INSERT、UPDATE 和 DELETE 语言。
对其它两种事务隔离级别的更多信息,参见 Oracle 官方文档第13 章“数据并发和一致性”。
DB2 中的事务隔离级别
DB2 具有四种隔离级别,分别称为可重复读、读稳定性、游标稳定性和未提交读。这四种级别并不与上述四种 ANSI 术语一一对应。
可重复读对应于 ANSI SQL 中的可序列化,意味着不可能存在脏读。
读稳定性对应于 ANSI SQL 中的可重复读。
游标稳定性用于提交读,是 DB2 的默认设置配置。对于 9.7 版快照语义生效。而在 9.7 的前期版本中,DB2 使用类似于 SQL Server 的锁机制。
未提交读在很大程度上类似于 SQL Server 中的未提交读,也允许脏读。手册中推荐仅在只读表上使用未提交读,或是用在“可以看到未被其它应用提交的数据时”。
更多信息参见“事务隔离级别”。
MongoDB 中的事务隔离级别
正如前文所提到的,MongoDB 不支持事务。在其手册中对此是这样描述的:
因为在 MongoDB 中对单一文档的操作是原子的,两阶段提交只能提供类事务语义。在两阶段提交或回滚期间,应用可在中间点返回中间数据。
事实上这意味着 MongoDB 使用脏读语义,具有双倍或丢失记录的可能性。
CouchDB 中的事务隔离等级
CouchDB 也不支持事务。但是不同于 MongoDB 的是,它使用了多版本并发控制去避免脏读。
读取请求将总是在请求开始时就能看到数据库的最新快照。
这所给予 CouchDB 的事务隔离等级,等价于具有快照语义的提交读。
更多的信息参见“最终一致性”。
Couchbase Server 的事务隔离级别
Couchbase Server 常被混淆为 CouchDB,但它是一种完全不同的产品。就索引而言,它并未提供任何形式的隔离。
当执行更新操作时,Couchbase Server 仅更新主索引,或称其为“真实的表”。所有的二级索引将被延迟更新。
虽然在 Couchbase Server 文档并没有明确说明,看上去它在构建索引时使用了快照,如果确是如此,脏读应该不成为问题。但是由于索引的延迟更新,在 Couchbase Server 中仍不能获得真正的提交读事务隔离级别。
和许多的 NoSQL 数据库一样,Couchbase Server 并不直接支持事务。但是你确实可以使用显式锁,但锁只能在被自动丢弃前维持 30 秒的时间。
更多的信息参见“对条目上锁”、“你所应知道的关于Couchbase 架构的所有事情”和“ Couchbase 视图引擎的内幕”。
Cassandra 中的事务隔离级别
Cassandra 1.0 隔离了甚至是对一行的写入操作。因为字段是被逐一更新的,所以可以终止对旧值和新值混合在一起的记录的读取。
从 1.1 版本开始,Cassandra 提供了“行级隔离”。这让 Cassandra 具有等同于其它的数据库中被称为“未提交读”的隔离级别。Cassandra 并未提供更高级别的隔离。
更多的信息参见“关于事务和并发控制”。
了解你的数据库的事务隔离级别
正如从上述实例中可看到的,仅从ACID 和非ACID 角度考虑你的数据库是不够的。你的确需要去知道你的数据库应在何种情况下支持何种的事务隔离级别。
关于作者
Jonathan Allen的首份工作是在上世纪九十年代末做诊所的 MIS 项目,Allen 将项目逐步由 Access 和 Excel 升级到企业级的解决方法。在从事为财政部门编写自动交易系统代码的工作五年之后,他成为项目顾问,参与了包括机器人仓库 UI、癌症研究软件中间层、主要房地产保险企业的大数据需求等在内的各种行业项目。在闲暇时间,他喜欢研究源于 16 世纪的武术,并为其撰写文章。
查看英文原文: A Quick Primer on Isolation Levels and Dirty Reads
感谢冬雨对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论