MySQL触发器中用SELECT MAX(id)+1生成ID必然并发撞车,因多个事务同时读到相同MAX值导致主键冲突;正确做法是用原子递增(如INSERT ON DUPLICATE KEY UPDATE或序列),并按业务维度分表计数、统一日期函数、校验前缀白名单。

MySQL触发器里用SELECT MAX(id)+1必然撞车
并发 INSERT 时多个触发器同时执行 SELECT MAX(id) FROM orders,结果都是 100,接着全设 NEW.id = 101,主键冲突直接报错 ERROR 1062 (23000): Duplicate entry '101' for key 'PRIMARY'。这不是概率问题,是确定性失败。
- 别信“我测试几十次都没事”——压测到 50+ QPS 就大概率复现
-
MAX(id)在 RR 隔离级别下会加间隙锁,还可能互相阻塞,引发死锁 - 即使加了
FOR UPDATE,锁的是索引范围,不是单行,性能差且仍不保序
SQL Server必须避开AFTER INSERT查自身表
在 AFTER INSERT 触发器里写 SELECT MAX(order_no) FROM orders,会立刻报错 ERROR 1442。SQL Server 内核禁止同一语句上下文中对目标表做读操作,哪怕加 WITH (NOLOCK) 也无效。
- 唯一可行路径是改用
INSTEAD OF INSERT,把插入拆成“先算号、再插入”两步 - 或彻底剥离逻辑:建独立计数表
order_counter,用MERGE或带HOLDLOCK的UPDATE更新计数 - 注意:计数表主键必须是业务维度(如
date_key),不能只靠自增ID
所有数据库都该用原子递增代替手工计算
真正能扛住并发的编号生成,核心就一条:用单条 DML 完成“读-改-回”三步,避免 SELECT + UPDATE 拆开。
- MySQL:用
INSERT ... ON DUPLICATE KEY UPDATE配合LAST_INSERT_ID(),例如:UPDATE seq_counter SET counter = LAST_INSERT_ID(counter + 1) WHERE date_key = CURDATE() - PostgreSQL:直接调
NEXTVAL('seq_so'),序列天生原子,无需额外锁 - SQL Server:用
MERGE或UPDATE ... WITH (HOLDLOCK),确保计数行被独占锁定 - 关键点:计数表每行对应一个业务维度(日期/类型/租户),别堆在一个字段里
前缀拼接和补零必须在触发器内完成,但别碰NOW()多次
流水号格式如 'SO-20260721-000123',日期部分若在触发器里调两次 NOW(),跨秒时可能变成 'SO-20260721-000123' 和 'SO-20260722-000123',数据错乱。
- 统一用
CURDATE()(MySQL)或CURRENT_DATE(PG/SQL Server)取日期,它在同一事务中恒定 - 数字部分用
LPAD(@next_no, 6, '0')补零,别用字符串拼接再转数字 - 前缀从
NEW.business_type动态取,但必须校验白名单,防止NULL或非法值导致CONCAT返回NULL

















