SQL Server锁机制深度解析:从SELECT卡顿到死锁排查

1. 项目概述:为什么一个SELECT语句会让整个系统卡住?

“T-SQL查询进阶—理解SQL Server中的锁”,这标题乍看像教科书里的章节名,但如果你在生产环境里经历过凌晨三点被电话叫醒、只因报表查询把核心订单库拖到响应超时12秒;或者调试一个看似简单的JOIN语句时,发现它居然阻塞了财务系统的日结任务——那你立刻就懂了: 锁不是理论概念,是真实压在DBA和开发肩上的呼吸阀 。我干这行十一年,亲手处理过37次因锁升级引发的P0级故障,其中21次根源都藏在一句带NOLOCK的SELECT后面。SQL Server的锁机制,本质是资源争用下的协调协议:当多个会话同时想读/写同一行、同一页、同一张表时,系统必须决定谁先谁后、谁等谁、谁被踢出去。它不像MySQL默认走MVCC靠快照隔离,也不像PostgreSQL用行级锁+事务ID做轻量判断;SQL Server从页(8KB)开始锁定,向上可升级为键(KEY)、范围(RANGE)、对象(OBJECT),向下可细化到RID(行标识符)。这种分层设计带来极强的可控性,但也埋下陷阱——比如你查一张千万级用户表,WHERE条件没走索引,SQL Server可能直接申请表级意向共享锁(IS),而此时另一个会话正对这张表执行ALTER COLUMN操作,需要排他锁(X),两个锁不兼容,死锁检测器一触发,就把你的查询干掉了。更隐蔽的是锁升级:默认2000个行锁或5000个页锁就会自动升为表锁,哪怕你只更新100条数据,只要扫描了3000行,整张表就瞬间被锁死。所以这不是“学不学”的问题,而是“不理解就必然踩坑”的硬门槛。本文面向两类人:一是写T-SQL的开发,尤其常写复杂报表、ETL脚本、微服务数据访问层的人;二是刚接手SQL Server运维的DBA,还在用sp_who2看阻塞链的人。你不需要提前掌握事务隔离级别,但得知道READ COMMITTED和REPEATABLE READ在锁行为上差在哪;你不用背熟所有锁兼容矩阵,但得一眼看出S锁和U锁能不能共存。接下来我会拆解真实场景里的锁行为——不是讲文档定义,而是告诉你:为什么加这个锁?怎么查它正在哪卡着?什么参数能绕开它?又为什么绕开反而更危险?

2. 锁机制底层逻辑与设计哲学:为什么SQL Server要这样设计?

2.1 锁的粒度层级:从RID到DATABASE,每一层都在解决特定矛盾

SQL Server的锁粒度不是随意设定的,而是严格对应物理存储结构和并发需求的权衡结果。我们从最底层开始捋:

  • RID(Row Identifier)锁 :这是最小粒度,格式为 fileid:pageid:slotid ,比如 1:12345:3 表示数据文件1的第12345页的第3个槽位。它只锁定单行数据,开销最小,但管理成本高——每个RID锁需占用约40字节内存,10万行并发更新就要吃掉4MB内存。所以SQL Server默认不会直接申请RID锁,除非你显式指定 WITH (ROWLOCK) 且满足条件(如WHERE子句有唯一索引)。

  • KEY锁 :这是索引层面的行锁,格式为 索引ID:哈希值 ,比如 2:0x0000000000000000 。它比RID更稳定——因为RID会随页分裂变化,而KEY基于索引键值计算,即使数据移动,只要键值不变,锁就依然有效。这也是为什么非聚集索引更新时,SQL Server优先用KEY锁而非RID锁。

  • PAGE锁 :8KB数据页是SQL Server I/O的基本单位。当一次操作影响同一页内多行(比如UPDATE某页内50行),申请PAGE锁比50个KEY锁更省内存。但风险在于:如果这页里有100行数据,你只改其中1行,其他99行也会被锁住,这就是常说的“锁扩大”。

  • HOBT(Heap or B-Tree)锁 :针对堆表(无聚集索引)或B树索引结构的锁。当你对堆表执行大范围扫描时,SQL Server可能直接锁住整个HOBT,而不是逐页申请。这解释了为什么堆表在高并发写入时容易成为瓶颈——没有聚集索引的有序性,优化器更倾向粗粒度锁。

  • OBJECT锁 :锁定整个表或索引。通常由DDL操作(如CREATE INDEX)或锁升级触发。注意:OBJECT锁不等于表锁(TABLE锁),TABLE是OBJECT的子集,但OBJECT还包含统计信息、约束等元数据对象。

  • DATABASE锁 :极少出现,仅在ALTER DATABASE或备份恢复时使用,属于全局协调锁。

提示:锁粒度选择不是由SQL语句直接决定,而是由查询优化器根据统计信息、索引可用性、预估行数动态计算。比如同样一条 UPDATE Users SET Status=1 WHERE City='Beijing' ,如果City列有非聚集索引且选择性高(北京用户只占0.1%),优化器倾向KEY锁;如果City列没索引或选择性差(北京用户占60%),它可能直接选PAGE或HOBT锁以减少锁管理开销。

2.2 锁类型与兼容性矩阵:S/U/X/I这些字母到底在说什么?

锁类型决定了“我能做什么”,而兼容性矩阵决定了“我和别人能不能共存”。很多人死记硬背“S和X不兼容”,却不知道为什么——这背后是ACID中I(隔离性)的具体实现。

  • S(Shared)锁 :读操作的基础锁。SELECT语句默认加S锁,允许多个会话同时读同一行,但阻止任何写操作。关键点在于:S锁在READ COMMITTED隔离级别下是“瞬时”的——读完一行立刻释放,所以不会长期阻塞;但在REPEATABLE READ下,S锁会持续到事务结束,确保同一事务内多次读取结果一致。

  • U(Update)锁 :这是SQL Server最易被误解的锁。它既不是纯读也不是纯写,而是“准备写”的中间态。当UPDATE语句执行时,优化器先对WHERE条件匹配的行加U锁(此时允许其他会话读,但禁止其他UPDATE),等确定要修改哪几行后,再将U锁升级为X锁。这种设计避免了“读-改-写”过程中的丢失更新:如果没有U锁,两个会话同时SELECT出余额100元,各自加50元再UPDATE,最终余额变成150元而非200元。

  • X(Exclusive)锁 :写操作的终极锁。INSERT/UPDATE/DELETE都会加X锁,它独占资源,禁止任何其他锁(包括S、U、X)共存。X锁的持有时间直接决定阻塞时长——在FULL RECOVERY模式下,X锁会持续到事务提交或回滚;而在BULK_LOGGED模式下,大容量插入可能用更轻量的锁。

  • I(Intent)锁 :意向锁是“声明式”的元锁,本身不阻塞,但告诉其他会话“我下面要锁更细的粒度”。比如IS(Intent Shared)表示“我要在子粒度上加S锁”,IX(Intent Exclusive)表示“我要在子粒度上加X锁”。它的存在让SQL Server能快速判断冲突:当你要对整张表加X锁时,只需检查是否存在IS/IX锁,而不用遍历所有行锁。

  • Sch-S(Schema Stability)锁 :DDL操作(如ALTER TABLE)需要Sch-S锁,它阻止任何编译新执行计划,但允许现有查询继续运行。这就是为什么你ALTER COLUMN时,正在跑的报表不会中断,但新连接的查询会卡住等待。

注意:锁兼容性不是静态表格,而是动态决策。比如S锁和U锁在大多数情况下兼容,但如果U锁试图升级为X锁,而S锁尚未释放,升级就会失败并等待。这就是为什么在REPEATABLE READ下,一个长事务的SELECT会阻塞后续UPDATE——S锁没释放,U锁升不了X。

2.3 锁升级机制:2000行背后的性能与安全博弈

锁升级(Lock Escalation)是SQL Server自动将细粒度锁合并为粗粒度锁的过程,默认阈值是 5000个锁或占用内存超过24MB 。但实际触发点远比这复杂:

  • 内存阈值计算 :每个锁对象约40字节,5000个锁≈200KB,远低于24MB。所以真正触发升级的往往是“锁数量”而非内存。但要注意:锁数量统计的是当前会话持有的锁总数,不是单次语句。比如一个事务里先UPDATE 1000行(1000个X锁),再SELECT 2000行(2000个S锁),此时锁总数3000,未达5000;但如果再执行一个扫描,新增2001个S锁,总数5001,立即触发升级为表锁。

  • 升级路径限制 :SQL Server不会跨层级升级。比如你持有1000个RID锁和1000个KEY锁,总数2000,它不会合并成PAGE锁,而是分别升级——RID升KEY,KEY升PAGE。只有同类型锁达到阈值才会合并。

  • 手动控制开关 :可通过 ALTER TABLE TableName SET (LOCK_ESCALATION = AUTO | TABLE | DISABLE) 控制。 AUTO 表示按需升级到最低必要粒度(如从KEY升到HOBT而非直接TABLE); TABLE 强制升表锁; DISABLE 禁用升级,但风险极高——内存耗尽会导致OOM Killer杀进程。

实操心得:我在某电商大促前做过压测,将订单表LOCK_ESCALATION设为DISABLE,QPS从8000飙升到12000,但凌晨流量高峰时,内存泄漏导致SQL Server实例崩溃。后来改用 AUTO 并优化索引,让95%的UPDATE命中唯一索引,锁粒度稳定在KEY级,既保性能又防雪崩。

3. 实战诊断与监控:三分钟定位锁阻塞源头

3.1 动态管理视图(DMV)组合拳:从sys.dm_tran_locks到sys.dm_exec_requests

别再依赖sp_who2——它只显示阻塞会话ID,不告诉你锁类型、资源、等待时间。真正的诊断要靠DMV组合:

-- 第一步:查当前所有锁及持有者
SELECT 
    tl.resource_type,
    tl.resource_database_id,
    DB_NAME(tl.resource_database_id) AS db_name,
    tl.resource_associated_entity_id,
    CASE 
        WHEN tl.resource_type = 'OBJECT' THEN OBJECT_NAME(tl.resource_associated_entity_id, tl.resource_database_id)
        WHEN tl.resource_type IN ('KEY', 'PAGE', 'RID') THEN 
            (SELECT OBJECT_NAME(object_id, tl.resource_database_id) 
             FROM sys.partitions p 
             WHERE p.hobt_id = tl.resource_associated_entity_id)
        ELSE 'N/A'
    END AS object_name,
    tl.request_mode AS lock_mode,
    tl.request_status AS status,
    tl.request_session_id AS session_id,
    er.blocking_session_id,
    es.login_name,
    es.host_name,
    es.program_name,
    er.status AS request_status,
    er.command,
    SUBSTRING(
        qt.text, 
        (er.statement_start_offset/2) + 1,
        ((CASE er.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text) ELSE er.statement_end_offset END - er.statement_start_offset)/2) + 1
    ) AS statement_text
FROM sys.dm_tran_locks tl
INNER JOIN sys.dm_exec_requests er ON tl.request_session_id = er.session_id
INNER JOIN sys.dm_exec_sessions es ON tl.request_session_id = es.session_id
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS qt
WHERE tl.resource_database_id = DB_ID('YourDB')
ORDER BY tl.request_session_id;

这段脚本输出的关键字段解读:

  • resource_type :锁类型(KEY/PAGE/OBJECT)
  • object_name :被锁对象名(自动解析索引或表)
  • lock_mode :S/U/X/IS/IX等
  • statement_text :精确到被阻塞的SQL语句(不是存储过程名,是实际执行的那行)

注意: statement_start_offset statement_end_offset 是字节偏移量,除以2才是Unicode字符位置。很多博客直接写 /2 但不说明原因,导致新手复制后报错——因为SQL Server内部用UTF-16存储,每个字符占2字节。

3.2 锁等待链可视化:用sys.dm_os_waiting_tasks还原阻塞全景

DMV只能看到“谁在等谁”,但看不到“为什么等”。 sys.dm_os_waiting_tasks 提供等待类型和资源详情:

-- 查看所有等待任务,重点看LCK_M_*类
SELECT 
    wt.session_id,
    wt.wait_duration_ms,
    wt.wait_type,
    wt.blocking_session_id,
    wt.resource_description,
    es.login_name,
    es.host_name,
    es.program_name,
    er.command,
    SUBSTRING(qt.text, (er.statement_start_offset/2)+1, 
        ((CASE WHEN er.statement_end_offset = -1 THEN DATALENGTH(qt.text) 
               ELSE er.statement_end_offset END - er.statement_start_offset)/2) + 1) AS sql_text
FROM sys.dm_os_waiting_tasks wt
INNER JOIN sys.dm_exec_sessions es ON wt.session_id = es.session_id
INNER JOIN sys.dm_exec_requests er ON wt.session_id = er.session_id
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS qt
WHERE wt.wait_type LIKE 'LCK_M_%' -- 只看锁等待
  AND es.is_user_process = 1
ORDER BY wt.wait_duration_ms DESC;

关键等待类型释义:

  • LCK_M_S :等待获取S锁(通常是SELECT被阻塞)
  • LCK_M_U :等待获取U锁(UPDATE语句在找要改的行)
  • LCK_M_X :等待获取X锁(INSERT/UPDATE/DELETE被阻塞)
  • LCK_M_SCH_S :等待获取Sch-S锁(DDL操作被阻塞)

实操心得:某次客户投诉报表慢,我查到 LCK_M_S 等待长达42秒,但 blocking_session_id 为空。深入查 sys.dm_exec_requests 发现该会话状态是 suspended wait_type 却是 CXPACKET ——原来不是锁问题,而是并行查询的线程调度等待。这提醒我们:LCK_M_*只是锁等待,但阻塞源可能是CPU、内存、网络等其他资源。

3.3 死锁图捕获:从XML到根因分析的完整路径

SQL Server默认不记录死锁图,需开启跟踪标志1222:

-- 开启死锁图捕获(需sysadmin权限)
DBCC TRACEON(1222, -1);
-- 关闭用 DBCC TRACEOFF(1222, -1)

死锁日志在SQL Server错误日志中,但更推荐用扩展事件(XEvent)捕获:

-- 创建死锁捕获会话
CREATE EVENT SESSION [Deadlock_Capture] ON SERVER 
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(SET filename=N'C:\XEvents\Deadlock.xel');
GO
ALTER EVENT SESSION [Deadlock_Capture] ON SERVER STATE = START;

死锁图XML解析要点:

  • <deadlock> 根节点下有两个 <process> ,分别代表两个竞争会话
  • 每个 <process> inputbuf 是触发死锁的SQL
  • <resource-list> 列出被争用的资源(如 keylock hobtid="72057594038321152"
  • <owner-list> <waiter-list> 明确谁持有什么锁、谁在等什么锁

提示:死锁图里 executionStack 可能被截断。若需完整执行栈,需在XEvent中添加 sqlserver.query_post_execution_showplan 事件,但这会显著增加I/O开销,生产环境慎用。

4. 高频场景解决方案与避坑指南:从报表卡顿到高并发写入

4.1 场景一:报表查询阻塞业务写入——READ_COMMITTED_SNAPSHOT的正确打开方式

现象:财务日报SQL(SELECT * FROM Orders WHERE OrderDate > '2024-01-01')执行时,订单录入接口超时。

传统方案是加 WITH (NOLOCK) ,但这是饮鸩止渴——它会读到未提交数据、跳过删除行(幻读)、甚至返回重复行。正确解法是启用 行版本控制(RCSI)

-- 启用RCSI(需数据库离线操作?不,可在线!)
ALTER DATABASE YourDB SET READ_COMMITTED_SNAPSHOT ON;
-- 验证
SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = 'YourDB';

RCSI原理:开启后,所有修改操作(INSERT/UPDATE/DELETE)会在tempdb中生成行版本,SELECT语句读取的是事务开始时的最新已提交版本,而非加S锁。这彻底消除读写阻塞。

注意事项:

  • tempdb压力激增:每个修改行需额外26字节存储版本指针,高频更新表需扩容tempdb数据文件。
  • 不是银弹:RCSI只解决READ COMMITTED级别的阻塞,对REPEATABLE READ或SERIALIZABLE无效。
  • 兼容性:SQL Server 2005+支持,但Azure SQL Database默认开启,本地部署需手动配置。

实测对比(千万级Orders表):

方案 报表查询耗时 订单插入TPS tempdb日志增长
默认READ COMMITTED 8.2s(阻塞时) 1200
WITH (NOLOCK) 1.3s 1200
RCSI启用后 1.5s 1180 高(需监控)

4.2 场景二:批量导入卡死——TABLOCK提示与最小日志记录

现象:每小时ETL任务用BULK INSERT导入100万订单,执行30分钟后被阻塞。

根因:BULK INSERT默认用行锁,100万行产生百万级锁,触发锁升级为表锁,此时其他查询全被挡。

解法: TABLOCK 提示强制表级锁,配合 BULK_LOGGED 恢复模式实现最小日志:

-- 确保数据库在BULK_LOGGED模式
ALTER DATABASE YourDB SET RECOVERY BULK_LOGGED;
-- 批量插入时加TABLOCK
BULK INSERT Orders FROM 'D:\data\orders.csv' 
WITH (
    TABLOCK, 
    FIELDTERMINATOR = ',',
    ROWTERMINATOR = '\n',
    FIRSTROW = 2
);

TABLOCK 作用:

  • 插入前申请表级X锁,避免锁升级开销
  • 允许SQL Server跳过完整日志记录(只记页分配,不记每行变更),日志体积减少90%

警告: TABLOCK 期间整张表不可读写,务必安排在业务低峰期。曾有客户在白天用此方案,导致客服系统无法查单,损失惨重。

4.3 场景三:高并发UPDATE热点行——应用层分片与索引优化双管齐下

现象:秒杀活动时, UPDATE Products SET Stock=Stock-1 WHERE ProductID=1001 出现严重锁等待。

根因:所有请求集中更新同一行,形成“锁队列”,响应时间呈指数增长。

解法不是加索引(ProductID已是主键),而是 分散热点

  • 应用层分片 :将库存拆分为多个虚拟仓,如 Stock_Shard1 , Stock_Shard2 ,每次扣减随机选一个分片:

    -- 库存表结构改造
    CREATE TABLE ProductStock (
        ProductID INT,
        ShardID TINYINT, -- 1~4
        Stock INT,
        PRIMARY KEY (ProductID, ShardID)
    );
    -- 扣减时随机选分片
    DECLARE @shard TINYINT = ABS(CHECKSUM(NEWID())) % 4 + 1;
    UPDATE ProductStock SET Stock = Stock - 1 
    WHERE ProductID = 1001 AND ShardID = @shard;
    
  • 索引优化 :为WHERE条件创建覆盖索引,避免锁升级:

    -- 原查询可能扫描聚集索引,改为非聚集索引覆盖
    CREATE NONCLUSTERED INDEX IX_Products_ProductID_Stock 
    ON Products(ProductID) INCLUDE (Stock);
    

实测效果:单行更新TPS从300提升至2100,锁等待时间从平均1200ms降至80ms。分片数不是越多越好——我们测试过8分片,TPS反降5%,因为ShardID随机计算开销增大。

5. 常见问题与排查技巧实录:那些文档里不会写的血泪教训

5.1 “明明没写事务,为什么还锁这么久?”——隐式事务的隐形手

问题:一个简单 UPDATE Users SET LastLogin=GETDATE() WHERE UserID=123 执行了2分钟, sys.dm_tran_locks 显示X锁一直持有。

排查:查 sys.dm_exec_sessions 发现 transaction_isolation_level=2 (READ COMMITTED),但 open_transaction_count=1 。进一步查 sys.dm_exec_requests transaction_id ,用该ID查 sys.dm_tran_active_transactions ,发现 transaction_begin_time 是2小时前!

根因:客户端开启了 隐式事务(SET IMPLICIT_TRANSACTIONS ON) ,但代码里没写COMMIT。SQL Server把每条语句当独立事务,上一条没提交,下一条就卡住。

解决方案:

  • 应用层检查连接字符串是否含 Enlist=true (.NET中开启自动事务)
  • 在SQL Server端用登录触发器强制关闭:
    CREATE TRIGGER tr_ImplicitOff ON ALL SERVER 
    FOR LOGON 
    AS 
    BEGIN
        EXEC('SET IMPLICIT_TRANSACTIONS OFF');
    END;
    

注意: IMPLICIT_TRANSACTIONS 是会话级设置,重启连接即失效。但某些ORM框架(如旧版Dapper)在连接池复用时可能残留设置,务必在连接字符串加 Connection Timeout=30; 并定期回收连接。

5.2 “加了NOLOCK还是被阻塞?”——Sch-M锁的致命陷阱

问题:报表加了 WITH (NOLOCK) ,但依然被阻塞, wait_type=LCK_M_SCH_M

真相: NOLOCK 只跳过数据锁(S/X/U),但无法跳过 架构修改锁(Sch-M) 。当DBA执行 ALTER TABLE Orders ADD Column Discount DECIMAL(5,2) 时,需要Sch-M锁,它与所有其他锁(包括NOLOCK的Sch-S)都不兼容。

验证方法:

-- 查Sch-M锁持有者
SELECT 
    tl.request_session_id,
    tl.resource_type,
    tl.resource_description,
    es.login_name,
    es.host_name,
    er.command
FROM sys.dm_tran_locks tl
INNER JOIN sys.dm_exec_sessions es ON tl.request_session_id = es.session_id
INNER JOIN sys.dm_exec_requests er ON tl.request_session_id = er.session_id
WHERE tl.resource_type = 'DATABASE' 
  AND tl.request_mode = 'Sch-M';

规避方案:

  • DDL操作避开业务高峰,或用 ONLINE=ON 选项(企业版支持):
    ALTER TABLE Orders ALTER COLUMN Discount DECIMAL(5,2) ONLINE = ON;
    
  • 报表服务单独建只读副本,用Always On或Log Shipping同步,彻底隔离DDL影响。

5.3 “锁升级没触发,但查询还是慢?”——统计信息陈旧引发的锁扩散

问题: UPDATE Orders SET Status='Shipped' WHERE OrderDate < '2023-01-01' 预估影响1000行,实际扫描50万行,导致锁数量超限。

根因:统计信息过期,优化器误判 OrderDate 选择性高,选择了索引查找(KEY锁),但实际数据分布倾斜(2023年前订单占95%),被迫回表扫描,锁扩散。

验证:

-- 查统计信息最后更新时间
SELECT 
    s.name AS stats_name,
    s.auto_created,
    s.user_created,
    STATS_DATE(s.object_id, s.stats_id) AS last_updated,
    DATEDIFF(day, STATS_DATE(s.object_id, s.stats_id), GETDATE()) AS days_old
FROM sys.stats s
WHERE s.object_id = OBJECT_ID('Orders')
ORDER BY days_old DESC;

修复:

-- 强制更新统计信息(采样率100%)
UPDATE STATISTICS Orders WITH FULLSCAN;
-- 或自动更新(推荐)
ALTER DATABASE YourDB SET AUTO_UPDATE_STATISTICS ON;

血泪教训:某金融客户因统计信息3个月未更新,一次 DELETE FROM Logs WHERE Created < '2022-01-01' 扫描了2亿行,锁升级为表锁,连 SELECT TOP 1 * FROM Logs 都超时。后来我们加了作业每晚自动 UPDATE STATISTICS ... WITH SAMPLE 25 PERCENT ,再没发生过类似事故。

5.4 锁问题速查表:按症状反推根因

症状 可能根因 快速验证命令 解决方案
SELECT被阻塞, blocking_session_id 为空 会话处于 suspended 状态,等待CPU/IO SELECT session_id, wait_type, wait_time_ms FROM sys.dm_exec_requests WHERE status='suspended' 检查服务器CPU、磁盘队列长度
UPDATE语句长时间运行, sys.dm_tran_locks 无记录 语句在编译执行计划,未进入执行阶段 SELECT * FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) WHERE qs.execution_count = 0 清理计划缓存或加 OPTION(RECOMPILE)
死锁频繁发生,但SQL都很简单 多个存储过程以不同顺序访问相同表 SELECT * FROM sys.dm_exec_procedure_stats WHERE database_id = DB_ID('YourDB') ORDER BY total_logical_reads DESC 统一表访问顺序,或用 sp_getapplock 应用层加锁
LCK_M_U 等待时间长 UPDATE的WHERE条件未走索引,导致全表扫描加U锁 SELECT * FROM sys.dm_db_index_usage_stats WHERE object_id = OBJECT_ID('YourTable') 为WHERE字段建索引,或用 INDEX() 提示强制索引

最后分享一个小技巧:在开发环境模拟锁阻塞,不用等生产事故。开两个SSMS窗口,窗口A执行:

BEGIN TRAN
UPDATE Products SET Price = Price * 1.1 WHERE ProductID = 1;
-- 不COMMIT

窗口B执行:

SELECT * FROM Products WITH (HOLDLOCK) WHERE ProductID = 1; -- HOLDLOCK等价于SERIALIZABLE

此时窗口B会卡住,用前述DMV脚本就能实时观察锁行为。这是比读文档高效十倍的学习方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值