重要要点
- 仅凭ACID或非ACID来思考,还需要知道数据库支持的隔离级别。
- 标榜为“最终一致”的某些数据库可能返回与任何时间点不一致的结果。
- 一些数据库提供的隔离级别比您要求的更高。
- 脏读会导致您看到同一记录的两个版本,或者完全错过一条记录。
- 在单个事务中多次重新运行查询时,可能会出现幻像行。
最近,当开发人员David Glasser了解MongoDB默认执行脏读的糟糕方式时,MongoDB再次成为Reddit的佼佼者。在本文中,我们将解释什么是隔离级别和脏读以及如何在流行的数据库中实现它们。
在ANSI SQL中,有四个标准隔离级别:可序列化,可重复读取,已提交读取和未提交读取。
许多数据库的默认设置为“读取已提交”,它仅保证在进行该事务时您不会看到过渡中的数据。它通过在读取期间短暂地获取锁来实现此目的,同时保持写入锁直到事务被提交。
如果您需要在一个事务中多次重复相同的读取操作,并且想要合理地确定它总是返回相同的值,则需要在整个持续时间内保持读取锁定。使用“可重复读取”隔离级别时,将自动为您完成此操作。
我们说“可重复读”是“合理肯定的”,因为可能存在“幻读”。使用where子句(例如“ WHERE Status = 1”)执行查询时,可能会发生幻像读取。这些行将被锁定,但是没有什么阻止添加符合条件的新行。术语“幻像”适用于第二次执行查询时出现的行。
为了绝对确保同一事务中的两次读取返回相同的数据,可以使用Serializable隔离级别。这使用“范围锁”,如果新行与打开的事务中的WHERE子句匹配,则可以防止添加这些行。
通常,隔离级别越高,由于锁争用而导致的性能越差。因此,为了提高读取性能,某些数据库还支持“读取未提交”。此隔离级别忽略锁(实际上在SQL Server中称为NOLOCK)。结果,它会执行脏读。
脏读问题
在讨论脏读之前,您必须了解表实际上并不存在于数据库中。表只是一个逻辑构造。实际上,您的数据存储在一个或多个索引中。在大多数关系数据库中,主索引被称为“聚集索引”或“堆”。(对于NoSQL数据库,术语有所不同。)因此,在执行插入操作时,它需要在每个索引中插入一行。执行更新时,数据库引擎仅需要触摸引用正在更改的列的索引。但是,它通常必须对每个索引执行两次操作,即从旧位置删除和向新位置插入。
在下图中,您可以看到一个简单的表和一个执行计划,其中更新了两个对象IX_Customer_State和PK_Customer。由于全名未更改,因此跳过了IX_Customer_FullName索引。
注意:在SQL Server中,PK前缀是指主键,它通常也是用于聚集索引的键。IX用于非聚集索引。其他数据库有其自己的约定。
通过这种方式,让我们看一下脏读可能导致数据不一致的多种方式。
未提交的读取最容易理解。通过忽略写锁定,使用“读未提交”的SELECT语句可以在事务完全提交之前看到新插入或更新的行。如果该转换然后被回滚,那么从逻辑上讲,SELECT操作将返回从不存在的数据。
在更新操作期间移动数据时,会发生两次读取。假设您正在按州读取所有客户记录。如果上述更新语句是在您加州记录的时间与您阅读德克萨斯州记录的时间之间执行的,则您可以看到客户1253两次;一次使用旧值,一次使用新值。
漏读的发生方式相同。如果我们将客户1253移到德克萨斯州到阿拉斯加,再按州选择数据,则可能会完全错过该记录。这就是David Glasser的MongoDB数据库所发生的事情。通过在更新操作期间从索引读取,查询会丢失记录。
根据数据库的设计方式和特定的执行计划,脏读也会干扰排序。例如,如果执行引擎收集指向所有感兴趣的行的一组指针,然后更新一行,然后执行引擎实际上使用所述指针从原始位置复制数据,则可能发生这种情况。
快照隔离或行级别版本控制
为了提供良好的性能同时避免脏读问题,许多数据库都支持快照隔离语义。在快照隔离下运行时,当前事务无法查看在当前事务之前启动的任何其他事务的结果。
这是通过制作要修改的行的临时副本来完成的,而不是仅仅依靠锁。这通常称为“行级版本控制”。
当请求读取提交隔离时,大多数支持快照隔离语义的数据库都会自动使用它。
SQL Server中的隔离级别
SQL Server支持所有四个ANSI SQL隔离级别以及一个显式的快照级别。取决于使用READ_COMMITTED_SNAPSHOT选项配置数据库的方式,“已提交读”也可以使用快照语义。
在启用此选项之前和之后,请彻底测试数据库。虽然它可以提高读取性能,但可能会减慢写入速度。如果您的tempdb处于慢速驱动器上,则尤其如此,因为这是行的旧版本存储的地方。
臭名昭著的NOLOCK指令(可应用于SELECT语句)与在设置为“读取未提交”的事务中运行具有相同的效果。由于SQL Server 2000和更早版本尚未提供行级版本控制,因此该版本已大量使用。尽管不再需要或不建议使用,但该习惯仍然存在。
有关更多信息,请参见SET TRANSACTION ISOLATION LEVEL(Transact-SQL)。
PostgreSQL中的隔离级别
虽然PostgreSQL正式支持所有四个ANSI隔离级别,但实际上它只有三个。每当查询请求“读取未提交”时,PostgreSQL都会以静默方式将其升级为“读取已提交”。因此PostgreSQL不允许脏读。
当选择级别Read Uncommitted时,您实际上会获得Read Committed,并且在Repeatable Read的PostgreSQL实现中不可能进行幻像读取,因此实际的隔离级别可能比您选择的严格。这是SQL标准所允许的:四个隔离级别仅定义了哪些现象一定不能发生,它们没有定义哪些现象必须发生。
PostgreSQL没有明确提供快照隔离。而是在使用“读取已提交”时自动发生。这是因为PostgreSQL从一开始就设计为具有多版本并发控制。
在9.1版之前,PostgreSQL不提供可序列化的事务,并且会静默地将它们降级为“可重复读”。当前没有支持的PostgreSQL版本仍然具有此限制。
有关更多信息,请参见13.2。事务隔离。
MySQL中的隔离级别
InnoDB默认为“可重复读取”,但提供所有四个ANSI SQL隔离级别。读取已提交使用快照隔离语义。
有关InnoDB的更多信息,请参见15.3.2.1事务隔离级别。
使用MyISAM存储引擎时,根本不支持事务。相反,它在表级别使用一个读写器锁。(尽管在某些情况下,插入操作可以绕过锁。)
Oracle中的隔离级别
Oracle仅支持3个事务级别:读已提交,可序列化和只读。在Oracle中,“默认值为读已提交”,它使用快照语义。
像PostgreSQL一样,Oracle不提供“读未提交”。绝对不允许脏读。
列表中还缺少“可重复读取”。如果您在Oracle中需要这种行为,则需要将隔离级别设置为Serializable。
Oracle唯一的隔离级别是只读。它没有很好的文档记录,手册只说:
只读事务仅查看那些在事务开始时提交的更改,并且不允许INSERT,UPDATE和DELETE语句。
有关其他两个隔离级别的更多信息,请参阅13数据并发性和一致性。
DB 2中的隔离级别
DB 2具有4个隔离级别,分别称为重复读取,读取稳定性,游标稳定性和未提交读取。但是,它们并不直接映射到ANSI术语。
可重复读是ANSI SQL称为可序列化的。也就是说,幻像读取是不可能的。
读取稳定性映射到ANSI SQL的可重复读取。
默认情况下,“游标稳定性”用于“读取已提交”。从9.7版开始,快照语义已生效。以前,它将使用类似于SQL Server的锁。
未提交读允许进行脏读,就像SQL Server的未提交读一样。该手册仅建议将其用于只读表,或者“在查看其他应用程序未提交的数据没有问题时”。
有关更多信息,请参见隔离级别。
MongoDB中的隔离级别
如前所述,MongoDB不支持事务。从手册中
由于MongoDB仅单文档操作是原子操作,因此两阶段提交只能提供类似于事务的语义。在两阶段提交或回滚期间,应用程序有可能在中间点返回中间数据。
实际上,这意味着MongoDB使用脏读语义,其中包括记录可能翻倍或丢失的可能性。
CouchDB中的隔离级别
CouchDB也不支持交易。但是与MongoDB不同,它确实使用多版本并发控制来防止脏读。
读取请求在请求开始时始终会看到您数据库的最新快照。
这使CouchDB等效于具有Snapshot语义的Read Committed隔离级别。
有关更多信息,请参见最终一致性。
Couchbase服务器中的隔离级别
尽管经常与CouchDB混淆,但Couchbase Server是一个非常不同的产品。对于索引,它没有隔离的概念。
在执行更新时,它仅更新主索引,如果您愿意,也可以更新“真实表”。所有二级索引均会延迟更新。
该文档尚不清楚,但在建立索引时似乎使用快照。如果是这样,脏读应该不是问题。但是由于延迟索引更新,您仍然无法获得真正的“读取已提交”隔离级别。
与许多NoSQL数据库一样,它不直接支持事务。但是,您确实可以使用显式锁。这些只能保留30秒,然后自动丢弃。
有关更多信息,请参阅锁定项目,您需要了解的有关Couchbase体系结构的所有信息以及Couchbase View Engine内部。
Cassandra的隔离级别
在Cassandra 1.0中,甚至没有隔离写入单个行。字段是一一更新的,因此您最终可能会读取包含新旧值的记录。
从1.1版开始,Cassandra提供“行级隔离”。这使其达到与其他数据库称为“读取未提交”的相同隔离级别。更高级别的隔离是不可能的。
有关更多信息,请参见关于事务和并发控制。
了解数据库的隔离级别
从上面的示例中可以看到,仅将数据库视为ACID或非ACID是不够的。您确实需要知道它支持什么隔离级别以及在什么情况下。