DB2的锁机制是保证事务隔离性和数据一致性的基础设施,但很多使用者在遇到锁等待、死锁或者锁升级问题时,往往只能看到报错的表面信息,说不清楚背后到底是哪一层锁在起作用。DB2的锁并不仅仅是简单的“锁住一行”或“锁住一张表”,而是一个包含表级、行级乃至谓词级的完整体系,不同粒度的锁之间还存在升级和转换关系。本文将从表锁、行锁、谓词锁三个维度展开,结合隔离级别和典型故障场景,把这个体系讲透。

一、DB2锁体系的基本结构
DB2的锁是一个分层模型。最上层是表空间级锁,中间是表级锁,最下层是行级锁。在讨论并发时,我们通常关注的是表锁和行锁这两个层级。每一个事务在对数据进行操作时,DB2都会首先在表级别获取一个某种意图的锁(Intent Lock),然后再根据访问方式决定是否在行级别加锁。
表锁的作用是控制对整张表的访问,常见模式包括S(共享)、X(排他)、U(更新)、IS(意图共享)、IX(意图排他)和SIX(带意图排他的共享)。行锁则是对具体数据行的控制,包括NS(下一键共享,Next Key Share)、S、X和U等模式。意图锁的存在是为了让DB2在判断“能否给这张表加表锁”时不必逐行检查行锁情况,只需要看表头的意图锁即可快速判断兼容性,这是一种典型的多粒度锁设计,和理论上的区间锁(谓词锁)思想一脉相承。
可以用一条SQL观察当前的锁情况:
-- 查看当前数据库中的锁信息
db2 "SELECT SUBSTR(TABNAME,1,20) AS TABNAME,
LOCK_OBJECT_TYPE, LOCK_MODE, LOCK_STATUS
FROM SYSIBMADM.LOCKS_HELD
WHERE TABSCHEMA = 'DB2INST1'"
执行结果的LOCK_MODE列会明确显示S、X、IX、NS等锁模式,这是排查锁问题的第一步。
二、表锁:粒度大但开销小
表锁直接作用于整张表,加锁和释放的成本很低,因为DB2只需要维护一个锁记录。但代价是并发度也随之下降:一旦某个事务持有表的X锁,其他事务对这张表的任何读写操作都必须等待。表锁常见的触发场景有ALTER TABLE、LOAD操作、没有合适索引导致的全表扫描配合特定的隔离级别,以及锁升级。
表锁模式之间的兼容关系需要重点理解。IS与IS兼容,IX与IS兼容,IX与IX兼容,但IX与S不兼容,X与几乎一切都冲突。这个兼容矩阵解释了很多看似奇怪的等待现象:两个事务都在更新同一张表的不同行,理论上行级锁可以并行,但如果一个事务用S锁读、另一个事务用IX写,表级别就会出现冲突。判断兼容性时,DB2先看表锁矩阵,再看行锁矩阵,两层都通过才能并发执行。
锁升级是表锁绕不开的话题。DB2通过LOCKLIST和MAXLOCKS两个参数控制锁内存:LOCKLIST决定锁列表的总大小,MAXLOCKS决定单个事务最多能占用锁列表的百分比。当事务持有的行锁数量过多,占用的锁内存超过MAXLOCKS规定的阈值时,DB2会自动将行锁升级为表锁,一次性释放大量行锁记录。升级虽然节省了内存,却可能瞬间造成大范围的并发阻塞,生产环境突然出现大量锁等待,往往第一反应就应该检查是否发生了锁升级:
-- 查看锁升级发生的次数
db2 "SELECT SNAP.GET_SNAPSHOT
FROM SYSIBMADM.SNAPLOCK"
-- 调整锁参数的典型配置
UPDATE DB CFG FOR SAMPLEDB USING LOCKLIST AUTOMATIC
UPDATE DB CFG FOR SAMPLEDB USING MAXLOCKS AUTOMATIC
如果不想让DB2自动升级,也可以在语句或事务级别使用LOCK TABLE ... IN EXCLUSIVE MODE显式加表锁,主动选择串行化执行的时机,这比被动接受升级更可控。
三、行锁:并发的主力,与隔离级别深度绑定
行锁是DB2日常并发控制的主力,但它的行为并不是孤立决定的,而是和隔离级别紧密相关。DB2支持四种隔离级别:UR(未提交读)、CS(游标稳定性)、RS(读稳定性)和RR(可重复读)。同一个SELECT语句在不同隔离级别下,加的行锁完全不同。
在CS级别下,DB2对被读取的行加NS锁,读完成后立即释放,只保证不读到未提交数据;在RS级别下,所有被读过的行都会持有NS锁直到事务结束,防止其他事务修改或删除这些行;在RR级别下,行为进一步扩展到范围,DB2会锁住扫描过的整个范围,阻止幻读,这就自然引出了谓词锁的概念。用一个例子说明:
-- 会话1:在RS隔离级别下读取范围数据 SET CURRENT ISOLATION RS; SELECT * FROM ORDERS WHERE ORDER_ID BETWEEN 100 AND 200; -- 会话2:尝试插入该范围内的新行 INSERT INTO ORDERS VALUES (150, 'NEW'); -- RS级别下该插入可以成功 -- RR级别下该插入会被阻塞(范围被锁定)
这个例子清楚展示了RS和RR的区别:RS锁住的是“已经存在的行”,而RR锁定的是“满足条件的行,包括还不存在的行”。后者本质上就是谓词锁——锁的对象不是某个具体的物理行,而是一个逻辑条件所描述的行的集合。
需要注意的是,行锁的数量与扫描的行数成正比,如果没有合适的索引,一次范围查询可能对成千上万行加锁,既消耗锁内存又加剧冲突。优化索引让访问路径精准,是减少行锁冲突最有效的手段之一。
四、谓词锁:锁定还不存在的数据
谓词锁(Predicate Lock)是锁机制中最容易被忽视的部分。传统行锁只能锁住物理存在的行,但有一类问题它解决不了:事务A查询了BETWEEN 100 AND 200的范围,事务B在这个范围内插入一行新的数据,事务A再次查询时会多出一行,这就是幻读。要阻止幻读,就必须锁住“满足谓词条件的所有可能数据”,包括尚不存在的行。
DB2在RR隔离级别下实现的正是这种机制。当查询带有范围条件时,DB2会对索引区间加锁,通过NS和X锁配合封锁键值范围,效果等同于对WHERE条件本身加锁。即使没有索引可用,DB2也会退化为锁住整个表来保证范围不被插入,这也是为什么在RR级别下无索引的范围查询会严重限制并发的根本原因。
在实际使用中,一个直观的验证方法是对比两种隔离级别下的插入行为:
-- 会话1使用RR隔离级别执行范围查询 SET CURRENT ISOLATION RR; SELECT COUNT(*) FROM ACCOUNT WHERE BALANCE > 10000; -- 会话2尝试插入一条满足条件的新记录 INSERT INTO ACCOUNT(ID, BALANCE) VALUES(999, 20000); -- 会话2被阻塞,直到会话1提交或回滚
谓词锁保证了会话1两次执行同样的查询得到完全一致的结果,代价是插入端的并发受限。业务上是否真的需要这种严格程度,需要根据场景权衡,报表类查询适合RR,高并发的在线交易则更适合CS。
五、常见锁问题的排查思路
掌握了三类锁的原理之后,排查实际问题的思路就清晰了。锁等待的排查可以用db2pd -db 数据库名 -locks showlocks或者快照表函数,重点看锁的持有者(持锁应用句柄)和等待者,以及具体的锁模式。死锁则会触发SQLCODE为-911(原因码2)或-913的报错,DB2会自动选择回滚代价较小的事务作为牺牲者。
另一个实用技巧是利用MON_LOCKWAITS管理视图持续监控等待链路,配合MON_GET_APPL_LOCKWAIT定位具体SQL。减少锁冲突的通用做法包括:事务尽量短小、避免在事务中做耗时操作、按固定顺序访问多张表、必要时使用WITH UR降低读操作的加锁强度。理解表锁、行锁、谓词锁三者的分工与转换关系,是做好DB2并发调优的地基,也是从“会写SQL”到“能hold住生产问题”的关键一步。