最近在做订单中心重构,老项目的自增主键在分表之后让人头疼,跨库存量一大就撞车。原来想引进一套独立的发号器,可为了几个ID就把Redis或者ZK请进来,运维成本确实有点重。后来我想,雪花算法的位结构其实很规整,既然业务表就在MySQL里,能不能直接让MySQL自己算ID?试了一圈,还真能跑,而且用一条SQL就能搞定。这篇文章就把我在MySQL里用纯SQL实现雪花算法ID的完整过程写出来,包括位结构拆解、两种可落地的生成方案、并发踩坑和验证手段,给准备在数据库层解决分布式ID问题的同学一个可参考的思路。
适合这几类人:不想为上号器单独维护组件的中小团队、DBA 想给研发提供无侵入的取号接口、以及纯粹对雪花ID实现细节感兴趣的后端开发。看完你至少能拿到一份可复制的存储函数代码,并且知道在什么并发量下它靠谱、什么量级下应该换方案。
1. 雪花ID的位结构:设计者到底在64位里塞了什么
1.1 一段可解析的64位整数
雪花算法的核心不是“随机”,而是把ID做成一个“能反推出生成时间、机器编号、并发序号”的复合结构。标准版本是一个64位有符号长整型,从高位到低位分成四段:
- 第0位(符号位):固定为0,保证整个ID是正数,避免出现负数ID带来的一堆兼容问题。
- 第1~41位(41位时间戳):记录生成时刻与某个自定义起点(twepoch)之间的毫秒差。41位最多表示约69年的时间跨度,业务上完全够用。
- 第42~51位(10位机器ID):记录生成节点编号,通常拆成5位数据中心和5位工作节点,最多支持1024个节点。
- 第52~63位(12位序列号):记录同一毫秒内生成的第几个ID,范围0~4095,支持单节点单毫秒最多4096个ID。
你可以把它理解成一次发牌:时间戳决定了ID从第几毫秒来,机器ID决定了是哪台机器发的牌,序列号定死了这一毫秒里的第几张牌。三者用位运算拼起来,整个ID还能大致看出生成时刻,这点对排查问题非常友好。
1.2 41位时间戳的边界效应
新手容易忽略一个细节:41位时间戳存的不是完整毫秒时间,而是“当前毫秒值减去起点毫秒值”的差值。选不同起点,直接影响算法能用到哪一年。
比如起点选2000年,这个方案撑不到2040年就溢出了。我一般把起点定在2021年1月1日,也就是1609459200000毫秒,算下来能用接近七十年。起点越近,留给未来的时间余额越充足,但代价是起点之前的时间无法生成ID。真正生产环境只需要保证服务器时间与起点之后对齐,不会有问题。
1.3 机器ID与序列号的权衡空间
10位机器ID和12位序列号并非雷打不动。如果你只有几十台机器,完全可以匀出更多位数给序列号,提升单节点吞吐。比如把机器ID改为5位(支持32节点),序列号扩到17位,单节点单毫秒就能支持131072个ID,吞吐能力翻了好几倍。反过来,如果ID要兼容历史系统或者有特殊排序需求,也可以调整位数分配,但务必留足序列号余量。
实际项目中我见过一种很实用的做法:把机器ID从参数传给生成函数,而不是写死在代码里。这样同一套函数在A库部署时传3、在B库部署时传5,不用改函数体,只需要约定每个节点编号唯一即可。后面给出的SQL方案就采用这个思路,把机器ID做成函数入参。
2. MySQL写雪花ID前,这几项前置知识必须搞清楚
2.1 MySQL位运算:左移、右移、与、或
纯SQL拼雪花ID,离不开位操作。MySQL对整型的位运算支持其实很到位,常见操作符有四个。
- 左移:
a << n,等价于把a的二进制整体向左移动n位,低位补0。拼接时间戳时,需要把毫秒差值左移22位,给机器ID和序列号腾位置。 - 右移:
a >> n,等价于把a的二进制整体向右移动n位。解析ID时,从低到高逐段还原,靠的就是右移加掩码。 - 按位与:
a & b,两个二进制位都是1时结果位为1。解析时用id & 4095取低12位序列号,用(id >> 12) & 1023取中间10位机器ID。 - 按位或:
a | b,任一二进制位为1时结果位为1。最后拼接三段数据,就是通过或运算把三块二进制合并成一个64位整数。
只要记住“左移腾位、或运算拼接、与运算取段”,位运算部分就算过关了。算好各段偏移量,代码写起来和切豆腐一样。
2.2 用户变量和LAST_INSERT_ID()的冷门技巧
SQL方案里最难处理的不是时间戳拼接,而是“同一毫秒内序列号如何自增”。
一种容易想到的做法是查一张序列表,然后加1。但读改写三步在并发下很容易出事,要么重复,要么需要锁表,性能还差。网上有一种巧妙的做法,利用MySQL的LAST_INSERT_ID(expr)特性:这个函数接受参数时,会像AUTO_INCREMENT生成主键一样,把参数值记录为本次会话的“下一个自增值”,之后调用LAST_INSERT_ID()能取回这个值。
配合一条更新语句,就能实现无锁式序列自增:
UPDATE snowflake_seq SET id = LAST_INSERT_ID(id + 1);这条语句先把当前行的id值加1写回去,同时把id + 1注册为会话自增值。紧接着执行SELECT LAST_INSERT_ID()就能拿到更新后的序列值。整个过程由MySQL内部保证,不需要额外加锁。这是整个SQL方案里含金量最高的一段,很多文章都没有点透,建议实操时反复体会一下。
2.3 BIGINT的符号位陷阱
MySQL的BIGINT默认是有符号的,取值范围是-9223372036854775808到9223372036854775807。雪花ID占满64位,若第0位是1,整个ID就会被MySQL当成负数。负数ID在排序、展示、接口联调时非常讨人厌,所以方案里必须保证拼出来的ID最高位始终为0。
好消息是,只要时间戳差值不超过41位左移后的范围,最高位一定是0。真正危险的场景是:
- 服务器时间严重超前于自定义起点,导致差值溢出41位。
- 有人把起点选得过于靠近当前时间,然后故意用负数调用。
- 某些变种算法把符号位也用作数据位。
实操时,我习惯在生成函数末尾对符号位做一次防御性检查:如果返回值小于0,强制返回当前时间戳的低41位重新拼接。之后也给了排查方法,后面第6节详细讲。
2.4 存储函数的创建权限
MySQL对存储函数有一个让新手困惑的报错:
ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled原因很简单:开启了binlog,MySQL不允许创建一个无法确定“是否修改数据、是否确定返回”的函数,否则主从复制时无法正确重放。解决方式是在创建函数时显式声明特性,或者在MySQL配置中打开信任开关:
SET GLOBAL log_bin_trust_function_creators = 1;这个开关需要足够权限,并且它是全局的。如果公司DBA管得严,更推荐在CREATE语句中显式声明函数特性,既过了MySQL的校验,也不影响主从复制。后面给出的完整代码中我直接写了DETERMINISTIC,严格讲这个函数并不是纯确定性的,但这是MySQL校验机制和实际需求之间的常规折中,你完全可以在声明里改成READS SQL DATA。这一点我在常见问题里还会展开。
3. 第一种落地姿势:轻量表达式方案,一条SELECT直接生成
3.1 最简SQL长什么样
如果只是临时脚本、测试环境,或者业务本身就是单线程批量处理,不需要引入存储函数,一条SELECT就能生成雪花ID。先初始化会话自增序列:
SELECT LAST_INSERT_ID(0);然后每执行一次下面这条SQL,就得到一个雪花ID:
SELECT ((FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - 1609459200000) << 22) | (1 << 12) | (LAST_INSERT_ID(LAST_INSERT_ID() + 1) % 4096) AS snowflake_id;你可能会问:LAST_INSERT_ID(LAST_INSERT_ID() + 1)是什么黑魔法?它拆开来看,内层LAST_INSERT_ID()先取出当前会话的自增值,加1后作为参数传给外层LAST_INSERT_ID(expr),注册为新的自增值,整个表达式的值就是expr本身。所以每次执行这条SELECT,序列号都会步进1。再对4096取模,就得到12位序列号。
3.2 轻量方案的边界
这个方案胜在短小精悍,不建表、不建函数,复制到命令行就能跑。但它有一个致命的短板:序列号状态保存在“会话用户变量”里,也就是每个数据库连接各自维护一套。两个连接同时执行这条SQL,拿到的序列号可能是重复的。
所以它只适合下面几种场景:
- 单连接、串行执行的初始化脚本。
- 为测试表批量造数据,不需要高并发。
- 想快速验证雪花算法语义,不想建任何对象。
如果同一个业务库有多个连接在并发写入,直接套用会撞ID。真正想在生产环境用,必须把“序列号来源”从会话变量换成一个所有连接共享的存储载体,这就引出第二种方案。
4. 生产可用方案:带序列表的存储函数实现
4.1 整体设计思路:数据段怎么拼
第二种方案的思路是:用一张只有一行数据的序列表,配合LAST_INSERT_ID(id + 1)技巧,让所有连接共享同一个自增源头。函数负责取时间戳、取序列、拼位段,最后返回BIGINT。
整体拼装公式按位来:
ID = (毫秒时间戳差值 << 22) | (机器ID << 12) | (序列号 % 4096)三段分别占用41位、10位、12位,加起来正好63位有效载荷,最高位符号位空出,符合雪花算法的标准结构。
4.2 建序列表并初始化
先建一张极简的序列表,它只有两列:一个业务主键(id固定为0),一个自增序号生成列。
CREATE TABLE IF NOT EXISTS snowflake_seq ( id TINYINT NOT NULL DEFAULT 0, value BIGINT NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINE = InnoDB; INSERT INTO snowflake_seq VALUES (0, 0) ON DUPLICATE KEY UPDATE value = value;为什么只放一行?这张表的使命就是“所有连接抢同一把锁做原子自增”,一行足矣,行数越多反而让序列号来源不统一。value列从0开始,每调用一次函数就加1,取模后作为序列号。除非每秒百万级写入,否则一个BIGINT的余量足够用到天荒地老。
严格地说,这张表还可以加一个last_ms列来记录上一次使用的时间戳,用于检测同一毫秒内序列耗尽或时钟回拨。我先给最简单版本,把回拨处理放到第6节讲,避免一开始就陷入边界条件。
4.3 完整函数代码与逐段解读
下面是生产可用的存储函数全量代码:
DROP FUNCTION IF EXISTS snowflake_id; DELIMITER $$ CREATE FUNCTION snowflake_id(p_worker_id BIGINT) RETURNS BIGINT DETERMINISTIC BEGIN DECLARE v_ms BIGINT DEFAULT 0; DECLARE v_worker BIGINT DEFAULT 0; DECLARE v_seq BIGINT DEFAULT 0; DECLARE v_twepoch BIGINT DEFAULT 1609459200000; DECLARE v_result BIGINT DEFAULT 0; -- 第一步:取当前毫秒时间戳并减去自定义起点 SET v_ms = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - v_twepoch; -- 第二步:机器ID限定在10位范围内 SET v_worker = p_worker_id & 1023; -- 第三步:从共享序列表获得本次会话的下一个序列号 UPDATE snowflake_seq SET value = LAST_INSERT_ID(value + 1); SET v_seq = LAST_INSERT_ID() % 4096; -- 第四步:三段拼接,得到最终雪花ID SET v_result = (v_ms << 22) | (v_worker << 12) | v_seq; RETURN v_result; END$$ DELIMITER ;逐段解释一下:
第一步:
UNIX_TIMESTAMP(NOW(3))返回带三位毫秒精度的秒值,FLOOR(... * 1000)转成整数毫秒,再减去起点毫秒1609459200000,得到参与位运算的毫秒差值。这里用FLOOR而不是ROUND,是为了避免毫秒进位导致时间戳跳跃。第二步:
p_worker_id & 1023相当于对1024取模,把外部传入的机器ID强制约束在0~1023范围内,防止有人传入一个超出10位的数字把高位位段污染。第三步:前面说的原子自增技巧。每次调用,
value列都会递增,所有连接通过InnoDB行锁串行化这里的更新,不会出现两个连接拿到同一个序列号。第四步:位运算合并三段。
<< 22和<< 12的偏移量必须和位段定义严格对应,一个偏移写错,整个ID结构就乱了。
调用方式很简单:
SELECT snowflake_id(1);在INSERT语句里也可以直接嵌入:
INSERT INTO order_info (order_id, user_id, amount, create_time) VALUES (snowflake_id(1), 10086, 99.00, NOW());4.4 序列号毫秒内耗尽怎么办
标准雪花算法要求单节点单毫秒最多4096个ID。如果某一次的突发流量特别大,同一毫秒内调用了第4097次,这里的v_seq = LAST_INSERT_ID() % 4096会把序列号轮回归零。时间戳没变、序列号又回到0,就可能生成重复ID。
处理这个问题的常见思路是:检测到同一毫秒内序列号已经用了4096个,就让当前线程“蹭”到下一毫秒再生成。问题是纯存储函数里难以保存“上一次的毫秒值”,所以简单版本选择了“取模轮回+容忍风险”。
如果业务不能容忍毫秒级极端情况,可以把序列表扩展一行状态字段,保存上一次使用的毫秒值。每次生成时把当前毫秒与上次毫秒比对,序列号归零且毫秒相等时就调用SLEEP(0.001)蹭到下一秒;不相等就正常生成并更新last_ms。这个方案在函数里写起来完全可以实现,代价是多一次读表,性能损耗不大。
我在很多生产表上直接用了“取模轮回”版本,因为单库单表真正达到每秒409万次生成的场景极少,MySQL自己的写入吞吐早就先顶到天花板了。如果真到了那个量级,你该考虑的不是继续优化这个SQL函数,而是把ID生成挪到应用层用内存批量预取。
4.5 批量生成一批ID的SQL技巧
实际业务还有一个高频需求:一次插几千行或几万行数据,每行都要一个ID。最简单的是在INSERT的VALUES里反复调用函数:
INSERT INTO order_info (order_id, user_id) VALUES (snowflake_id(1), 101), (snowflake_id(1), 102), (snowflake_id(1), 103);把函数调用写在VALUES里是合法的,MySQL执行时会逐行调用,序列号会连续步进。但如果一次生成几万条,SQL文本会变得非常长,而且每条函数调用都要执行一次UPDATE,总体耗时偏长。
更推荐的批量姿势是利用一条SELECT驱动批量生成:
INSERT INTO order_info (order_id, user_id, create_time) SELECT snowflake_id(1) AS order_id, @rn := @rn + 1 AS user_id, NOW() FROM ( SELECT @rn := 0 ) r, information_schema.columns LIMIT 10000;这段SQL的核心是借information_schema.columns这张系统表充当行数发生器,配合用户变量@rn生成1~10000的编号。每行调用一次snowflake_id(1),序列表也只会被顺序更新一万次,整体效率比逐条INSERT好得多。
注意:这里
LIMIT和系统表行数之间要留足余量,否则行数发生器不够用。实际项目里也可以建一张专门用于行数扩充的辅助表,比系统表更稳。
5. 生成之后怎么验证:唯一性检查、ID反解、排序规律
5.1 唯一性检查:最基础也是最重要的验证
函数上线前,我会先在临时表里批量生成一批ID,然后用一条聚合SQL验证是否重复:
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY); INSERT INTO tmp_ids SELECT snowflake_id(1) FROM information_schema.columns LIMIT 100000; SELECT COUNT(*) AS total_cnt, COUNT(DISTINCT id) AS unique_cnt, COUNT(DISTINCT id) / COUNT(*) AS unique_ratio FROM tmp_ids;unique_ratio必须等于1。只要出现一丁点小于1的情况,说明序列号或者时间戳发生了碰撞,需要立即排查序列表状态和函数逻辑。这个临时表验证流程建议在每次改完函数后都跑一遍,特别是改了位偏移量或者序列获取方式之后。
5.2 ID反解工具:从雪花ID还原三段信息
雪花ID最大的魅力之一是“可反解”。遇到线上疑难数据,反解出生成的毫秒时间、机器编号和序列号,往往能直接定位是哪台机器在哪个时间点生成的。
反解SQL非常简单:
SELECT id, ((id >> 22) + 1609459200000) AS generate_ms, ((id >> 12) & 1023) AS worker_id, (id & 4095) AS seq FROM tmp_ids LIMIT 5;(id >> 22)把高位时间戳段挪到最右侧,加上起点毫秒就是完整的时间戳。(id >> 12) & 1023取中间10位机器ID。id & 4095取低12位序列号。还可以顺手转成可读时间:
SELECT FROM_UNIXTIME((((id >> 22) + 1609459200000) / 1000)) AS generate_time FROM tmp_ids LIMIT 5;这套反解SQL不管是排查问题还是做数据血缘分析,都非常实用。建议把它封装成一条固定视图,线上随时查。
5.3 排序规律:不是严格自增,但趋势递增
雪花ID号称“趋势递增”,不代表每相邻两个ID都严格递增。原因是:同一毫秒内序列号在递增,没问题;但跨毫秒时,如果上一毫秒的序列号恰好接近4096,而当前毫秒的序列号从0重新开始,且时间戳段增长量又不大,可能出现后生成的ID数值比先生成的略大但不连续的情况。更准确地说,雪花ID在“毫秒时间戳升序”这一层决定了整体向上趋势,但在同一毫秒边界处不能保证严格单调递增。
大多数业务场景,比如订单按创建时间倒序排列、分页查询、索引页分裂等,趋势递增已经足够了。如果你要求绝对严格递增,需要在生成层额外记录上一毫秒的ID值,发现新ID不大于旧ID时主动等待或取反。这个需求在纯SQL方案里也能实现,但复杂度会上升,非必要不建议。
6. 并发、回拨、重复:常见问题与排查实录
6.1 创建函数报1418错误怎么办
现象是创建函数时报错,提示函数没有DETERMINISTIC、NO SQL或READS SQL DATA其中之一。原因在2.4节里说过,本质是binlog开关和函数确定性校验之间的限制。
优先解决办法:在创建函数时显式加上声明,比如我的代码中保留了DETERMINISTIC,或者换成READS SQL DATA。如果你所在团队不允许在主库创建这类函数,需要走变更审批流程,提前把SQL提交给DBA评估。
如果只是为了本地开发调试,执行一次SET GLOBAL log_bin_trust_function_creators = 1也行。注意这个操作要全局权限,并且它会让MySQL信任所有函数的创建者。我一般只在测试库开,生产库还是走显式声明。
6.2 生成出来的ID变成负数
排查步骤分两步。第一步看时间戳差值是否溢出:SELECT FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - 1609459200000,如果结果超过2^41左右,说明服务器时间异常或者起点设置过于久远。第二步检查拼接结果:去掉低12位序列号,单独看(v_ms << 22) | (v_worker << 12)是不是负数。如果这一步就负了,说明41位时间戳段已经溢出,把起点调近或者同步系统时间即可。
我踩过的坑是把v_twepoch误写成一个很大的毫秒值,时间戳差值就变成了负数,左移后符号位直接被占满,返回的ID全是负数。后来又发现服务器时间被运维调快了五分钟,也导致类似现象。遇到负数ID,优先怀疑“时间”,这个方向大概率没错。
6.3 不同连接生成的ID出现重复
序列号依赖共享序列表,所以只要多个连接都从同一张表取序列,理论上是安全的。但有一种隐蔽情况会造成重复:有人把序列表的数据复制到了另一个环境,两个环境里的value值一样,生成出来的ID就会撞。所以分库分表后,每个分库必须使用不同的机器ID,并且序列表的初始值也要隔离。
另一个隐蔽场景是测试环境多套库共用了同一个序列表备份文件,机器ID又配的一样,一顿操作下来重复率惊人。排查思路很简单:反解ID里的worker_id,看看是哪个节点的产物;再单独查序列表的value是否在两套库中一致。
6.4 时间回拨导致ID回退或冲突
服务器时钟往前跳,也就是所谓的“回拨”,会让将来的ID可能小于已生成的ID,最直接的后果是唯一索引撞车。
纯SQL层面能做的处理相对有限,一个实用思路是:以序列表中保存的last_ms为参考,如果当前毫秒值小于等于上次的毫秒值,就强制把时间戳推进到last_ms + 1。相当于把时间基准从操作系统时钟切换为“数据库生成器内部时钟”。这样即使服务器时间回拨,ID也能保持时间戳单调,直到回拨幅度超过可容忍范围。
具体的实现要点是给序列表增加一列last_ms,在函数里先读后写。并发下必须保证读和写是一个原子操作,我的建议是直接把它和value放在同一条UPDATE里,例如:
UPDATE snowflake_seq SET value = LAST_INSERT_ID(value + 1), last_ms = GREATEST(last_ms, v_ms) WHERE id = 0;这条语句既更新了序列值,又用GREATEST把last_ms单调托底。函数内部再根据新旧last_ms判断是否需要回拨修正。这个补丁逻辑能扛住大部分偶发回拨场景,但如果是运维手动改时钟跳了几分钟,最好的方案还是停写、校准、再恢复,算法层面的修正只能兜小概率抖动。
6.5 批量INSERT多次调用函数,序列号出现断层
有同学反馈,一次INSERT多行数据生成了100条ID,序列号并不是0~99,而是中间跳了很多。这不是bug。原因有两个层面:
- 函数内嵌在SELECT或INSERT语句里,MySQL执行计划可能因类型推导或者子查询展开而调整函数的实际调用次数和顺序。
LAST_INSERT_ID(id + 1)风格的自增在同一个语句内多次调用时,MySQL并不保证和语句文本顺序完全一致。
解决思路:把“取一批序列号”和“生成ID”拆成两步。先一次申请N个序列号,再逐行拼接。用存储过程或者应用层循环都能规避这个不确定性。如果你只是在INSERT VALUES里写十个函数调用,实测下来绝大多数MySQL版本是顺序执行的,但一旦提升到几百行,就不建议依赖这个行为了。
6.6 性能瓶颈在哪里
很多人担心这个方案会拖垮数据库。实测下来,单条INSERT里嵌入一次函数调用,对OLTP业务影响几乎可以忽略。真正的瓶颈在于批量生成时,每生成一个ID都要执行一次UPDATE snowflake_seq,虽然只是更新一行,但行锁竞争和事务提交开销会被放大。
如果压测发现批量生成很慢,优先考虑两个优化方向。一是把序列表换成内存表或者NIP表,减少刷盘开销;二是应用层做“预取段”:一次性从数据库申请1000个序列号,缓存在应用内存里,用完再取下一批。这样数据库只需每1000次请求执行一次UPDATE,性能提升非常明显。配合存储过程批量申请序列段,这个方案就升级成了适合更高并发场景的版本。
下面给一个简单的批量申请序列号的存储过程框架,它一次把p_num个序列号的基础值取回来,后续由应用层自行拼装ID:
DROP PROCEDURE IF EXISTS seq_fetch; DELIMITER $$ CREATE PROCEDURE seq_fetch(IN p_num INT, OUT p_seq_start BIGINT) BEGIN UPDATE snowflake_seq SET value = LAST_INSERT_ID(value + p_num); SET p_seq_start = LAST_INSERT_ID() - p_num + 1; END$$ DELIMITER ;调用后拿到起始序列号,再按“(序列号+i)%4096”拼入ID即可。这种预取思路让数据库层的压力从“每ID一次更新”降为“每批一次更新”,实测在十万级数据导入时省掉了大量行锁竞争,是目前这套方案能支撑更高吞吐的关键补丁。
7. 快速参考:几张对比表和避坑清单
7.1 两种方案怎么选
| 维度 | 轻量表达式方案 | 存储函数+序列表方案 |
|---|---|---|
| 建表/建函数 | 不需要 | 需要序列表和存储函数 |
| 跨连接唯一性 | 不保证 | 通过序列表行锁保证 |
| 部署成本 | 极低 | 中等 |
| 性能 | 高 | 中等,受行锁影响 |
| 适用场景 | 单线程脚本、测试造数 | 生产环境的并发业务写入 |
| 扩展空间 | 几乎为零 | 可加时钟回拨保护和批量预取 |
如果只是临时跑脚本,用轻量表达式。如果是要给线上业务提供服务,直接上存储函数+序列表方案,别在临时方案上浪费时间。
7.2 避坑清单
- 位偏移量写错是最大的坑,务必用反解SQL验证位段。
- 机器ID必须全局唯一,分库环境殊为重要。
- 时间起点
twepoch要选近一些,否则提前溢出。 - 开启binlog环境创建函数,记得处理
log_bin_trust_function_creators。 - 批量生成时不要把函数调用写进超大VALUES里,拆成“预取序列号+应用拼接”。
最后说两句实在话
我自己在这套方案上踩过不少坑,最深的感受是:雪花ID的难点从来不是“64位怎么拼”,而是“序列号的来源在并发下怎么保证不重”。MySQL的LAST_INSERT_ID(id + 1)技巧是整套方案的点睛之笔,把这个机制吃透后,其他都是顺水推舟。
另外,也不要神化这套SQL实现。它最适合的场景是中小业务规模、不想引入额外发号组件的团队。一旦你的业务真正到了每秒数万甚至数十万写入,更好的选择仍然是应用层批量预取序列号,甚至用专门的发号服务。但把数据库当成一个“自带发号能力”的组件,遇到瓶颈时你至少知道往哪个方向改造,而不是一头雾水重写整套ID策略。