MySQL乐观锁需由应用层实现,核心是带版本校验的原子UPDATE:UPDATE t SET v=v+1 WHERE id=? AND version=?;触发器无法前置拦截,不能替代应用层条件拼装与ROW_COUNT()校验。

触发器不能实现真正的乐观锁
MySQL 触发器本身不适用于实现乐观锁。乐观锁的核心是「应用层控制版本比对 + 原子性 UPDATE 条件校验」,而触发器在 UPDATE 执行过程中才被调用,无法干预 WHERE 条件的构造,更无法阻止已满足条件的更新发生。你写一个 BEFORE UPDATE 触发器去检查 NEW.version != OLD.version + 1,然后 SIGNAL SQLSTATE '45000' 报错?这已经晚了——UPDATE 语句早已通过语法解析、权限校验、MVCC 可见性判断,甚至可能已加行锁;触发器只是最后一道钩子,不是前置拦截开关。
为什么不能把 version 自增逻辑塞进 BEFORE UPDATE 触发器
常见误解是:让触发器自动做 SET NEW.version = OLD.version + 1,省得每次 SQL 都手写。这会导致严重问题:
- 触发器无法感知“本次更新是否应生效”——它不知道你这次 UPDATE 是不是带着正确的旧
version来的 - 如果业务 SQL 漏写了
WHERE version = ?,触发器照常自增,等于直接绕过乐观锁校验 - 多个并发 UPDATE 同时命中同一行,触发器看到的
OLD.version是事务开始时 MVCC 快照里的值,不是当前最新值,比对失效 - 触发器里读不到当前事务外的最新
version,没法做真实 CAS 判断
真正起作用的只有这一条 UPDATE 语句结构
乐观锁生效的唯一可靠路径,是应用层拼出带完整条件和自增动作的单条 UPDATE:
UPDATE t_order SET status = ?, version = version + 1 WHERE id = ? AND version = ?;
关键点在于:
-
WHERE id = ? AND version = ?中的version = ?必须是应用层从上一次SELECT拿到的旧值,不能由数据库生成或触发器代劳 -
version = version + 1必须显式出现在SET子句中,确保更新后版本推进 - 执行后必须检查
ROW_COUNT():返回 0 表示条件不匹配,即已被其他事务抢先更新 - 不要混用
SELECT ... FOR UPDATE—— 那是悲观锁语义,跟乐观锁设计目标冲突
ORM 框架里怎么避免触发器式幻觉
MyBatis-Plus 的 @Version 注解、JPA 的 @Version 字段,其生效前提是走框架封装的 update 方法(如 updateById 或 save)。一旦你写 @Update("UPDATE ...") 或原生 JDBC executeUpdate,框架就不再介入——此时 version 条件和自增逻辑全靠你自己写进 SQL 里。容易踩的坑包括:
- MyBatis-Plus 开启
OptimisticLockerInnerInterceptor后,仅对lambdaUpdate().eq("id", x).set("status", y)这类链式调用自动注入AND version = ?,但不会帮你改SET version = version + 1 - JPA 的
@Modifying @Query完全不识别@Version,必须手动拼WHERE version = :oldVersion和SET version = version + 1 - 所有自定义 SQL 场景下,
version参数必须作为入参显式传入,且与 SELECT 时拿到的值严格一致
最危险的盲区是:你以为用了 @Version 就万事大吉,结果在某个批量更新接口里切到了原生 SQL,漏掉 WHERE 条件——那行数据就彻底裸奔了。











