根本原因是将并发安全交由应用层逻辑承担,而触发器无事务隔离能力,查+算+赋值无法原子执行;高并发下select ... order by id desc limit 1必然重复,必须用独立计数表+last_insert_id()原子递增并配unique约束兜底。

根本原因不是触发器写错了,而是把本该由存储引擎保障的并发安全,交给了应用层逻辑去扛——而触发器本身不具备事务隔离能力,查+算+赋值三步无法原子执行。
BEFORE INSERT 里 SELECT ... ORDER BY id DESC LIMIT 1 必然重复
这是最典型也最危险的写法。看起来“取最大值+1”很直观,但高并发下多个会话同时执行这条 SELECT,查到的是同一行旧数据,结果都生成相同流水号。
- MySQL 的
SELECT ... FOR UPDATE在触发器内不被允许,会直接报错 - 即使能加锁,
SELECT ... ORDER BY ... LIMIT 1无法命中索引时还会触发全表扫描,进一步放大竞争窗口 - PostgreSQL 中类似写法同样不可靠,
SELECT ... ORDER BY ... LIMIT 1在不同事务快照下可能返回不同结果
没用计数表或序列,靠 UPDATE + LAST_INSERT_ID() 却漏了原子性保障
MySQL 没原生序列可用,但很多人误以为 UPDATE seq_counter SET counter = counter + 1 就够了——这行 SQL 本身不是原子递增,必须配合 LAST_INSERT_ID() 才行。
- 错误写法:
UPDATE seq_counter SET counter = counter + 1 WHERE biz_type = 'SO'→ 多个会话读到同一 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 再 UPDATE 同表,导致逻辑错乱或报错
想“先插入再补流水号”,这条路在绝大多数数据库里走不通。
- MySQL 报错:
Can't update table 'orders' in stored function/trigger - PostgreSQL 报错:
mutating table(触发器中不能修改正在被操作的表) - 即便绕过限制(如用事件调度器延后更新),也失去事务一致性:插入成功但流水号没写上,业务就断了
-
NEW.bill_no在AFTER INSERT里已不可写,赋值静默失败,不会报错但也不生效
没配 UNIQUE 约束兜底,把防重全押在触发器上
触发器只是预处理钩子,不是数据校验终点。哪怕流水号生成逻辑完全正确,中间环节出任何偏差(比如字段类型隐式转换、空格截断、大小写不敏感比对),都会让重复漏网。
- 必须给
bill_no字段加UNIQUE约束,这是唯一能真正拦截并发冲突的机制 - 触发器只负责格式填充和简单校验(如
business_type是否合法),不承担“是否存在”的判定职责 - 应用层要捕获错误码
1062(MySQL)或23505(PostgreSQL),而不是依赖触发器SIGNAL——因为约束检查总在触发器之后执行
真正难的不是写出能跑的触发器,而是接受一个事实:数据库的并发安全不靠“聪明的 SQL”,而靠“笨但稳”的机制——唯一索引 + 原子计数 + 应用层幂等识别。漏掉其中任一环,重复就只是时间问题。











