天天看点

《NoSQL权威指南》——1.4 悲观并发详解

本节书摘来自异步社区出版社《nosql权威指南》一书中的第1章,第1.4节,作者:【美】joe celko(乔•塞科) ,更多章节内容可以访问云栖社区“异步社区”公众号查看。

悲观并发控制假定冲突是预料之中的情况,必须警惕。在关系数据库管理系统(relational database management system,rdbms)中最流行的模型是基于加锁的。锁是一种允许一个用户会话对资源的访问同时保持或限制其他会话对同一资源的访问的装置。每个会话可以针对资源获得对应的锁,对资源进行修改,然后在数据库中<code>提交(commit)</code>或回滚(<code>rollback</code>)相应的操作。<code>commit</code>语句将修改持久保存,<code>rollback</code>语句将数据库恢复到会话之前的状态。如果修改遇到问题,系统也可以做一个rollback操作。这时,锁会被释放,其他会话可以访问对应的表或其他资源。

有各种各样的“加锁”,但sql的基本模型中有如下几种事务之间可以互相影响的方式。

p0(脏写)。事务t1修改一个数据项,此时另一个事务t2在事务t1执行commit或rollback之前也在修改同一数据项。如果t1或t2执行rollback,系统会不清楚正确的数据值应该是什么。脏写很不好,原因是这样会违反数据库的一致性。假设在x和y之间有约束(如x = y),并且如果t1和t2单独运行,它们会各自保持约束的一致性。但是,当两个事务以不同的顺序写入x和y时(这种情况只有允许脏写才会发生),约束就很容易被打破。

p1(脏读)。事务t1修改了一行数据,然后事务t2在t1执行<code>commit</code>之前读取该行数据。如果t1执行<code>rollback</code>,t2读到的行是从未被“提交”的,因此可以认为是根本不存在的。

p2(不可重复读)。事务t1读取了一行数据,事务t2修改或删除了该行数据,并执行<code>commit</code>操作。那么,如果t1尝试重读该行,则可能会收到修改后的值或者发现该行已被删除。

p3(幻读)。事务t1读取一组满足某些搜索条件的行n,然后事务t2执行了某些生成了满足事务t1(使用的)所有搜索条件的一行或多行。那么,如果事务t1以同样的搜索条件重新读,将会得到不同的行集合。

p4(丢失更新)。事务t1读取了一个数据项后,事务t2更新相关的数据项(可能基于先前读出的数据),然后事务t1(根据它较早读取的值)来更新数据项,并执行<code>commit</code>,这时会发生异常的更新丢失。

这些现象并不总是坏事。如果数据库仅用于查询,或是在工作日期间不会进行任何修改,那么就不会发生这些问题。如果数据库系统不需要设法保护自己避免这些问题,那么它将运行得更快。在某些情况下对数据进行更改也是可以接受的。

想象一下,有一张表,表中存储着世界上所有的汽车。我想执行一个查询,找到红色跑车司机的平均年龄。这个查询需要花一定的时间来运行,在此期间,汽车会报废、买入或卖出,新车会被生产出来,等等。但是,我接受符合p1~p3这3个现象的情况,因为平均年龄从我开始查询到完成查询的时候不会改变太多。第二位小数的变化并不重要。

可以通过设置事务隔离级别防止这些现象发生,这就是系统使用锁的方法。原始的ansi模型仅包括p1、p2和p3。其他定义最早出现在由hal berenson和他的同事(1995年)撰写的微软研究院技术报告msr-tr-95-51“ansi sql隔离级别的批判”中。

1.4.1 隔离级别

在标准sql中,用户可以在会话中设置事务的隔离级别。隔离级别可以避免刚才谈到的一些现象,并给数据库一些其他信息。下面是<code>set transaction</code>语句的语法:

可选的<code>&lt;diagnostics size&gt;</code>子句告诉数据库设置给定大小的错误信息列表。这是标准sql功能之一,因此在个别的产品中可能没有这个功能。其原因是,单条语句中可能存在多个错误,数据库引擎应该发现这些错误,并支持在宿主程序中通过<code>get diagnostics</code>语句在诊断区报告这些错误。

<code>&lt;transaction access mode&gt;</code>子句是自解释的。<code>readonly</code>选项表示这是一个查询,让sql引擎可以放松一些(不用考虑冲突等问题)。<code>read write</code>选项告知sql引擎,数据行可能会被修改,它必须注意上面提到的3个现象。

在当前大多数sql产品中都实现的重要子句是<code>&lt;isolation level&gt;</code>。事务的隔离级别定义了一个事务的操作允许受到并发事务影响的程度。事务的默认隔离级别是<code>serializable</code>,但用户可以在<code>set transaction</code>语句中明确地设置隔离级别。

每个隔离级别都确保每个事务将完全执行或完全不执行,而且不会有更新操作会丢失。当sql引擎检测到无法保证两个或两个以上并发事务的串行化时,或当它检测到发生了不可恢复的错误时,可以自行启动<code>rollback</code>语句。

来看一下表1-1。表1-1展示了隔离级别和3个现象。yes代表该现象在对应的隔离级别下是可能发生的。

《NoSQL权威指南》——1.4 悲观并发详解

在表1-1中:

<code>serializable</code>隔离级别是保证那些不得不并发执行的事务与它们在串行顺序执行情况下产生同样的结果。事务串行执行是指一个事务执行完成后才开始下一个事务执行。访问数据库的用户就像是在排队等候获得对数据库的完整访问。

<code>repeatableread</code>隔离级别是保证在用户会话期间为用户维护数据库的相同镜像。

<code>readcommitted</code>隔离级别允许当前会话中的事务能够读到会话运行期间其他事务已经提交的行。

<code>readuncommitted</code>隔离级别允许当前会话中的事务能够读到会话运行期间其他事务创建但不一定提交的数据。

不管事务隔离级别是哪种,在执行语句、检查完整性约束、执行与引用约束相关的引用操作等隐含读取模式(schema)定义期间,现象p1、p2和p3都是不应该出现的。我们不希望模式自己针对不同用户发生变化。

我们已经讨论了ansi/iso模型,但厂商往往会实现一些专有的隔离级别。我们需要知道它们的工作原理,以便在工作中使用这些产品。ansi/iso在会话级别为整个模式设置隔离级别。专有模型可能会允许程序员用额外的语法分配表级锁。微软公司使用的语法有这样的提示列表:

<code>select.. from &lt;base table&gt; with (&lt;hint list&gt;)</code>

该模型可以采用行级锁或表级锁。如果它们采用表级锁,可以得到与ansi/iso一致的特性。例如,with (holdlock)相当于<code>serializable</code>,但它仅适用于指定的表和视图,且只适用于由正在使用的语句定义的事务的运行期间。

用“读者”(reader)和“写者”(writer)的概念是解释各种模式的最简单的方法。读者和写者这两个名字无需解释。

在oracle中,写者是彼此阻塞的,数据将保持被锁定状态,直到<code>commit</code>、<code>rollback</code>或不保存数据停止会话为止。如果两个用户试图同时编辑同一数据,当第一个用户完成操作数据后,数据便被锁定。锁继续被保持,即使这个用户正在处理其他数据。

读者不会阻塞写者:读取数据库的用户不会在任何隔离级别阻止其他用户修改同一数据。但db2和informix有所不同。例如,在oracle中,写者会阻塞写者,但在db2和informix,写者在<code>uncommitted read</code>以上的任何隔离级别都会禁止其他用户读同一数据。在较高的隔离级别,锁定数据直到保存或回滚被编辑的数据,可能会导致并发问题。如果你正处在一个编辑会话中,其他任何会话都不能读你已锁定在编辑的数据。

读者阻塞写者:在db2和informix中,在<code>uncommitted read</code>以上的任何隔离级别读者都会禁止其他用户修改同一数据。应用程序在dbms中打开游标,每次读取一行,一边遍历结果集一边处理数据,只有在这样的应用程序中读者才能真正阻塞写者。在这种情况下,db2和informix开始获取锁,并在处理结果集的时候持有锁。

在postgresql中,直到更改该行的第一个事务被提交给数据库或回滚的时候,行才会被更新。当两个用户试图同时编辑同一数据时,第一个用户会阻塞其他用户更新该行。直到该用户保存了修改,提交了对数据库的更改,或在不保存的情况下停止该编辑会话(回滚所有在该编辑会话中进行的编辑操作),其他用户才能编辑该行。如果使用postgresql的多版本并发控制(mvcc)(这是默认和推荐的方式),写入数据库的事务操作不会阻塞读者查询该数据库。不论使用默认的<code>read committed</code>隔离级别还是设置隔离级别为<code>serializable</code>,这都是成立的。读者不会阻塞写者:无论在数据库中设置哪个隔离级别,读者都不锁定数据。