SQL Server里头怎么自己搞锁,举个例子演示下过程和效果
- 问答
- 2025-12-31 08:31:18
- 3
在 SQL Server 里,虽然数据库引擎会自动处理大多数锁的问题,以保证数据的一致性,但有时候我们可能需要自己动手,更精细地控制锁的行为,在一个非常重要的业务流程中,你明确知道需要防止其他人在某个时间段内读取或修改某一行数据,这时候自动锁可能不够直接,或者你想避免某些默认锁行为带来的性能问题,手动控制锁就派上用场了。
手动控制锁的核心,是使用一个叫做 HOLDLOCK 的关键词,但更现代、更推荐的做法是使用设置事务隔离级别,或者在查询中加表提示,这里我们主要讲通过查询加提示这种更直接的方式。
举个例子:模拟一个火车票订票场景
假设我们有一张火车票表,叫做 TrainTickets,结构很简单:
TicketID:票的唯一编号。TrainNo:车次,‘G101’。SeatNo:座位号,‘01车01A’。Status:状态,0 代表未售,1 代表已售。
用户 A 要预订车次 ‘G101’ 的座位 ‘01车01A’,这个操作需要两步:
- 检查这张票的状态是否为 0(未售)。
- 如果未售,将其状态更新为 1(已售)。
问题在于,如果在用户 A 执行完第1步但还没执行第2步的极短时间内,用户 B 也来查询同一张票,他也会看到票是未售,然后也发起购买,这就导致了经典的“超卖”问题。
为了解决这个问题,我们需要在用户 A 查询这张票的时候,就“锁定”它,让其他用户必须等待,直到用户 A 的整个操作(包括更新)完成。
演示过程和效果
我们使用 SQL Server Management Studio (SSMS) 打开两个查询窗口,模拟用户 A 和用户 B。
第一步:准备测试数据
先在任何一个窗口执行,创建表和测试数据:
CREATE TABLE TrainTickets (
TicketID INT PRIMARY KEY IDENTITY(1,1),
TrainNo VARCHAR(10),
SeatNo VARCHAR(10),
Status INT
);
INSERT INTO TrainTickets (TrainNo, SeatNo, Status) VALUES
('G101', '01车01A', 0), -- 这张是未售的票
('G101', '01车01B', 1); -- 这张是已售的票
SELECT * FROM TrainTickets; -- 查看一下数据
第二步:用户 A 开始事务并手动加锁
在查询窗口 A(模拟用户 A) 中,我们开始一个事务,并在查询时使用表提示,这里我们使用两个关键的提示组合:
UPDLOCK:强制在读取数据时使用更新锁,更新锁是一种特殊的锁,它允许其他事务来读取数据(共享锁),但不允许再对其加更新锁或排他锁,这为后续的更新操作“占个坑”。HOLDLOCK(或者它同义的SERIALIZABLE):这个提示会让共享锁或更新锁持续到事务结束,而不是像默认情况下一旦读完数据就释放,这是关键所在,确保了在整个事务过程中,数据都被锁定。
用户 A 执行以下语句:

BEGIN TRANSACTION; -- 开始一个事务 -- 查询票的状态,并手动加上锁 SELECT TicketID, TrainNo, SeatNo, Status FROM TrainTickets WITH (UPDLOCK, HOLDLOCK) WHERE TrainNo = 'G101' AND SeatNo = '01车01A' AND Status = 0; -- 注意:此时事务还没有提交,查询窗口A不要关闭,也不要执行 COMMIT。
执行后,用户 A 会看到 ‘01车01A’ 这条记录,状态是 0,最重要的是,这条记录现在已经被用户 A 的事务锁定了。
第三步:用户 B 尝试操作同一条数据
切换到查询窗口 B(模拟用户 B),用户 B 也想来买这张票。
-
用户 B 尝试普通查询: 用户 B 执行一个简单的查询,看看票卖了没:
SELECT TicketID, TrainNo, SeatNo, Status FROM TrainTickets WHERE TrainNo = 'G101' AND SeatNo = '01车01A’;
效果: 这个查询能正常执行并立即返回结果,因为默认的
READ COMMITTED隔离级别允许读取已提交的数据,而UPDLOCK并不阻止共享锁,所以用户 B 看到的仍然是 Status = 0。 -
用户 B 尝试更新(购票): 用户 B 看到票是未售,于是尝试更新状态来购票:
UPDATE TrainTickets SET Status = 1 WHERE TrainNo = 'G101' AND SeatNo = '01车01A’;
效果: 执行这个语句后,查询窗口 B 会卡住,显示一个旋转的图标,表示正在执行中,这是因为用户 A 的事务持有该记录的更新锁(UPDLOCK),这个锁会阻塞其他事务尝试获取排他锁(X锁)来进行更新,用户 B 的更新操作必须等待用户 A 释放那个锁。

第四步:用户 A 完成操作
现在回到查询窗口 A,用户 A 决定购买这张票,执行更新并提交事务。
-- 在窗口A中,接着之前的SELECT语句执行 UPDATE TrainTickets SET Status = 1 WHERE TrainNo = 'G101' AND SeatNo = '01车01A'; -- 提交事务,释放所有锁 COMMIT TRANSACTION; SELECT '购票成功!' AS Result;
当 COMMIT 执行后,用户 A 的事务结束,它持有的更新锁被释放。
第五步:观察用户 B 的变化
立刻切换回查询窗口 B。
效果: 你会看到之前被卡住的 UPDATE 语句现在执行完毕了,它显示 “(0 行受影响)”,这是因为当用户 B 的更新操作终于获得锁,准备更新数据时,它重新检查了 WHERE 条件,发现此时 Status 已经被用户 A 更新为 1 了,不再满足 Status = 0 的条件(如果UPDATE语句没有WHERE Status=0条件,则会发生覆盖更新,这就是为什么业务逻辑要严谨),所以实际上没有更新任何行,用户 B 的业务逻辑应该检查受影响的行数,如果为0,则提示用户“票已售出”。
总结一下这个过程的效果:
通过手动在查询中添加 WITH (UPDLOCK, HOLDLOCK) 提示,我们成功地:
- 实现了悲观锁: 我们“悲观”地认为并发冲突会发生,所以先占住锁。
- 防止了超卖: 确保了在用户 A 的整个“查询-判断-更新”业务逻辑过程中,其他事务无法更改这条关键数据。
- 演示了阻塞: 直观地展示了锁如何导致一个会话等待另一个会话。
需要注意的是,手动加锁是一把双刃剑,用得好可以解决并发问题,用得不好(比如锁住大量数据或锁住时间过长)会导致严重的性能瓶颈和死锁,它通常只用于对数据一致性要求极高、且并发冲突概率大的关键场景,在实际应用中,你需要根据具体的业务逻辑来选择最合适的锁提示和事务范围。
(主要参考来源:Microsoft Learn 官方文档中的“表提示 (Transact-SQL)”和“锁模式”相关章节)
本文由太叔访天于2025-12-31发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/71780.html
