4. 锁的粒度与锁升级

SQL Server 的锁并不是“要么锁一行、要么锁整表”这么简单。它把锁资源按层级组织成一条从细到粗的链条:行(RID / KEY)→ 页(PAGE)→ 区(EXTENT)→ 表(OBJECT)→ 数据库(DATABASE)。粒度越细,并发越高,但锁数量越多、管理开销越大;粒度越粗,开销越小,但阻塞范围越大。理解这套层级,是治理并发问题的起点。

需要先澄清一个常见误解:SQL Server 默认并不会“先锁行、再自动升级为页、再升级为表”这样逐级爬升。锁升级(Lock Escalation)通常是从较细粒度直接跳到表级,而不是先经过页锁。页锁更多出现在特定访问路径(如扫描)或显式提示下。因此“行→页→表”的转换逻辑,应理解为层级关系与升级目标,而非必然的三级台阶。

锁粒度层级与锁升级路径 行锁 RID / KEY 页锁 PAGE 区锁 EXTENT 表锁 OBJECT 粒度由细到粗 · 并发由高到低 · 开销由大到小 锁升级:细粒度锁直接跃迁为表锁(不经过页锁) 锁升级触发条件 ① 单条语句在单个对象上持有约 5000 个锁 ② 同一语句在同一对象上锁数持续增长 ③ 内存压力大,锁管理器需回收资源 ④ 表已设置 LOCK_ESCALATION 策略 ⑤ 扫描类操作(无合适索引)更易触发 阈值 5000 为近似值,随版本与内存状态浮动 抑制与监控策略 · 优化索引,减少扫描与锁数量 · 分批提交,控制单事务锁规模 · 表级 LOCK_ESCALATION = DISABLE · 分区表设为 AUTO,升级到分区级 · 监控 sys.dm_tran_locks 锁计数 · 扩展事件捕获 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)以及触发语句,是定位升级根因最有效的工具。

抑制策略与取舍

抑制锁升级的核心思路是减少单语句在单对象上的锁数量,而不是简单地禁用升级。具体做法包括:

  1. 优化索引:让查询走索引查找而非全表扫描,减少触及的行数。这是最根本的手段。
  2. 分批处理:将大批量 UPDATE/DELETE 拆成多个小批次,每批提交一次,控制单事务锁规模。例如每批 1000 行。
  3. 使用 LOCK_ESCALATION = DISABLE:对高并发小表或锁数量可控的表禁用升级,但需监控锁内存。
  4. 分区表使用 AUTO:将升级范围限制在分区级,避免整表阻塞。
  5. 调整事务隔离级别:在允许的范围内使用 RCSI(读已提交快照),减少读操作持有的共享锁。
判断是否该禁用锁升级:先查 sys.dm_tran_locks,如果某表在高峰期频繁出现 OBJECT 锁且伴随大量阻塞,同时该表并发访问模式以短事务为主,可以考虑 DISABLE。如果表本身很大、单语句经常触及数万行,禁用后锁数量可能失控,此时应优先优化语句或分批处理。
常见误区:锁升级不是“错误”,而是 SQL Server 在开销与并发之间的自动权衡。盲目禁用升级可能把表锁问题转化为锁内存耗尽问题。治理的目标是让锁粒度与业务并发需求匹配,而不是追求“永远不升级”。

总结来说,锁粒度层级决定了并发与开销的平衡点,锁升级是锁管理器在锁数量过多时的自动收缩机制。理解触发条件、用 sys.dm_tran_locks 和扩展事件监控升级行为,再结合索引优化、分批提交和 LOCK_ESCALATION 策略进行抑制,才能在实际系统中把锁升级从“突发阻塞源”变成“可控的常规行为”。