级别: 中级
齐克科 (qikeke@cn.ibm.com), 软件工程师, IBMElaine Zhan (ezhan@cn.ibm.com), 软件咨询工程师, IBM陈荔龙 (chenlil@cn.ibm.com), 软件工程师, IBM
2008 年 5 月 08 日
本文介绍一种数据库并发机制表格分析方法。借助表格,能够帮助理解、分析 DB2 的并发机制,并能对 DB2如何实现这种并发机制窥探一二。我们还将借助一些实例,来进行概念的验证和对理论更加深刻的理解。
基本概念
对于数据库初学者来说,深刻理解 DB2 里面的并发机制并不是一件很容易的事。事务、隔离级别、锁、锁兼容这些概念读起来十分晦涩,这些概念之间的关系似乎也是若隐若现,令人琢磨不透。
这里我们无意把所有的数据库并发机制相关概念再复述一遍,这不是本文的重点,但是很有必要在这里对一些重要的概念稍微复习一下,包括事务,隔离级别,锁,锁兼容等。对这些概念有大概的了解会对您阅读本文大有帮助。
事务
事 务在 DB2 中通常称为工作单元。工作单元是应用程序进程内可恢复的操作序列。数据库管理器使用它来确保数据库处于一致状态。对数据库的任何读取或写入都在一个工作单 元内执行。当对数据库发出第一条 SQL 语句时,就隐式启动了一个工作单元。由同一应用程序执行的所有后续读写操作被认为是同一工作单元的一部分,直到应用程序发出 COMMIT 或 ROLLBACK 语句来结束该工作单元。
隔离级别
隔离级别确定访问数据时如何锁定或者隔离该数据,以使其不受其他进程的影响。除非显式地进行更改,否则隔离级别在工作单元的运行期间保持有效。 由于多个用户访问和更改数据,所以必须维护关系数据库数据完整性。并发性指的是多个交互式用户或应用程序可以同时共享资源。隔离级别指定: 应用程序读取和更新的行可供其他并发执行的应用程序进程使用的程度;以及应用程序受其他并发执行的应用程序进程的更新活动的影响程度。
DB2 支持下列隔离级别:
- 可 重复读 (Uncommitted Read - UR):在工作单元内,应用程序读取的所有行都被锁定。因此,当游标处于打开状态时,在同一工作单元内发出两次的查询将返回相同的结果。在该工作单元完成 之前,其他应用程序无法更新、删除或插入可能会影响结果表的行。“可重复读”应用程序看不到其他应用程序的未落实更改。
- 读 稳定性 (Read Stability - RS):读稳定性只锁定应用程序在工作单元内检索的那些行。它确保在某个工作单元完成之前,在该工作单元运行期间的任何限定行读取不被其他应用程序进程更 改,且确保不会读取由另一个应用程序进程所更改的任何行,直至该进程落实了这些更改。
- 游标稳 定性 (Cursor Stability - CS):对于事务访问的行,只要游标定位在该行上,就锁定该行。此锁定在访存下一行或事务终止之前一直有效。但是,如果更改了某一行上的任何数据,则在对 数据库落实更改之前必须挂起该锁定。因此,在同一个工作单元内发出了两次的查询可能不会返回相同的结果。
- 未落实的读 (Uncommitted Read - UR) :应用程序可以访问其他事务的未落实更改。除非其他应用程序尝试删除或改变该表,否则该应用程序也不会锁定正读取的行而使其他应用程序不能访问该行。此隔离级别通常用于对只读表执行的查询。
锁
要提供并发性控制并防止无控制的数据访问,数据库管理器将锁定置于缓冲池、表、数据分区、表块或表行上。锁定将数据库管理器资源与应用程序关联(称为锁定所有者),以控制其他应用程序访问同一资源的方式。
锁的基本属性包括:object, size, duration, mode。如果您对这些属性所表达的含义不是特别清楚,建议您先读一下相关文章和 DB2 infocenter 里相关章节。我们在这里只是罗列出本文所需要的必要的信息。
锁的持续时间会根据隔离级别而有所不同:
- UR 扫描:除非行数据正在进行更改,否则不挂起行锁定。
- CS 扫描:仅当游标定位在行上时,才挂起行锁定。
- RS 扫描:事务期间只挂起符合查询条件的行锁定。
- RR 扫描:事务期间挂起所有行锁定。
锁状态(mode)
- IN: 无意向
- IS: 意向共享
- NS: 下一键共享
- S: 共享
- IX: 意向互斥
- SIX:在意向互斥下共享
- U: 更新
- NW: 下一键弱互斥
- X: 互斥
- W: 弱互斥
- Z: 超级互斥
锁 兼容:在一个应用程序当前拥有对某个对象的锁定,而另一个应用程序请求对同一个对象的锁定时,就会出现锁定兼容性问题。当两种锁定方式兼容时,可以同意对 该对象的第二个锁定请求。如果请求的锁定的锁定方式与已挂起的锁定不兼容,则不能同意锁定请求。相反,请求要等到第一个应用程序释放其锁定,并且释放所有 其他现有不兼容锁定为止。
表格分析法
也许读完上面的一些概念,还是会有许多疑问,诸如在不同的隔离级别下做不同的操作前需要满足那些条件,以及操作完成后对其他事务到底会产生什么影响?除了从文字上进行推敲,有没有其他的办法帮助我们分析和判断 DB2 的并行机制? 现在就让我们进入本文主题。本文将介绍一种表格分析法,借助两种表格,可以对使用不同隔离级别的事务进行不同操作时的行为进行精确分析和判断。 这里我们要引入两类特别重要的表,一张表是用来描述使用不同隔离级别在做不同的操所时需要获取的锁类型,另一张表描述的是锁类型的兼容性。
事务在不同的隔离级别中进行不同的操作,获取的锁类型也不同。比如在最高隔离级别 RR 中修改数据获取的锁 (S) 与 CS 级别中读数据获取的锁(NS) 就不一样。 另外访问方案也可对锁定方式具有很大影响。举例来说,当使用索引扫描来查找特定行时,优化器将可能为该表选择行级别锁定(IS);而如果不使用索引,则必须按顺序扫描整个表来找到所选的行,并且可因此获取单个表级锁定(S)。 因此获取的锁定的类型依赖于隔离级别、操作类型,以及访问方案诸多因素。为了简单起见,我们只对标准表,使用索引和全部扫描的访问方案来做分析。本文没有涉及集群表中的锁定以及被延迟的数据页访问方式。
表1描述了在不使用谓词 ( SQL 语句里的 where 部分) 进行表扫描的访问方式下,在不同隔离级别下做各种操作所要获取的锁定方式。
表 1. 不使用谓词的表扫描的锁定方式
隔离级别 | 只读和模糊扫描 | 游标-扫描 | 游标-当前行 | 更新或删除-扫描 | 更新或删除-更新行 |
RR | S/-- | U/-- | SIX/X | X/-- | X/-- |
RS | IS/NS | IX/U | IX/X | IX/X | IX/X |
CS | IS/NS | IX/U | IX/X | IX/X | IX/X |
UR | IN/-- | IX/U | IX/X | IX/X | IX/X |
表2描述了在使用谓词 ( SQL 语句里的 where 部分) 进行表扫描的访问方式下,在不同隔离级别下做各种操作所要获取的锁定方式。
表 2. 使用谓词的表扫描的锁定方式
隔离级别 | 只读和模糊扫描 | 游标-扫描 | 游标-当前行 | 更新或删除-扫描 | 更新或删除-更新行 |
RR | S/-- | U/-- | SIX/X | U/-- | SIX/X |
RS | IS/NS | IX/U | IX/X | IX/U | IX/X |
CS | IS/NS | IX/U | IX/X | IX/U | IX/X |
UR | IN/-- | IX/U | IX/X | IX/U | IX/X |
表3描述了在使用具有唯一性的索引来进行索引扫描的访问方式下,在不同隔离级别下做各种操作所要获取的锁定方式。
表 3. 使用单个合格行的 RID 索引扫描的锁定方式
隔离级别 | 只读和模糊扫描 | 游标-扫描 | 游标-当前行 | 更新或删除-扫描 | 更新或删除-更新行 |
RR | IS/S | IX/U | IX/X | IX/X | IX/X |
RS | IS/NS | IX/U | IX/X | IX/X | IX/X |
CS | IS/NS | IX/U | IX/X | IX/X | IX/X |
UR | IN/-- | IX/U | IX/X | IX/X | IX/X |
在上面两张表中,每一项由两部分组成: 表锁定和行锁定,横线代表没有该对象上的锁定。
表4显示锁定之间的兼容关系。Y 表示锁定兼容,即当一个进程挂起或正在请求对同一个资源的锁定时,可以同意锁定请求。N 表示锁定不兼容,请求者必须等待,直到所有不兼容的锁定被其他进程释放为止。
表 4. 锁定类型兼容性
无 | IN | IS | NS | S | IX | SIX | U | X | Z | NW | W | |
无 | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
IN | Y | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y |
IS | Y | Y | Y | Y | Y | Y | Y | Y | N | N | N | N |
NS | Y | Y | Y | Y | Y | N | N | Y | N | N | Y | N |
S | Y | Y | Y | Y | Y | N | N | Y | N | N | N | N |
IX | Y | Y | Y | N | N | Y | N | N | N | N | N | N |
SIX | Y | Y | Y | N | N | N | N | N | N | N | N | N |
U | Y | Y | Y | Y | Y | N | N | N | N | N | N | N |
X | Y | Y | N | N | N | N | N | N | N | N | N | N |
Z | Y | N | N | N | N | N | N | N | N | N | N | N |
NW | Y | Y | N | Y | N | N | N | N | N | N | N | Y |
W | Y | Y | N | N | N | N | N | N | N | N | Y | N |
下面我们通过一个实例来看一个如何根据这些表格来分析 DB2 中的锁机制。首先需要准备试验环境。
这里我们使用了 DB2 V9,实际上这些试验同样适用于 DB2 V8。
首先运行 db2cmd,初始化 DB2 CLP (命令行处理器)的运行环境,然后输入 db2,进入命令行处理器。创建数据库,连接到数据库,创建表格,以及插入两条试验数据,具体操作如 listing1:
清单 1. 创建数据库和表
|
然 后我们使用两个 CLP 作为两个独立的进程,分别运行事务,来观察和分析 DB2 中的锁机制。两次运行 db2cmd,打开两个 DB2 的命令运行环境,然后输入 db2 +c,进入命令行处理器。加参数 +c 是为了保证在 DB2 CLP 中的 SQL 语句不自动提交,这种方式下,只有输入命令 commit 或者 rollback 才会结束一个事务。
在第一个 CLP 中运行
清单 2. 事务处理一
|
我们设定了事务的隔离级别为读稳定 (RS),并且修改了一条数据。注意这里没有 commit,因为我们不想结束这个事务。
在第二个 CLP 中,运行
清单 3. 事务处理二
|
在第二个事务中,我们会发现能够读取出 id='2' 的数据,但是当查询 id='1' 的数据时,没有数据输出,说明发生了锁等待。
下面我们来分析一下:事务一执行 update 操作时,因为在 where 语句中使用的是name='a'
,没有包括键值,所以数据库管理器在进行操作时会进行全表扫描。 我们就应该看使用谓词的表扫描的锁定方式表格,从表 2 中可以判断,在 RS 级别下,更新的行(被修改的第一行)加 X 锁,更新-扫描行(第二行)加 U 锁。
事务二在执行select * from test where id='2'
时,where 语句中使用主键值,对于主键是建有索引的,因此根据表 3 (唯一匹配的索引扫描锁定表)来判断需要获取的锁模式)。 从表中可以看出,RS 级别下被扫描的行(只有第二行)需要申请 NS 锁。第二行已经被事务一加了 U 锁,那么事务二能否获取到 NS 锁呢,这就要看 U 锁和 NS 锁是否兼容。 这时需要查看锁的兼容表。从表4中可以看出,U 和 NS 是兼容的,因此事务二可以获取锁,能够完成查询操作。
当事务二执行select * from test where id='1'
时,从表 3 中查出,被扫描的第一行需要 NS 锁,与事务一加的 X 锁不兼容,因此发生锁等待。 这时只有第一个事务运行 commit 或者 rollback,事务一结束,X 锁被释放,第二个事务才可以获取到 NS 锁,得到查询结果。
那么如果在事务二中运行select * from test where name='b'
呢?这条语句是要查询的第二条记录,需要的 NS 锁与事务一在这条记录上的U锁是兼容的,应该不会发生锁等待,可以得到结果。事实是不是这样呢? 做一下试验就会发现,也是需要等待的,为什么呢?仔细想想就会发现,语句select * from test where name='b'
并没有使用索引,而是做全表扫描,从 table 1 中可以得到,在隔离级别为 RS 的事务中,所有被扫描的行都需要 NS 锁,包括第一行,与第一行记录已经有的 X 锁是不兼容的,因此当扫描到第一行时就需要锁等待了。
试验
热身完毕,我们已经基本了解如何使用获取锁表和锁兼容表来分析事务的行为了。下面我们针对各个隔离级别分别设计一些试验来理解隔离级别的意义。
未提交的读隔离级别 (UR) 是最不严格的隔离级别。实际上,在使用这个隔离级别时,仅当另一个事务试图删除或更改被检索的行所在的表时,才会锁定一个事务检索的行。 因为在使用这种隔离级别时,行通常保持未锁定状态,所以脏读、不可重复的读和幻像都可能会发生。下面我们就设计几个试验看如何发生的脏读,不可重复的读以及幻像。
试验一:UR下的脏读
当事务读取尚未提交的数据时,就会发生脏读。例如:事务 1 更改了一行数据,而事务 2 在事务 1 提交更改之前读取了已更改的行。如果事务 1 回滚该更改,则事务 2 就会读取被认为是不曾存在的数据。我们用试验来感受一下。
首先在事务一中执行connect reset; change isolation to UR; connect to testdb
来把事务的隔离级别修改为UR。 第二个事务可以采用任何隔离级别,只需要保证在第二个窗口中采用不自动提交方式进入 CLP。在第二个事务中运行 update test set name='abc' where id='01'
, 然后在第一个事务中运行 select * from test
。这里读取的数据是第二个事务已经修改过的,但是还没有提交的数据。在第二个事务运行 rollback,取消刚才的修改, 在第一个事务运行 select * from test
,发现读取的数据又变成了修改前的数据。
图 1. UR 下的脏读(事务一)
图 2. UR 下的脏读(事务二)
事务一的第一次读,读到的就是脏数据。对照表格稍微分析一下就可以得知,发生脏读原因就是 UR 事务读操作的时候不对行进行的锁定,这样一方面事务本身读数据时不受约束,同时由于不对数据进行锁定,那么使得其他事务修改时也就不受 UR 事务的约束了。
游标稳定性隔离级别在隔离事务效果方面非常宽松。它可以防止脏读;但有可能出现不可重复的读和幻像。
试验二:CS 下如何防止脏读
把事务一的隔离级别改为 CS,然后重复上面的试验,会发现在事务二修改数据以后,在事务二提交以前,事务一去读修改的数据时会发生锁等待,直到事务二 commit 或者 rollback,因此不会发生脏读。试验过程如下图所示:
图 3. CS 下防止脏读(事务一)
图 4. CS 下防止脏读(事务二)
还是利用我们的表格分析法,从表中可以看出,无论采用什么访问方式,在 CS 隔离级别下,读取数据时需要在行上加 NS 锁,如果此时数据上己经有其他事务加了X锁(被修改时加X锁),由于 NS 锁与 X 锁的不兼容,使得读数据的时候一定发生锁等待。
试验三:CS 下不可重复的读
CS 可以防止脏读,但是可能出现不可重复的读和幻像。如下图试验所示:事务一读完数据后,事务二修改数据(或者添加数据),并且提交,那么事务一再去读数据, 会发生同一条记录跟上一次读的结果不一样,这就是不可重复的读(也会发生读出上一次没有查询到的记录,这就是幻像)。
图 5. CS 下不可重复的读(事务一)
图 6. CS 下不可重复的读(事务二)
但 是如果我们仔细按照表格分析就会产生这样的疑问:当第一个事务读取数据以后,按照表格1,数据应该加 NS 锁,事务二在修改数据的时候,需要在被修改数据行上加X锁,表 4 中看出,X 锁与 NS 锁不兼容,事务二应该是锁等待阿,然而情况并非如此。从试验中可以看出,事务二在修改数据时没有等待。 不过这确实符合 CS 隔离级别的描述,CS 级别下,只读取已经提交了数据,但是读取并不会影响其他事务对数据的修改。但是与表格分析得出的结果矛盾,原因在哪儿呢? 我们可以借助 DB2 活动监视器来分析一下。我们先来复现一下事务一读取数据以后的状态。首先在通过 commit 来结束事务一,然后重新执行 select 语句以恢复到读取后的状态。 打开 DB2 活动监控器,选择数据库 testdb,然后在检视任务中选择正在解决应用程序锁定状态,点击完"成看"。 在报告栏中选择"具有最大锁定超时数的应用程序",找到第一个 CLP 对应的程序,右击,选择显示锁定链,如下图:
图 7. 显示锁定链
在应用程序锁定链窗口中,右击程序,选择显示锁定详细信息。
图 8. 显示锁定详细信息
在详细信息窗口中,会列出此程序已经拥有的锁定和等待的锁定。在这里面,我们并没有发现针对行对象的 NS 锁。 我们分别在 CS 级别和 RS 级别下做相同的查询操作,然后比较它们的锁定状况,就会更清楚地发现它们的不同,尽管在表 1 和表 2 里 CS 和 RS 看起来是一样的。
图 9. CS 级别锁定状况
图 10. RS 级别锁定状况
什么原因呢?是不是表 1 和表 2 把锁模式弄错了,CS 隔离级别下读操作也不需要 NS 锁?应该也不是,因为如果我们让事务二先修改数据,再让事务一去读,就回发生锁等待,因此事务一读数据时,还是需要行的 NS 锁,否则象 UR 级别一样,也会发生脏读。 回头看一下我们在基本概念一节中的这样一段话:
锁的持续时间会根据隔离级别而有所不同:
- CS 扫描:仅当游标定位在行上时,才挂起行锁定。
也就是说在只有游标操作方式的时候,才会在当前行加锁,象我们所做的只读操作,不会持续占用锁,读操作完成后锁就会被释放掉,不会等到事务结束,这样就可以解释在 CS 级别下,为什么对数据的读取不会阻塞其他事务对该数据的修改了。
读稳定性隔离级别:读稳定性隔离级别可以防止脏读和不可重复的读,但是可能出现幻像。在使用这个隔离级别时,只锁定事务实际检索和修改的行。
试验四:RS 下防止不可重复的读
将事务一的隔离级别改为 RS,然后执行select * from test where id='1'
读取数据; 事务二执行update test set name='abc' where id='1'
来修改数据,会发现事务二发生锁等待。 事实上,无论第二个进程使用何种隔离级别,现象是一样的。还是用锁兼容表分析一下。在事务一读数据时,因为是索引扫描,因此从表 3 中得出,只是在被检索的数据上加 NS 锁(不同于 CS,锁会持续到事务结束), 事务二在做修改的时候,获取 X 锁,与 NS 不兼容,因此锁等待,直至事务一结束。这样就保证了在事务一的操作过程中,被检索的行不会被其他进程修改,每次读到的同一条记录都不会有变化,从而保证了读稳定。
图 11. RS 下防止不可重复的读(事务一)
图 12. RS 下防止不可重复的读(事务二)
试验五:RS 下的幻像
那么如何发生的幻像呢?事务一读取数据后,在事务二中插入一条数据insert into test values('3', 'c')
并且提交, 再在事务一中执行select * from test
, 就会发现与前一次 select 结果相比,多了一条数据,这就是幻像。
图 13. RS 下的幻像(事务一)
图 14. RS 下的幻像(事务二)
可重复读隔离级别:可重复读隔离级别是最严格的隔离级别。在使用它时,一个事务的影响完全与其他并发事务隔离:脏读、不可重复的读、幻像都不会发生。我们现在就来验证一下。
试验六:RR 下防止幻像
事务一隔离级别改为 RR,然后执行select * from test
, 事务二执行insert into test values('4', 'd')
,会发现事务二发生锁等待。 还是用表格分析法,事务一读取数据时,从表1可以看出,在表上加了 S 锁,事务二插入数据时需要在表上申请 X 锁,X 与 S 不兼容,因此进程二只能锁等待。
图 15. RR 下防止幻像(事务一)
图 16. RR 下防止幻像(事务二)
这样我们就会很自然地发现,隔离级别越高,操作越安全,但是发生锁等待的机会就越大,效率会降低,甚至会发生死锁。下面我们再来设计一个死锁的例子。
两个进程中的事务都采用 RS (读稳定)的隔离级别,事务一执行select * from test where id='1'
, 事务二执行select * from test where id='2'
, 事务一执行update test set name='bb' where id='2'
, 事务二执行update test set name='bb' where id='1'
。 这种情况下,就会发生事务一在执行 update 的时候等待事务二的 NS 锁被释放,而事务二执行 update 的时候也会等待事务一的 NS 锁别释放,这样就发生了死锁。 好在 DB2 能进行死锁检测,当发现死锁时,会中断并回滚其中一个事务,另一个事务就可以继续了。
图 17. RR 下防止幻像(事务三)
图 18. RR 下防止幻像(事务四)
总结
我们已经通过试验的方法,掌握了如何运用表格来分析和判断事务在不同的隔离级别下,执行不同的操作时的行为,从而能够更加深刻理解 DB2 的并发机制。 您可以用这种方法来对您的数据库并发操作方案进行分析和评估,来判断是否满足实际需求。但是要指定好的并发操作方案,还是需要对数据库的并发机制有着融会贯通的理解。