触发器无法实现列级乐观锁,因其缺乏“检查—更新”原子性:after触发器在更新后才执行,instead of无法获取原始旧值,且内部dml引入额外锁与死锁风险;真正有效的方式是单条update语句中合并版本校验与业务约束,并由应用层严格检查@@rowcount。

触发器不能实现列级别的并发乐观锁控制——它既不原子、也不可控,强行使用只会掩盖问题,让超卖、负余额、双写等错误在生产环境静默发生。
为什么触发器做不了乐观锁
乐观锁的核心是“检查—更新”原子性:读取当前值(含版本号或时间戳)→ 在 UPDATE 的 WHERE 子句中校验该值未变 → 成功则更新,失败则拒绝。触发器无法参与这个校验过程:
-
AFTER UPDATE触发器在更新完成之后才运行,此时冲突已成事实,回滚成本高且不可控 -
INSTEAD OF UPDATE虽能拦截,但无法读取“原始旧值”用于比对(SQL Server 不暴露 OLD 行给触发器),你只能查当前值,又回到竞态起点 - 触发器内再执行
SELECT或UPDATE会引入额外锁、死锁风险,且无法保证与主语句同事务边界 - 所有主流文档(包括 Microsoft 官方 SQL Server 锁机制说明)都明确将乐观锁归为应用层或单条语句级设计,而非触发器适用场景
真正有效的列级乐观锁写法(SQL Server)
必须把版本判断和更新合并到一条带条件的 UPDATE 中,并由应用层检查影响行数:
- 表结构需包含版本字段,例如
version INT DEFAULT 0或updated_at DATETIME2 - 执行更新时,WHERE 条件必须同时校验业务约束 + 版本值,例如:
UPDATE inventory SET qty = qty - @delta, version = version + 1 WHERE sku = @sku AND qty >= @delta AND version = @expected_version;
- 用
@@ROWCOUNT判断是否更新成功:为 0 表示被其他事务抢先修改,需重试或报错 - 整个操作必须包裹在显式事务中(
BEGIN TRAN),否则版本校验失去意义 - 确保
sku和version字段有合适索引,避免 WHERE 条件走全表扫描导致锁升级
常见踩坑点:你以为在用乐观锁,其实没生效
这些写法看似像乐观锁,实则完全失效:
- 应用先
SELECT qty, version FROM inventory WHERE sku = 'A123',再拼接字符串构造UPDATE ... WHERE version = xxx—— 中间存在竞态窗口,两个请求可能读到相同version - 触发器里写
IF EXISTS (SELECT 1 FROM inserted i JOIN deleted d ON i.id = d.id WHERE i.version != d.version + 1) ROLLBACK—— 这不是校验,是事后补救,且deleted行在AFTER触发器中已是更新后状态 - 用
UPDLOCK+ 触发器组合:例如SELECT ... WITH (UPDLOCK)后再进触发器做判断 —— 锁只保留在 SELECT 阶段,触发器运行时锁已释放,照样并发 - 版本字段没索引,WHERE version = @v 导致全表扫描,不仅慢,还可能让
UPDLOCK升级为表锁,彻底拖垮并发能力
最易被忽略的一点:乐观锁的有效性完全依赖应用层对 @@ROWCOUNT 的严格检查。哪怕只漏掉一次判断,就会让“乐观”变成“裸奔”。










