关键要点
仅从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角度考虑你的数据库是不够的。你的确需要去知道你的数据库应在何种情况下支持何种的事务隔离级别。 |