4. 锁的粒度与锁升级
SQL Server 的锁并不是“要么锁一行、要么锁整表”这么简单。它把锁资源按层级组织成一条从细到粗的链条:行(RID / KEY)→ 页(PAGE)→ 区(EXTENT)→ 表(OBJECT)→ 数据库(DATABASE)。粒度越细,并发越高,但锁数量越多、管理开销越大;粒度越粗,开销越小,但阻塞范围越大。理解这套层级,是治理并发问题的起点。
需要先澄清一个常见误解:SQL Server 默认并不会“先锁行、再自动升级为页、再升级为表”这样逐级爬升。锁升级(Lock Escalation)通常是从较细粒度直接跳到表级,而不是先经过页锁。页锁更多出现在特定访问路径(如扫描)或显式提示下。因此“行→页→表”的转换逻辑,应理解为层级关系与升级目标,而非必然的三级台阶。
锁升级的触发条件
锁升级由锁管理器自动判断,核心触发条件可以归纳为以下几点:
- 锁数量阈值:当单条语句在同一个对象上持有的锁数量达到约 5000 个时,SQL Server 会考虑升级。这个阈值不是硬编码常量,会随版本、内存压力和对象类型变化,但 5000 是最常被引用的参考值。
- 锁数量持续增长:即使未到阈值,如果同一语句在同一对象上的锁数持续快速增加,也可能提前触发升级。
- 内存压力:锁管理器需要为每个锁分配约 96 字节的内存。当系统内存紧张时,升级为表锁可以大幅减少锁对象数量,释放内存。
- 访问路径:全表扫描、无合适索引的查询、大批量 UPDATE/DELETE 更容易触发升级,因为它们会触及大量行。
- 表级策略:通过
ALTER TABLE ... SET (LOCK_ESCALATION = ...)可以控制升级行为。
LOCK_ESCALATION 的三种取值
| 取值 | 行为 | 适用场景 |
|---|---|---|
| TABLE | 默认值,升级到表级锁 | 普通表,并发要求不高 |
| DISABLE | 禁止锁升级,保持细粒度锁 | 高并发 OLTP 表,但可能增加锁管理器开销 |
| AUTO | 分区表升级到分区级,非分区表等同 TABLE | 分区表,希望缩小升级影响范围 |
注意:DISABLE 并非“免费午餐”。它阻止了表锁,但锁数量会继续增长,可能耗尽锁内存并导致更多阻塞检测开销。因此它适合锁数量可控、并发冲突敏感的表,而不是所有表。
监控锁升级的实操方法
监控锁升级最直接的方式是查询 sys.dm_tran_locks,观察锁的粒度和数量分布:
-- 查看当前锁的粒度分布
SELECT
request_session_id AS spid,
resource_type,
COUNT(*) AS lock_count
FROM sys.dm_tran_locks
GROUP BY request_session_id, resource_type
ORDER BY lock_count DESC;
-- 查看是否有表级锁(OBJECT)
SELECT
request_session_id,
resource_type,
resource_database_id,
resource_associated_entity_id
FROM sys.dm_tran_locks
WHERE resource_type = 'OBJECT';
如果某个会话的 resource_type = 'OBJECT' 且锁数量突然从大量 KEY/RID 变为少量 OBJECT,基本可以判定发生了锁升级。更精细的监控可以使用扩展事件:
CREATE EVENT SESSION [TrackLockEscalation]
ON SERVER
ADD EVENT sqlserver.lock_escalation(
ACTION(sqlserver.sql_text, sqlserver.database_name)
)
ADD TARGET package0.ring_buffer;
ALTER EVENT SESSION [TrackLockEscalation]
ON SERVER STATE = START;
lock_escalation 事件会记录升级发生的对象、模式(如从 KEY 升级到 OBJECT)以及触发语句,是定位升级根因最有效的工具。
抑制策略与取舍
抑制锁升级的核心思路是减少单语句在单对象上的锁数量,而不是简单地禁用升级。具体做法包括:
- 优化索引:让查询走索引查找而非全表扫描,减少触及的行数。这是最根本的手段。
- 分批处理:将大批量 UPDATE/DELETE 拆成多个小批次,每批提交一次,控制单事务锁规模。例如每批 1000 行。
- 使用 LOCK_ESCALATION = DISABLE:对高并发小表或锁数量可控的表禁用升级,但需监控锁内存。
- 分区表使用 AUTO:将升级范围限制在分区级,避免整表阻塞。
- 调整事务隔离级别:在允许的范围内使用 RCSI(读已提交快照),减少读操作持有的共享锁。
sys.dm_tran_locks,如果某表在高峰期频繁出现 OBJECT 锁且伴随大量阻塞,同时该表并发访问模式以短事务为主,可以考虑 DISABLE。如果表本身很大、单语句经常触及数万行,禁用后锁数量可能失控,此时应优先优化语句或分批处理。
总结来说,锁粒度层级决定了并发与开销的平衡点,锁升级是锁管理器在锁数量过多时的自动收缩机制。理解触发条件、用 sys.dm_tran_locks 和扩展事件监控升级行为,再结合索引优化、分批提交和 LOCK_ESCALATION 策略进行抑制,才能在实际系统中把锁升级从“突发阻塞源”变成“可控的常规行为”。