MySQL存储过程不能直接用AUTO_INCREMENT生成业务序列号,必须用SELECT...FOR UPDATE加独立序列表实现原子性取号,否则高并发下必重复或跳号。

MySQL 存储过程中不能直接用 AUTO_INCREMENT 生成业务序列号
因为 AUTO_INCREMENT 是表级属性,只在 INSERT 时由引擎自动填充主键,无法在存储过程里“取一个未被占用的下一个号”再做其他逻辑。业务上常见的「订单号」「单据号」需要前缀+日期+流水号(如 ORD202410250001),必须自己维护序列逻辑。
用 SELECT ... FOR UPDATE + 独立序列表最稳妥
这是唯一能保证高并发下不重复、不跳号、可回滚的方案。核心是把“取号”变成一个带行锁的原子操作:
- 建一张专用表:
CREATE TABLE seq_counter (name VARCHAR(50) PRIMARY KEY, next_val BIGINT NOT NULL DEFAULT 1); - 插入初始值:
INSERT INTO seq_counter (name, next_val) VALUES ('order_no', 1); - 在存储过程中这样取号:
BEGIN DECLARE v_next BIGINT; SELECT next_val INTO v_next FROM seq_counter WHERE name = 'order_no' FOR UPDATE; UPDATE seq_counter SET next_val = next_val + 1 WHERE name = 'order_no'; -- 此时 v_next 就是本次唯一分配的序号 END
- 注意:整个操作必须在事务内(
START TRANSACTION开启),否则FOR UPDATE无效
LAST_INSERT_ID() 不适合业务序列号生成
它只返回最近一次 INSERT 语句产生的自增主键值,且有严重限制:
- 仅对当前连接有效,跨连接不可见
- 依赖真实表的
AUTO_INCREMENT字段,无法按需格式化(比如加前缀、补零) - 如果中间有其他
INSERT操作(哪怕在另一个表),值就被覆盖了 - 在存储过程中调用
LAST_INSERT_ID(expr)只是设置返回值,并不真正“生成”新号,容易误以为它能当序列器用
避免用 MAX(id)+1 或时间戳拼接
这两种方式在并发场景下必然出问题:
-
SELECT MAX(seq) + 1 FROM xxx没锁,两个会话同时查到同一个最大值,生成重复号 -
CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(FLOOR(RAND()*10000), 4, '0'))纯随机/时间戳,不保证唯一、不可预测、无法排序 - 有人想用
UUID()或SYS_GUID(),但它们是字符串、无序、太长,不适合作为单据号主键或索引字段
真正要落地的序列号,本质是「带锁的计数器读-改-写」,没有捷径。漏掉事务或锁,上线后第一个大促就暴露。


















