UPDATE本身不防死锁,死锁源于多事务以不同顺序争抢资源;需确保索引有效、避免子查询/JOIN、用单条批量语句、慎用UPDLOCK、优先用条件UPDATE+@@ROWCOUNT校验、唯一约束防数据异常但不防死锁。

UPDATE语句本身不防死锁,但写法决定是否容易触发
死锁不是UPDATE命令的bug,而是多个事务以不同顺序争抢资源时形成的循环等待。最典型场景:事务A先UPDATE orders再UPDATE inventory,事务B反过来操作——两边都卡在第二步,互相等对方释放第一把锁。
关键点在于:SQL Server不会自动帮你按主键顺序加锁,它只按语句执行路径和索引扫描顺序锁定行。所以哪怕你UPDATE同一张表,WHERE id IN (100, 200) 和 WHERE id IN (200, 100) 在无索引或统计信息过期时,可能锁定顺序相反,埋下死锁隐患。
- 必须确保WHERE条件字段有有效索引(最好是唯一或主键),避免全表扫描导致锁升级为页锁或表锁
- UPDATE中避免子查询、函数或JOIN,它们会扩大锁范围或打乱预期锁定顺序
- 批量UPDATE尽量用单条语句完成,不要拆成循环+单行UPDATE——每轮都新建锁,窗口期翻倍
WITH (UPDLOCK) 不是保险丝,用错反而加速死锁
很多人以为SELECT + WITH (UPDLOCK)能“提前占位”,结果发现死锁更频繁了。问题出在锁持有时间被拉长,且没配合事务边界控制。
典型错误:SELECT * FROM accounts WITH (UPDLOCK) WHERE user_id = 123 后,调用外部HTTP接口耗时2秒,再执行UPDATE——这2秒内所有想操作user_id=123的请求全被堵住。
-
WITH (UPDLOCK)必须包裹在显式事务中(BEGIN TRANSACTION),否则锁语句一结束就释放 - 优先用
WITH (UPDLOCK, ROWLOCK, HOLDLOCK)组合:ROWLOCK防止锁升级,HOLDLOCK等价于SERIALIZABLE,让锁持续到事务结束 - 如果只是为防覆盖,直接用带条件的UPDATE更轻量,比如
UPDATE accounts SET balance = balance + 50 WHERE id = 123 AND balance >= 50,根本不需要SELECT阶段
影响行数校验比锁提示更可靠
依赖锁机制防冲突,本质是靠“阻塞”;而检查@@ROWCOUNT是靠“结果反馈”。后者不争资源,自然不参与死锁环路。
例如扣减库存:应用层读出当前stock = 10,发起UPDATE products SET stock = 9 WHERE id = 1001 AND stock = 10,执行后立刻查@@ROWCOUNT。为0说明已被别人抢先修改,直接报“库存不足”,而不是重试或等待。
- 这个模式要求WHERE里包含业务约束字段(如
stock、status、version),不能只靠主键 - 务必在UPDATE后**立即**检查
@@ROWCOUNT,中间不能夹杂其他语句,否则值会被覆盖 - 该方案在
READ COMMITTED隔离级别下完全有效,无需抬高到SERIALIZABLE
唯一约束才是最后防线,但它不解决死锁
所有上述手段都依赖开发者写对SQL。一旦漏掉WHERE条件或写错字段,UPDATE仍会覆盖数据——这时唯一索引会拦住非法状态,比如抢座系统中place_id必须建UNIQUE索引,重复占用会报Duplicate key violation。
但它对死锁没帮助:两个事务同时尝试INSERT同一place_id,一个拿到X锁,另一个阻塞等待,等第一个回滚或提交后,第二个才继续——这是阻塞,不是死锁。但如果它们还各自锁了别的资源(比如用户积分表),就可能卷入死锁环。
真正容易被忽略的是:唯一约束必须配合业务逻辑设计。比如“用户只能报名一门课”,不能只在(user_id, course_id)上建唯一索引就完事,UPDATE语句里还得显式检查AND status = 'active',否则软删除记录也会被重复插入。

















