AFTER UPDATE触发器不能做乐观锁校验,因其在更新完成、行已落盘后才执行,无法获取用户读取时的原始version值,校验失去意义;真正有效的乐观锁需在单条UPDATE语句中通过WHERE version = @expected_version校验并结合@@ROWCOUNT判断是否生效。

触发器无法实现真正的乐观锁机制——它既不满足“检查—更新”原子性,也无法安全暴露原始旧值用于校验。
为什么AFTER UPDATE触发器不能做乐观锁校验
乐观锁的核心是“读取时记下版本 → 提交时验证未变 → 失败则拒绝”,而AFTER UPDATE触发器在UPDATE语句执行完成、行已落盘后才被调用。此时冲突早已发生,你看到的deleted和inserted表里的数据都是更新后的状态,无法还原“用户上次读到的version是多少”。常见错误写法如:
IF EXISTS (SELECT 1 FROM inserted i JOIN deleted d ON i.id = d.id WHERE i.version != d.version + 1) ROLLBACK
这段逻辑不是校验,是补救;且d.version已经是新值,校验恒成立或恒失败,完全失去意义。
INSTEAD OF UPDATE触发器也拿不到OLD行原始值
SQL Server 不向INSTEAD OF UPDATE触发器暴露原始未修改前的行(即传统意义上的OLD值)。你只能查当前表,但并发下SELECT可能立刻读到其他事务刚提交的新值,竞态窗口依然存在。更危险的是,触发器内再执行SELECT或UPDATE会引入额外锁,极易与主语句形成死锁,且事务边界不可控。
- 无法保证触发器内DML与主语句同属一个事务快照
- 没有
OLD.version可用,所有“基于旧值”的判断都成空谈 - 触发器报错(如
SIGNAL)返回的是SQL异常,应用层无法区分是乐观锁失败还是语法错误、权限不足等其他问题
真正有效的乐观锁必须由单条UPDATE语句+应用层校验共同完成
把版本校验、业务约束、字段更新全部压进一条UPDATE,并依赖@@ROWCOUNT(SQL Server)、ROW_COUNT()(MySQL)或GET DIAGNOSTICS(PostgreSQL)判断是否生效:
UPDATE inventory SET qty = qty - @delta, version = version + 1 WHERE sku = @sku AND qty >= @delta AND version = @expected_version;
关键点:
-
@expected_version必须来自上一次SELECT,且不能缓存复用;重试前必须重新查询最新version - WHERE中漏掉
AND version = ?,等于没加锁 - 索引必须覆盖
sku和version,否则WHERE走全表扫描,锁升级风险陡增 - ORM用户注意:MyBatis-Plus的
@Version、JPA的@Version只对框架生成SQL生效;手写SQL必须手动拼AND version = #{version}
最易被忽略的一点:乐观锁失效往往不是因为不会写SQL,而是因为没检查@@ROWCOUNT——数据库从不因WHERE不匹配而报错,它安静返回0行影响,程序若不主动判断,就等于默许覆盖他人修改。

















