简介: OLTP 数据库通常是高并发的使用模式,具有高的并发性对 OLTP 系统来说至关重要,并发事务实际上取决于资源的使用状况,原则上应尽量减少对资源的锁定时间,减少对资源的锁定范围,从而能够尽量增加并发事务的数量,那么影响并发的因素有哪些呢?这篇系列文章分 2 部分就这个内容进行一些探讨,同时会尽量使用不同的工具(数据库快照、事件监视器、控制中心等)来演示如何监控资源的使用状况,让大家在获得知识的同时,也可以熟悉各种工具的使用。
<script type="text/javascript"></script><!-- Rating_Area_End -->
<!-- dW_Summary_Area_END --><!-- CONTENT_BODY -->
OLTP 数据库通常是高并发的使用模式,具有高的并发性对 OLTP 系统来说至关重要,并发事务实际上取决于资源的使用状况,原则上应尽量减少对资源的锁定时间,减少对资源的锁定范围,从而能够尽量增加并发事务的数量,那么影响并发的因素有哪些呢?这篇系列文章之一将从 DB2 数据库本身所提供的机制对并发性的影响进行一些探讨,这些机制包括隔离级别、数据库锁参数、实例注册表变量、乐观锁定。
同时本文讨论及实验所基于的环境如下,见图 1:
设置 DB2 注册表变量 DB2OPTIONS,将自动提交功能关闭,后面的实验默认都基于这个设置。
db2set DB2OPTIONS=+c db2 terminate db2stop db2start |
当多个用户访问同一数据库时会发生的现象介绍如下:
在单用户环境中,每个事务都是顺序执行的,而不会遇到与其他事务的冲突。但是,在多用户环境下,多个事务可以(而且常常)同时执行。因此每个事务都有可能与其他正在运行的事务发生冲突。有可能与其他事务发生冲突的事务称为交错的 或并行的 事务,而相互隔离的事务称为串行化 事务,这意味着同时运行它们的结果与一个接一个连续地运行它们的结果没有区别。在多用户环境下,在使用并行事务时,会发生四种现象:
维护数据库的一致性和数据完整性,同时又允许多个应用程序同时访问同一数据,这样的特性称为并发性。 DB2 数据库用来尝试强制实施并发性的方法之一是通过使用隔离级别——通过‘事务、隔离级别、锁’机制,它决定在第一个事务访问数据时,如何对其他事务锁定或隔离该事务所使用的数据。 DB2 使用下列隔离级别来强制实施并发性:
可重复的读隔离级别可以防止所有现象,但是会大大降低并发性的程度(可以同时访问同一资源的事务数量)。未提交的读隔离级别提供了最大的并发性,但是后三种现象都可能出现。
DB2 UDB 支持以下隔离级别:可重复读(Repeatable read,RR)、读稳定性(Read stability,RS)、游标稳定性(Cursor stability,CS)、未提交读(Uncommitted read,UR),下面分别讲述:
可重复读(Repeatable read,RR) 确保 工作单元(UOW)期间的任何表行读操作直到 UOW 完成,不会被其他应用程序进程更改。类似地,由另一个应用程序进程更改的任何行直到由该应用程序进程提交,不会被读取。运行在 RR 级别的应用程序进程是完全与并发应用程序进程的效果相隔离的。 |
RR 隔离级别通常会直接对表加 S 锁,所以对并发的影响最大,但有一种情况例外:如果 WHERE 条件字段上建有主键或者 UNIQUE INDEX,并且通过主键或者 UNIQUE INDEX 进行查询,那么数据库将只对表加 IS 锁,结果行加 S 锁——在锁列表足够用,没有发生锁升级的情况下才成立,也就是说,这个时候 RR 级别 =RS 级别,这时不允许其他事务对这些行进行更新或者删除,因为对行的更新或者删除会对相应的行加 X 锁,这和行 S 锁相排斥;其他情况下,会直接对表加 S 锁,这时将不允许其他事务对任何行进行更新或者删除。
下面我们做个实验,验证使用RR隔离级别的查询对表的加锁情况。
表名 | 表结构 | 主键/ UNIQUE INDEX |
数据规模 |
TEST_HAVE_PRI | A 列上建有主键 | 10 行 | |
TEST_NO_PRI | 无主键和 UNIQUE INDEX |
同样的 10 行数据 |
使用 RR 隔离级别对 TEST_HAVE_PRI 表进行查询,见图 2:
可以看到表 TEST_HAVE_PRI 上加了 IS 锁,行上加了 S 锁,这是通过主键查询时锁的使用情况,见图 3:
通过主键字段查询后,被 RR 隔离级别锁定的行,不能够被其他事务更新(UPDATE/DELETE),session2 中的 UPDATE 事务将被锁住,见图 4:
通过 DB2 控制中心工具可以查看锁定链条——即谁锁定了谁,打开‘应用程序’窗口,可以看到选中的应用程序状态为‘正在等待锁定’——就是 session2 中的事务的状态,见图 5:
点击‘显示锁定链’按钮,进入下一窗口,下图如何解释呢?
请点击‘图注’按钮,先了解一下各种图形元素所代表的意思,见图 7:
根据图 7 所示,图 6 所表示的意思为:下方框事务正在等待上方框事务释放锁定,也就是 session2 中的事务正在等待 session1 中的事务释放锁定。
更详细的信息,可以通过右键点击方框,选择‘显示锁定详细信息’菜单,见图 8:
下图为 session1 中的查询事务获得的锁的详细信息——获得一个表 IS 锁、一个行 S 锁,见图 9:
图 10 为 session2 中的更新事务获得的锁的详细信息,这个事务处于正在等待锁定 状态,获得了一个表 IX 锁,同时想要获得行 X 锁,但是不成功,于是发生了锁等待,与句柄为 901 的代理程序(即运行查询事务的 session1 应用)中的行 S 锁冲突(见图 9),所以只能等待,直到句柄为 901 的代理程序提交或者回滚工作单元,才能真正获得行 X 锁。
表中其他的行可以被其他事务更新,见图 11:
使用 RR 隔离级别对 TEST_NO_PRI 表进行查询,让我们看看不通过主键字段查询时锁的使用情况,见图 12:
可以看到表 TEST_NO_PRI 上直接加了 S 锁,其他任何事务不能对此表做任何更新操作 (DELETE/UPDATE/INSERT),见图 13:
读稳定性(Read stability,RS)类似于 RR 。但是,运行在 RS 级别的应用程序进程不是完全与并发应用程序进程的效果相隔离的。如果这样的应用程序进程不止一次发出同样的查询,它就会看到更改了的数据或者由其他应用程序进程添加的新的“幻影(phantom)”行。 |
RS 隔离级别会对表加 IS 锁,结果行加 NS 锁,这时不允许其他事务对这些行进行更新(UPDATE/DELETE),但是允许插入任何行,因为对这些行的更新会对相应的行加 X 锁,这和行 NS 锁相排斥——上述说法基于锁列表足够用,没有发生锁升级的情况下才成立。
下面我们做个实验,验证使用RS隔离级别的查询对表的加锁情况:
表名 | 表结构 | 索引 |
TEST | A 列上建有索引 |
使用 RS 隔离级别对 TEST 表查询,见图 14:
可以看到表 TEST 上加了 IS 锁,行上加了 NS 锁(存在 32768 个行 NS 锁,这里不一一列举),见图 15:
预更新(UPDATE/DELETE)这些行的事务将被锁住,见图 16、图 17:
但是可以插入任何行,见图 18:
游标稳定性(Cursor stability,CS)也确保由另一个应用程序进程更改的任何行直到被那个应用程序进程提交,不会被读取。 CS 隔离级别只确保每个可更新游标的当前行不被其他应用程序进程更改;在 UOW 期间读过的行可以被其他应用程序进程更改。针对可滚动更新游标,在提交之前,会对所有结果行一直加 U 锁,无论游标滚动到什么地方; CS 隔离级别针对不可更新游标会对表加 IS 锁,如果未在 WHERE 条件字段上创建索引,查询首先会查找锁列表,检查锁列表中是否存在与 IS 锁相排斥的锁,如果存在的话,那么将等待所有持有排斥锁的事务提交,查询才能执行下去;如果 WHERE 条件字段创建了索引,并且使用了索引,那么查询将通过索引得到结果行,然后检查锁列表中是否存在与结果行相排斥的锁,如果存在的话,那么将等待所有持有排斥锁的事务提交,查询才能执行下去 . |
下面我们做个实验,验证使用CS隔离级别的查询对表的加锁情况:
使用 CS 隔离级别的可更新游标对 TEST_HAVE_PRI 表进行查询,并 FETCH 第 1 行(见图 19),使用的数据模型见表1:
可以看到表 TEST_HAVA_PRI 上加了 IX 锁,被取的当前行 (a= ’ 1 ’ ) 加了 U 锁,符合条件的其他行暂时不加锁,见图 20:
这时其他事务不可以对这行进行更新操作 (UPDATE/DELETE) ——因为行上的 U 锁与 X 锁排斥,但是可以对其他行进行更新(比如对 a= ’ 3 ’这行),见图 21、图 22:
转到 session1 窗口,继续 FETCH 下一行,这时将释放前一行 (a= ’ 1 ’ ) 上的 U 锁,在当前行上 (a= ’ 2 ’ ) 加 U 锁,见图 23:
转到 session2 窗口,可以看到之前被锁住的 UPDATE 语句已经执行成功,见图 24:
在 session4 窗口对 a= ’ 2 ’这行进行 UPDATE 操作,可以预见将被锁住,见图 25:
转到 session1 窗口,继续 FETCH 下一行,将释放 a= ’ 2 ’这行上的 U 锁(session4 中对 a= ’ 2 ’这行的 UPDATE 事务将成功执行,为什么呢?),将要在 FETCH 的下一行上加 U 锁,发现 session1 中的游标 FETCH 操作被锁住,这又为什么呢?
因为 session1 中的事务释放了 a= ’ 2 ’上的 U 锁,所以 session4 中被锁住的针对 a= ’ 2 ’这条记录进行的 UPDATE 事务能够执行成功,见图 26:
为什么 session1 中的事务(FETCH 下一条记录)被锁住呢?是因为这次恰好要 FETCH a= ’ 3 ’这条记录,需要在行上加 U 锁,这与 session3 中的 update 事务对这行所加的 X 锁相排斥,所以 session1 窗口的 FETCH 下一行操作发生了锁等待,直到 session3 中的事务提交或者回滚为止,见图 27:
未提交读(Uncommitted read,UR)对于某些操作,允许在 UOW 期间读过的任何行可以被其他应用程序进程更改,并允许读任何被另一个应用程序进程更改过的行,即使该更改还没有提交。对于其他操作,UR 类似于 CS 。 |
下面我们做个实验,验证使用UR隔离级别的查询是不会获得任何级别锁的:
表名 | 表结构 | 索引 |
TEST_NO_IND | 没有索引 |
采用 UR 隔离级别查询 TEST_NO_IND 表,同时运行快照,可见,使用 UR 隔离级别查询时,对表 TEST_NO_IND 加了 IN 锁,IN 就是 intent none 意思,就是不加任何锁,见图 28、图 29:
综上所述,离级别对并发性具有最显著的影响,不同隔离级别获得的资源的锁定范围也不同,如果所有事务都能做到不过分贪婪的占有锁资源——锁的范围大、占用时间长,那么事务之间发生锁冲突的可能性将大大降低,事务的并发性也将会很好。那么如何选择正确的隔离级别呢?
使用的隔离级别不仅影响数据库对并发性的支持如何,而且影响并发应用程序的性能。通常,使用的隔离级别越严格,并发性就越小,某些应用程序的性能可能会越低,因为它们要等待资源上的锁被释放。那么,如何决定要使用哪种隔离级别呢?最好的方法是确定哪些现象是不可接受的,然后选择能够防止这些现象发生的隔离级别:
当应用程序挂起的锁定总数达到可供应用程序使用的最大锁定列表空间量时,锁定将会升级,将影响到应用程序并发性。可用锁定列表空间量由 locklist 和 maxlocks 配置参数确定。
当应用程序达到允许的最大锁定数并且没有其他要升级的锁定时,它将使用锁定列表中为其他应用程序分配的空间。当整个锁定列表已满时,将发生错误。
以下几种原因可能会导致产生过量锁定升级:
要解决这些问题,可以:
locks_held_top
监视器元素)的应用程序,或借助以下公式并将该值与maxlocks
进行比较以标识在锁定列表中挂起过量锁定的应用程序:(((locks held * 36) ⁄ (locklist * 4096)) * 100) |
这些应用程序还可能因为在锁定列表中使用过量资源而导致其他应用程序中发生锁定升级。这些应用程序可能需要求助于使用表锁定(而不是行锁定),尽管表锁定可能导致lock_waits
和lock_wait_time
增加。
锁参数(LOCKLIST、MAXLOCKS 和 LOCKTIMEOUT)背景知识
这些与锁相关的控制都是数据库配置参数:
LOCKLIST
表明分配给锁列表的存储容量。每个数据库都有一个锁列表,锁列表包含了并发连接到该数据库的所有应用程序所持有的锁。锁定是数据库管理器用来控制多个应用程序并发访问数据库中数据的机制。行和表都可以被锁定。根据对象是否还持有其它锁,每把锁需要的锁列表字节数不一样:在 32 位平台上,每个锁需要 48 或 96 字节的锁定列表,这取决于是否对该对象挂起了其他锁:
96
字节48
字节在64
位平台(HP-UX/PA-RISC
除外)上,每个锁需要64
或128
字节的锁定列表,这取决于在该对象上是否挂起了其他锁定:
128
字节64
字节。MAXLOCKS
定义了应用程序持有的锁列表的百分比,在数据库管理器执行锁升级之前必须填充该锁列表。当一个应用程序所使用的锁列表百分比达到MAXLOCKS
时,数据库管理器会升级这些锁,这意味着用表锁代替行锁,从而减少列表中锁的数量。当任何一个应用程序所持有的锁数量达到整个锁列表大小的这个百分比时,对该应用程序所持有的锁进行锁升级。如果锁列表用完了空间,那么也会发生锁升级。数据库管理器通过查看应用程序的锁列表并查找行锁最多的表,来决定对哪些锁进行升级。如果用一个表锁替换这些行锁,将不再会超出MAXLOCKS
值,那么锁升级就会停止。否则,锁升级就会一直进行,直到所持有的锁列表百分比低于MAXLOCKS
。MAXLOCKS
参数乘以MAXAPPLS
参数不能小于100
。虽然升级过程本身并不用花很多时间,但是锁定整个表(相对于锁定个别行)降低了并发性,而且数据库的整体性能可能会由于对受锁升级影响的表的后续访问而降低。
下面是一些控制锁列表大小的建议:
SQL LOCK TABLE
语句)。这只使用了一把锁从而防止其它事务妨碍这些更新,但是对于其他用户它的确减少了数据并发性。alter TABLE
语句的LOCKSIZE
参数控制如何在持久基础上对某个特定表进行锁定。Cursor Stability
)隔离级别。如果没有损害应用程序完整性需求,那么可以使用未提交的读隔离级别而不是游标稳定性隔离级别,以进一步减少锁的数量。使用下列步骤确定数据库锁列表所需的页数:
(512 * 32 * MAXAPPLS) / 4096
,其中512
是每个应用程序平均所含锁数量的估计值,32
是对象(已有一把锁)上每把锁所需的字节数。(512 * 64 * MAXAPPLS) / 4096
,其中64
是某个对象上第一把锁所需的字节数。使用数据库系统监视器调优MAXLOCKS
值。
设置MAXLOCKS
时,请考虑锁列表的大小(LOCKLIST
):
MAXLOCKS = 100 * (512
锁/应用程序* 32
字节/锁* 2) / (LOCKLIST * 4096
字节)
该样本公式允许任何应用程序持有的锁是平均数的两倍。如果只有几个应用程序并发地运行,则可以增大MAXLOCKS
,因为在这些条件下锁列表空间中不会有太多争用。
LOCKTIMEOUT
指定了应用程序为获取锁所等待的秒数。这有助于应用程序避免全局死锁。
0
,那么应用程序将不等待获取锁。在这种情形中,如果请求时没有可用的锁,那么应用程序立刻会接收到-911
。-1
,那么将关闭锁超时检测。在这种情形中,应用程序将等待获取锁(如果请求时没有可用的锁),一直到被授予了锁或出现死锁为止。要更改锁参数,请运行以下命令:
db2 -v update db cfg for DB_NAME using LOCKLIST a_number db2 -v update db cfg for DB_NAME using MAXLOCKS b_number db2 -v update db cfg for DB_NAME using LOCKTIMEOUT c_number db2 -v terminate |
一旦锁列表满了,由于锁升级生成更多的表锁和更少的行锁,因此减少了数据库中共享对象的并发性,从而降低了性能。另外,应用程序间可能会发生更多死锁(因为它们都等待数量有限的表锁),这会导致事务被回滚。当数据库的锁请求达到最大值时,应用程序将接收到值为-912
的SQLCODE
。如果锁升级造成并发性方面的问题,则可能需要增大LOCKLIST
参数或MAXLOCKS
参数的值。可以使用数据库系统监视器来确定是否发生锁升级,跟踪应用程序(连接)遭遇锁超时的次数,或者数据库检测到的所有已连接应用程序的超时情形。
DB2
监视器:db2 -v update monitor switches using lock on db2 -v terminate |
db2 -v get snapshot for database on DB_NAME |
Locks held currently = 0 Lock waits = 0 Time database waited on locks (ms) = 0 Lock list memory in use (Bytes) = 504 Deadlocks detected = 0 Lock escalations = 0 Exclusive lock escalations = 0 Agents currently waiting on locks = 0 Lock Timeouts = 0 |
如果“Lock list memory in use (Bytes) ”
超过定义的LOCKLIST
大小的50%
,那么就增加LOCKLIST
数据库配置参数中的4KB
页的数量。锁升级、锁超时和死锁将表明系统或应用程序中存在某些潜在问题。锁定问题通常表明应用程序中存在一些相当严重的并发性问题,在增大锁列表参数的值之前应当解决这些问题。
从事务双方的锁定关系上来讲有死锁和活锁 2 种,所谓活锁:举例来说可能会存在事务 A 在等待事务 B 释放所占用的锁资源,但是事务 B 并不等待事务 A 释放所占用的锁资源,所谓死锁:举例来说可能会存在事务 A 在等待事务 B 释放所占用的锁资源,同时事务 B 在等待事务 A 释放所占用的锁资源,也就是说,事务之间互相等待,如果没有合理设置检查死锁的时间间隔 (DLCHKTIME) 参数,可能会导致相关事务双方永远等待下去,DLCHKTIME 参数默认设置为 10000 毫秒——即 10 秒,也就是说每隔 10 秒钟,死锁检测进程会自动检查数据库范围内有无死锁存在,如果发现了死锁存在,那么将随机挑选一个事务并强制终止它,这个事务将被回滚,那么这时另外一个事务将可以顺利执行下去。
总的来说,死锁可能是由下列情况导致的: