不能靠存储过程生成全局id,因其运行于单实例内,无法保证跨库唯一、锁竞争严重、主从不一致,且auto_increment_offset等变量不可在会话中动态设置,仅能作为id分发中转而非生成主体。

MySQL存储过程本身不适合承担分库分表后的全局唯一ID生成任务——它缺乏跨库原子性、无法规避单点瓶颈,且在高并发下极易成为性能拖累。
为什么不能靠存储过程生成全局ID?
存储过程运行在单个MySQL实例内,而分库分表意味着数据分散在多个物理库/表中。你想用一个存储过程“统一发号”,就会立刻撞上三个硬伤:
- 无法保证跨库唯一:
INSERT ... SELECT LAST_INSERT_ID()只对当前连接生效,其他分库实例完全不可见 - 锁竞争严重:若用序列表(如
Tickets64)+REPLACE INTO+LAST_INSERT_ID(),所有请求串行排队,QPS 上不去 - 主从不一致风险:若存储过程依赖
NOW()或UUID()等非确定性函数,在语句复制(SBR)模式下,从库执行结果与主库不同
auto_increment_offset + auto_increment_increment 配置必须在会话外完成
有人试图在存储过程中动态执行 SET SESSION auto_increment_offset = 1,这是无效的——MySQL 不允许在存储过程中修改这些系统变量。它们只能通过以下方式设置:
- 全局配置:在
my.cnf中写auto-increment-offset = 1和auto-increment-increment = 10,然后重启 mysqld - 启动时指定:mysqld 启动参数加
--auto-increment-offset=1 --auto-increment-increment=10 - 运行时全局设置:
SET GLOBAL auto_increment_offset = 1(需 SUPER 权限,且只影响后续新连接)
注意:SET SESSION 对这两个变量完全无效,任何尝试都会报错 ERROR 1238 (HY000): Variable 'auto_increment_offset' is a GLOBAL variable。
如果非要写存储过程,只能做“ID分发中转”,而非“ID生成”
真正可行的做法是:让存储过程只负责从已有的、外部可靠的ID池里取值,比如查一张中心化的 global_id 表,并用 SELECT ... FOR UPDATE 加行锁批量预占。示例逻辑如下:
DELIMITER ;;
CREATE PROCEDURE get_batch_id(OUT start_id BIGINT, OUT end_id BIGINT, IN batch_size INT)
BEGIN
DECLARE current_max BIGINT DEFAULT 0;
START TRANSACTION;
SELECT max_id INTO current_max FROM global_id FOR UPDATE;
UPDATE global_id SET max_id = max_id + batch_size;
SET start_id = current_max + 1;
SET end_id = current_max + batch_size;
COMMIT;
END;;
DELIMITER ;
但要注意:
- 这张
global_id表本身就是单点,必须独立部署、高可用(如双主+VIP),否则整个ID系统就垮了 -
batch_size不能太大(建议 ≤ 1000),否则大事务阻塞时间长;也不能太小,否则频繁调用开销大 - 应用层必须自己维护“已分配未使用”的ID缓存,否则每次插入都调一次存储过程,性能崩盘
真正健壮的方案,是把ID生成彻底移出MySQL——用 UUID_SHORT()(需严格校验 server_id)、Snowflake服务,或 Leaf 这类带DB兜底的中间件。存储过程只该干它擅长的事:封装查询、校验、简单聚合,而不是挑战分布式一致性的底层难题。











