mysql可通过事务表+函数模拟oracle序列,最轻量方案是update配合last_insert_id()实现无锁并发;需格式化输出时则必须加for update锁保证原子性。

MySQL 没有原生 SEQUENCE 对象,但用事务表 + 函数可以稳定模拟 Oracle 的 NEXTVAL/CURRVAL 行为。关键不在于“能不能做”,而在于“并发是否安全”和“是否需要锁”。直接上结论:**用 UPDATE ... SET current_val = LAST_INSERT_ID(current_val + increment_val) 配合 LAST_INSERT_ID() 是最轻量、无显式锁、连接隔离的方案;若需前缀/补零/多规则,则必须加 FOR UPDATE,但会带来锁竞争。**
为什么不能只靠 AUTO_INCREMENT 表?
单列自增表(如 CREATE TABLE seq (id BIGINT AUTO_INCREMENT PRIMARY KEY))看似简单,但实际踩坑很多:
-
INSERT INTO seq () VALUES (); SELECT LAST_INSERT_ID();看似可行,但每次调用都写一行,数据无限膨胀,清理困难 - 无法回滚:事务中
INSERT生成了 ID,但事务失败后该 ID 仍被消耗,造成空洞 - 不支持步长、重置、查看当前值等基本操作,更无法实现
CURRVAL - 高并发下
INSERT ... SELECT类操作可能因间隙锁引发死锁
推荐方案:带命名的序列管理表 + 无锁更新
核心是利用 MySQL 的 LAST_INSERT_ID(expr) 函数——它只影响当前连接的会话级返回值,不依赖表锁,且与 UPDATE 原子绑定:
建表:
DROP TABLE IF EXISTS `sequence`; CREATE TABLE `sequence` ( `seq_name` VARCHAR(50) NOT NULL PRIMARY KEY, `current_val` BIGINT NOT NULL DEFAULT 1, `increment_val` INT NOT NULL DEFAULT 1 ) ENGINE=InnoDB;
插入初始值:
INSERT INTO `sequence` VALUES ('order_seq', 1000, 1);
获取下一个值(等效 Oracle NEXTVAL):
UPDATE `sequence` SET `current_val` = LAST_INSERT_ID(`current_val` + `increment_val`) WHERE `seq_name` = 'order_seq'; SELECT LAST_INSERT_ID();
这个组合的关键点:
-
LAST_INSERT_ID()在UPDATE中被赋值后,同一连接后续调用它就返回该值,无需再查表 - 不同连接即使同时执行相同
UPDATE,各自拿到的LAST_INSERT_ID()互不干扰 - 没有
SELECT ... FOR UPDATE,也没有显式LOCK TABLES,吞吐更高 - 步长由
increment_val控制,改表字段即可动态调整
需要格式化输出(如 'ORD-000123')时必须加锁
一旦涉及字符串拼接、补零、前后缀,就必须读取当前值并计算,此时单纯靠 LAST_INSERT_ID() 不够用了。典型场景:生成订单号 ORD-000001、工单号 TICKET-20260411-001。
这时必须用 SELECT ... FOR UPDATE 保证读-改-写原子性:
函数示例:
DELIMITER $$
CREATE FUNCTION nextval_formatted(seq_name VARCHAR(50))
RETURNS VARCHAR(32)
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE next_id BIGINT DEFAULT 0;
DECLARE prefix VARCHAR(20) DEFAULT 'ORD-';
DECLARE len TINYINT DEFAULT 6;
SELECT current_val + increment_val INTO next_id
FROM sequence
WHERE seq_name = seq_name
FOR UPDATE;
UPDATE sequence
SET current_val = next_id
WHERE seq_name = seq_name;
RETURN CONCAT(prefix, LPAD(next_id, len, '0'));
END$$
DELIMITER ;
注意:
-
FOR UPDATE会阻塞其他连接对同一行的读写,高并发下响应延迟明显上升 - 该函数必须在事务内调用,否则隐式提交后锁立即释放,失去保护意义
- 不能在存储过程中嵌套调用多个此类函数,容易触发死锁
- 如果业务允许“跳号”,建议把格式化逻辑移到应用层,数据库只管发纯数字 ID
容易忽略的兼容性细节
MySQL 8.0+ 默认开启 sql_mode=STRICT_TRANS_TABLES,这会导致几个隐蔽问题:
- 如果
sequence表里某条记录被误删,UPDATE影响行为为 0 行,但LAST_INSERT_ID()仍返回上一次值 —— 这不是 bug,是设计行为,务必检查ROW_COUNT() -
LAST_INSERT_ID()在存储函数中不可被直接返回(会报Invalid use of non-aggregated column),必须先SET到变量再RETURN - 函数定义中若漏写
READS SQL DATA或MODIFIES SQL DATA,某些版本(如 5.7)在严格模式下会拒绝创建 -
BIGINT最大值约 9.2e18,但如果你用LPAD(..., 20, '0'),超过 20 位就会截断,别只看类型长度不看业务上限
真正难的从来不是写出来,而是让 nextval('user_id') 在每秒 2000 次请求下不重复、不阻塞、不越界、不悄悄跳号。选方案前,先想清楚你的峰值 QPS 和容忍空洞程度。











