根本原因在于将并发安全责任错误地交给应用层,触发器缺乏事务隔离能力导致查+算+赋值无法原子执行;典型错误是BEFORE INSERT中用SELECT MAX()+1生成流水号,高并发下必然重复,且LIKE和ORDER BY等操作加剧锁竞争,UPDATE counter=counter+1非原子,正确做法是UPDATE SET counter=LAST_INSERT_ID(counter+1),并确保biz_type有UNIQUE索引,AFTER INSERT中修改NEW.bill_no无效,且必须配置UNIQUE约束兜底,防范隐式转换、大小写比对、字符集等问题。

根本原因不是触发器写错了,而是把本该由存储引擎保障的并发安全,交给了应用层逻辑去扛——而触发器本身不具备事务隔离能力,查+算+赋值三步无法原子执行。
BEFORE INSERT 里 SELECT MAX() + 1 必然重复
这是最典型也最危险的写法。看起来“取最大值+1”很直观,但高并发下多个会话同时执行 SELECT MAX(id) FROM orders WHERE DATE(create_time) = CURDATE(),查到的是同一行旧数据,结果都生成相同流水号。
- 即使加了
WHERE order_no LIKE 'ORD20260811%',LIKE 无法走索引,触发全表扫描,锁竞争窗口更大 -
ORDER BY id DESC LIMIT 1同样不可靠:若没索引,MySQL 可能用文件排序;即使有索引,不同事务快照下也可能返回不同结果 - MySQL 不允许在触发器内使用
SELECT ... FOR UPDATE(会直接报错),所以无法靠锁补救
UPDATE seq_counter SET counter = counter + 1 不是原子递增
很多人误以为这行 SQL 本身就能保证唯一性,但它不是原子操作:多个会话读到同一 counter 值,各自 +1 后写回,最终只 +1 一次。
- 正确写法必须是:
UPDATE seq_counter SET counter = LAST_INSERT_ID(counter + 1) WHERE biz_type = 'SO' -
LAST_INSERT_ID()是会话级函数,保证每个连接拿到自己更新后的值 - 漏掉
biz_type的UNIQUE索引,会导致 WHERE 匹配多行,LAST_INSERT_ID()返回不确定值
AFTER INSERT 触发器里改流水号根本行不通
想“先插入再补流水号”,这条路在 MySQL 里走不通:
- MySQL 报错:
Can't update table 'orders' in stored function/trigger -
NEW.bill_no在AFTER INSERT里已不可写,赋值静默失败,不会报错但也不生效 - 绕过限制(如用事件调度器延后更新)会破坏事务一致性:插入成功但流水号没写上,业务就断了
没配 UNIQUE 约束兜底,把防重全押在触发器上
触发器只是预处理钩子,不是数据校验终点。哪怕流水号生成逻辑完全正确,中间环节出任何偏差都会让重复漏网:
- 字段类型隐式转换(比如
VARCHAR字段存数字时自动截断空格) - 大小写不敏感比对(
bill_no设为utf8mb4_general_ci,'ORD001'和'ord001'被视为重复) - 字符集不一致导致拼接异常(如
CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(@n, 4, '0'))中 @n 为 NULL,结果变成'ORD20260811')
真正容易被忽略的点是:你写的每一条 SELECT、UPDATE、INSERT 都在默认事务隔离级别下运行,而触发器没有自己的事务上下文——它完全依赖外层语句的 autocommit 设置和锁行为。一旦外层是单条语句且 autocommit=1,哪怕用了 FOR UPDATE(在允许的场景下),锁也会立刻释放。


















