W
Y
Y
N
N
N
N
N
N
N
N
Y
N
下面我们通过一个实例来看一个如何根据这些表格来分析 DB2 中的锁机制。首先需要准备试验环境。
这里我们使用了 DB2 V9,实际上这些试验同样适用于 DB2 V8。
首先运行 db2cmd,初始化 DB2 CLP (命令行处理器)的运行环境,然后输入 db2,进入命令行处理器。创建数据库,连接到数据库,创建表格,以及插入两条试验数据,具体操作如 listing1:
清单 1. 创建数据库和表
db2 = > CREATE DATABASE TESTDB USING CODESET UTF-8 TERRITORY US COLLATE USING SYSTEM
DB20000I CREATE DATABASE命令成功完成。
db2 = > connect to TESTDB
数据库连接信息
数据库服务器 = DB2/NT 9.1.0
SQL 授权标识 = QKK
本地数据库别名 = TESTDB
db2 = > create table test (id varchar(2) not null, name varchar(20), primary key(id))
DB20000I SQL命令成功完成。
db2 = > insert into test values('1', 'a')
DB20000I SQL命令成功完成。
db2 = > insert into test values('2', 'b')
DB20000I SQL命令成功完成。
数据库连接信息
数据库服务器 = DB2/NT 9.1.0
SQL 授权标识 = QKK
本地数据库别名 = TESTDB
db2 => select * from test where id='2'
ID NAME
-- --------------------
2 b
1 条记录已选择。
db2 =>select * from test where id='1'
在第二个事务中,我们会发现能够读取出 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,因此不会发生脏读。试验过程如下图所示:
也就是说在只有游标操作方式的时候,才会在当前行加锁,象我们所做的只读操作,不会持续占用锁,读操作完成后锁就会被释放掉,不会等到事务结束,这样就可以解释在 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 能进行死锁检测,当发现死锁时,会中断并回滚其中一个事务,另一个事务就可以继续了。