如何通过触发器利用版本戳有效防止SQL并发更新数据覆盖?
- 内容介绍
- 相关推荐
本文共计981个文字,预计阅读时间需要4分钟。
触发器不能替代应用层的验证。MySQL触发器在`BEFORE UPDATE`阶段可以读取旧值、修改新值,但无法阻止已完成的SQL执行——例如,你写了如下SQL:
常见错误现象:
- 触发器里写了
IF NEW.version != OLD.version + 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'version mismatch'; END IF;,但应用层根本没传version字段,NEW.version是NULL或默认值,条件永远不成立 - ORM 自动生成 SQL 时跳过
version字段,触发器拿到的NEW.version和OLD.version相等,误判为合法更新
真能用触发器做版本拦截?只适用于极窄场景
仅当所有写入口严格受控(比如禁止 ORM、禁止直连客户端、全走存储过程),且业务允许“强制 version 自增+校验”时,才可考虑 BEFORE UPDATE 触发器。但它必须同时满足:
- 应用层每次 UPDATE 都显式带上
version字段(哪怕只是传原值) - 触发器逻辑是:若
NEW.version != OLD.version,则允许;否则SIGNAL中断 - 表结构中
version字段不能设默认值或ON UPDATE CURRENT_TIMESTAMP,否则会干扰比对
示例触发器(仅作示意,不推荐生产使用):
DELIMITER $$ CREATE TRIGGER check_version_before_update BEFORE UPDATE ON order_info FOR EACH ROW BEGIN IF NEW.version != OLD.version THEN SET NEW.version = OLD.version + 1; ELSE SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'optimistic lock failed'; END IF; END$$ DELIMITER ;
注意:NEW.version 必须由应用层传入,不能依赖触发器自动生成——否则所有并发请求都塞同一个 version,校验形同虚设。
为什么别把版本校验逻辑扔给触发器
核心问题不在技术可行性,而在控制权和可观测性:
- 影响行数(
ROW_COUNT())在应用层才能拿到;触发器报错后 JDBC 返回的是 SQL 异常,不是“0 行影响”,上层无法区分是校验失败还是其他数据库错误 - 批量更新时,触发器逐行执行,但应用层需要统一处理“部分成功”场景,而触发器中断后事务已回滚,重试逻辑更难对齐
- MyBatis、JPA 等框架的乐观锁机制(如
@Version)依赖返回值做判断,触发器屏蔽了这个信号链 - 排查时发现数据被覆盖,第一反应查 SQL 日志——但日志里只看到
UPDATE ... WHERE id = ?,根本看不出version是否参与了条件
真正该做的:让 version 出现在 WHERE 里,且立刻检查返回值
所有安全的并发更新,本质都是“单条 SQL 原子判断 + 应用层反馈验证”。触发器加一层,反而模糊了这根关键链条。
- 手写 SQL 时,
WHERE子句必须包含id = ? AND version = ?,缺一不可 - 用 MyBatis,
<update>标签里显式拼AND version = #{version},别信“自动乐观锁”配置 - JPA 中
@Version字段类型必须是Integer或Long,short在高并发下溢出后,WHERE version = -32768可能意外命中旧记录 - 执行完
executeUpdate(),第一件事就是判断返回值:if (rows == 0) throw new OptimisticLockException();
最容易被忽略的一点:很多 SDK 或中间件把 rowsAffected == 0 当成“无操作”静默吞掉,而不是抛异常。只要这一环松动,乐观锁就等于没开。
本文共计981个文字,预计阅读时间需要4分钟。
触发器不能替代应用层的验证。MySQL触发器在`BEFORE UPDATE`阶段可以读取旧值、修改新值,但无法阻止已完成的SQL执行——例如,你写了如下SQL:
常见错误现象:
- 触发器里写了
IF NEW.version != OLD.version + 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'version mismatch'; END IF;,但应用层根本没传version字段,NEW.version是NULL或默认值,条件永远不成立 - ORM 自动生成 SQL 时跳过
version字段,触发器拿到的NEW.version和OLD.version相等,误判为合法更新
真能用触发器做版本拦截?只适用于极窄场景
仅当所有写入口严格受控(比如禁止 ORM、禁止直连客户端、全走存储过程),且业务允许“强制 version 自增+校验”时,才可考虑 BEFORE UPDATE 触发器。但它必须同时满足:
- 应用层每次 UPDATE 都显式带上
version字段(哪怕只是传原值) - 触发器逻辑是:若
NEW.version != OLD.version,则允许;否则SIGNAL中断 - 表结构中
version字段不能设默认值或ON UPDATE CURRENT_TIMESTAMP,否则会干扰比对
示例触发器(仅作示意,不推荐生产使用):
DELIMITER $$ CREATE TRIGGER check_version_before_update BEFORE UPDATE ON order_info FOR EACH ROW BEGIN IF NEW.version != OLD.version THEN SET NEW.version = OLD.version + 1; ELSE SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'optimistic lock failed'; END IF; END$$ DELIMITER ;
注意:NEW.version 必须由应用层传入,不能依赖触发器自动生成——否则所有并发请求都塞同一个 version,校验形同虚设。
为什么别把版本校验逻辑扔给触发器
核心问题不在技术可行性,而在控制权和可观测性:
- 影响行数(
ROW_COUNT())在应用层才能拿到;触发器报错后 JDBC 返回的是 SQL 异常,不是“0 行影响”,上层无法区分是校验失败还是其他数据库错误 - 批量更新时,触发器逐行执行,但应用层需要统一处理“部分成功”场景,而触发器中断后事务已回滚,重试逻辑更难对齐
- MyBatis、JPA 等框架的乐观锁机制(如
@Version)依赖返回值做判断,触发器屏蔽了这个信号链 - 排查时发现数据被覆盖,第一反应查 SQL 日志——但日志里只看到
UPDATE ... WHERE id = ?,根本看不出version是否参与了条件
真正该做的:让 version 出现在 WHERE 里,且立刻检查返回值
所有安全的并发更新,本质都是“单条 SQL 原子判断 + 应用层反馈验证”。触发器加一层,反而模糊了这根关键链条。
- 手写 SQL 时,
WHERE子句必须包含id = ? AND version = ?,缺一不可 - 用 MyBatis,
<update>标签里显式拼AND version = #{version},别信“自动乐观锁”配置 - JPA 中
@Version字段类型必须是Integer或Long,short在高并发下溢出后,WHERE version = -32768可能意外命中旧记录 - 执行完
executeUpdate(),第一件事就是判断返回值:if (rows == 0) throw new OptimisticLockException();
最容易被忽略的一点:很多 SDK 或中间件把 rowsAffected == 0 当成“无操作”静默吞掉,而不是抛异常。只要这一环松动,乐观锁就等于没开。

