SQL Server 的 SELECT 读取 1 行数据时,不会在表级别加 S 锁(共享锁),只是在表级别加上 IS 锁(意向共享锁),表示"这个表里有行级共享锁(S 锁)",IS 锁(意向共享锁)不阻塞写操作。
行级 S 锁只会阻塞对同一行的写操作;
表级 S 锁才会阻塞对整张表的写操作。
普通 SELECT 不会阻塞写操作(INSERT/UPDATE/DELETE)。只有 SELECT 触发了行级S锁 升级到表级S锁时,才会阻塞整张表的写操作(INSERT/UPDATE/DELETE)。
在默认的 READ COMMITTED 隔离级别下,SELECT 读取行时加行级 S 锁,但行级 S 锁是语句级释放,不是读完一行就释放。也就是说,SELECT 在执行过程中会累积已读行的 S 锁,直到整个语句结束才统一释放。当累积的行级 S 锁数量超过阈值(约 5000),SQL Server 会尝试将这些行级 S 锁升级为表级 S 锁。这不是从 IS 锁升级------IS 锁是表级意向锁,从 SELECT 开始执行时就存在,只有当行级 S 锁成功升级为表级 S 锁后,IS 锁才会被释放。当然升级是"尝试性"的:如果表上已有不兼容的锁(比如别的会话持有 IX 锁),升级会失败,SQL Server 会继续持有这些行级 S 锁,直到语句结束。升级失败不会导致报错,也不会改变锁的释放时机,只是锁的粒度保持为行级,没有变成表级。
因此,普通 SELECT是否触发行级S锁升级到表级S锁关键看"执行过程中累积的行级S锁对象数量",而不是"最终返回的行数",以及表上是否有不兼容的锁(比如别的会话持有 IX 锁)
SELECT 触发锁升级的场景(前提是表上没有有不兼容的锁比如 IX 锁)
1、走索引并回表、返回超过 5000 行:这是最容易触发升级的场景。回表逐行加锁,锁数量接近行数,容易突破5000阈值
2、没走索引、大表全表扫描、最终只返回 1 行:全表扫描通常用页锁,大表全表扫描的情况下,页锁也可能突破5000阈值
补充1:如果数据库开启了 RCSI(READ_COMMITTED_SNAPSHOT),SELECT 不加行级 S 锁,而是读行版本存储。因此,SELECT 不会产生锁累积,也不会阻塞 DML,DML 也不会阻塞 SELECT。
补充2:比如一个 SELECT 要扫描 1 万行。当它扫描到第 8000 行、累积的行级 S 锁触发升级并成功拿到表级 S 锁时,之前那 8000 行对应的行级 S 锁还在吗?后续 8001--10000 行还会继续逐行加锁吗?
锁升级成功 = "清零重来"。0-8000 行辛苦累积的行级 S 锁会被全部释放,由 1 个表级 S 锁统一接管。后续的 8001-1 万行不再逐行加锁,因为整张表已经在表级 S 锁的保护下了。 这正是锁升级"减少系统开销"的体现------用更少的锁,保护更多的数据。
SELECT 走索引要回表读 1 万行,耗时 10 分钟:
读到第 1 行 → 加行级 S 锁(保留)
读到第 2 行 → 加行级 S 锁(保留)
......
读到第 10000 行 → 加行级 S 锁(保留)
语句结束 → 统一释放所有行级 S 锁
任意时刻,这个 SELECT 持有的是已读过的所有行的 S 锁,而不是仅当前那一行的 S 锁。这正是锁数量会累积、可能触发锁升级的原因。
如果 UPDATE 要改第 1 行,而 SELECT 正在执行:
SELECT 读到第 1 行 → 加行级 S 锁并保留
UPDATE 想改第 1 行 → 需要等 SELECT 的行级 S 锁释放
如果 SELECT 执行 10 分钟,第 1 行的 S 锁就可能被持有 10 分钟,UPDATE 的阻塞时间也可能是几分钟,而不是几毫秒
示例
第一步:准备测试表
CREATE TABLE dbo.LockTest_A (id INT PRIMARY KEY, name NVARCHAR(50));
INSERT INTO dbo.LockTest_A VALUES (1, 'a1'), (2, 'a2');
CREATE TABLE dbo.LockTest_B (id INT PRIMARY KEY, name NVARCHAR(50));
INSERT INTO dbo.LockTest_B VALUES (1, 'b1'), (2, 'b2');
第二步:模拟 A 表持有行级 S 锁
会话1
BEGIN TRAN;
SELECT * FROM dbo.LockTest_A WITH (HOLDLOCK) WHERE id = 1;
第三步:模拟 B 表持有表级 S 锁
会话2
BEGIN TRAN;
SELECT * FROM dbo.LockTest_B WITH (TABLOCK, HOLDLOCK);
第四步:验证阻塞效果
会话3
UPDATE dbo.LockTest_A SET name = 'b1_updated' WHERE id = 1;--堵塞
会话4
UPDATE dbo.LockTest_A SET name = 'b1_updated' WHERE id = 2;--正常执行
会话5
UPDATE dbo.LockTest_B SET name = 'b1_updated' WHERE id = 2;--堵塞
第五步:查询琐结果类型
sql
SELECT
l.request_session_id AS SPID,
OBJECT_NAME(COALESCE(p.object_id, l.resource_associated_entity_id)) AS TableName,
l.resource_type AS LockResource,
l.request_mode AS LockMode,
l.request_status AS LockStatus,
r.blocking_session_id AS BlockedBy
FROM sys.dm_tran_locks l
LEFT JOIN sys.partitions p
ON l.resource_associated_entity_id = p.hobt_id
AND l.resource_type IN ('KEY', 'RID', 'PAGE')
LEFT JOIN sys.dm_exec_requests r
ON l.request_session_id = r.session_id
WHERE
OBJECT_NAME(COALESCE(p.object_id, l.resource_associated_entity_id)) IN ('LockTest_A', 'LockTest_B')
ORDER BY TableName, LockMode;
返回结果如下
SPID TableName LockResource LockMode LockStatus BlockedBy
会话1 LockTest_A PAGE IS GRANT NULL
会话1 LockTest_A OBJECT IS GRANT NULL
会话3 LockTest_A OBJECT IX GRANT 会话1
会话3 LockTest_A PAGE IX GRANT 会话1
会话1 LockTest_A KEY S GRANT NULL
会话3 LockTest_A KEY X WAIT 会话1
会话5 LockTest_B OBJECT IX WAIT 会话2
会话2 LockTest_B OBJECT S GRANT NULL
解读返回结果
会话1持有LockTest_A的行级S锁,页级IS锁,表级IS锁,其中会话1持有LockTest_A的行级S锁,堵塞了会话3的行级X锁
会话2持有LockTest_B表级S锁,堵塞了会话5的表级IX锁