mysql触发器中不能用auto_increment实现字段版本号,必须在before update中设new.version = old.version + 1,并校验null、防止绕过及并发跳号。

触发器里不能直接用 AUTO_INCREMENT 实现字段版本号
MySQL 的 AUTO_INCREMENT 只能作用于主键或唯一索引列,且是表级递增,无法按行、按字段做独立版本控制。想让某字段(比如 version)在每次更新特定列时自动加 1,必须靠触发器手动维护——但得小心绕过“不能在触发器里修改当前行所在表”的限制。
BEFORE UPDATE 触发器中只读取旧值,用 NEW.version := OLD.version + 1
这是最常用也最安全的做法:在 BEFORE UPDATE 阶段,OLD 和 NEW 都可读写,但只能改 NEW(即即将写入的值)。只要不碰 OLD 或执行额外 UPDATE,就不会触发递归或报错。
- 只对目标字段变化才累加?需显式判断:
IF OLD.name != NEW.name OR OLD.email != NEW.email THEN NEW.version := OLD.version + 1; END IF; - 如果所有字段更新都算版本变更,直接
SET NEW.version = OLD.version + 1;即可 - 注意 NULL 比较:用
IS NOT DISTINCT FROM或(OLD.x NEW.x)替代!=,避免 NULL 导致误判 - 初始值必须设好(如建表时
version INT DEFAULT 0),否则OLD.version为 NULL,+1结果仍是 NULL
避免触发器被绕过:禁止直接 UPDATE ... SET version = ?
用户如果在 SQL 中显式指定 version 值,会覆盖触发器逻辑。要强制走自动累加,得加一层防护:
- 把
version列设为GENERATED ALWAYS AS (…)不现实(MySQL 不支持基于行的生成列做累加) - 更可行的是在触发器里校验:
IF NEW.version != OLD.version + 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'version must be auto-incremented'; END IF; - 或者干脆去掉该字段的写权限,只开放给应用层通过存储过程操作
- 别依赖客户端传值——哪怕加了校验,绕过触发器的
INSERT ... SELECT或批量导入仍可能跳过逻辑
并发更新下 version 累加不是原子的,可能重复或跳号
触发器本身不提供行级锁保障。两个事务同时读到同一 OLD.version,各自加 1 后写入,结果只+1而非+2。这不是 bug,而是设计使然——它只保证“每次成功更新都推进版本”,不保证严格连续。
- 若业务要求强一致性(比如乐观锁校验失败必须重试),应配合
WHERE version = ?条件更新,并检查ROW_COUNT() - 不要用
version做分页或排序依据,跳号会导致漏数据 - 高并发场景下,纯触发器方案不如应用层用
UPDATE t SET ..., version = version + 1 WHERE id = ? AND version = ?可控
BEFORE UPDATE 里真正修改了 NEW.version,以及有没有其他地方(比如 ORM、迁移脚本)绕过触发器直接赋值。版本号逻辑越简单,越容易被忽略掉这些缝隙。











