Node.js游戏服务器项目移植 5-唯一 ID 生成方案
前言在游戏后端业务开发中临时会话 ID、任务编号、房间 ID、临时资源编号等场景频繁需要区间内不重复数字 ID。一般常用雪花、UUID但部分业务需要「限定起止区间、纯数字、随机无序、支持 ID 回收复用」雪花、自增主键无法满足需求。本文分享三策略 ID 生成思路分别适配小量临时 ID、中量级内存随机 ID、海量落地持久 ID 三种业务场景无第三方依赖。源码设计思路按需取舍内存开销、随机均匀性、持久化能力分层设计避免过度设计。整体功能一览实现类实现原理存储介质最佳适用场景优缺点RandomIds区间随机数 重复递归重试对象记录已占用 ID进程内存小区间临时 ID千级以内如 1000~9999优点无需预生成、极简缺点ID 临近占满时递归爆炸、性能骤降ForcedRandomIds全量预生成 ID 数组→洗牌打乱→Map 缓存弹出消耗、支持回收放回进程内存几万几十万 ID进程生命周期内使用需要均匀随机优点无冲突、随机均匀、无重试损耗缺点全量驻留内存超大区间内存溢出IdsPoolID 落地磁盘文件 pos 偏移点位记录消费位置 固定大小内存缓冲分片加载磁盘文件 少量内存百万 / 亿级超大区间 ID、服务重启不丢失不重复、生产长期运行优点海量数据低内存占用、断电续跑缺点少量磁盘 IO性能略低于内存方案一、方案一RandomIds轻量随机 ID 生成器设计思路限定[from,to]数值区间每次随机取值通过字典记录已分配 ID命中已占用 ID 则递归重试删除字典键实现 ID 回收复用。核心源码精简function RandomIds(from, to) { this.from from; this.to to; this.ids {}; // 已占用ID注册表 } // 获取唯一随机ID RandomIds.prototype.get_id function () { const id UnitTools.random(this.from, this.to); // 冲突递归重试 if (!UnitTools.is_null_or_undefined(this.ids[id])) return this.get_id(); this.ids[id] 1; return id; }; // 回收ID可再次分配 RandomIds.prototype.remove_id function (id) { delete this.ids[id]; };使用示例const {RandomIds} require(./ids_generator); const rid new RandomIds(1000,9999); const id rid.get_id(); rid.remove_id(id); // 释放ID落地建议✅ 推荐临时房间号、短时有效任务 ID、小范围编号❌ 禁止区间可用量20% 场景大量重试造成栈溢出、CPU 飙升二、方案二ForcedRandomIds预洗牌内存 ID 池设计思路一次性生成区间全部 ID使用洗牌算法打乱顺序存入Map取 ID 从 Map 头部弹出、用完 ID 直接塞回 Map 实现复用从根源消除 ID 冲突与重试逻辑随机分布均匀可控。核心源码精简function ForcedRandomIds() { this.unUsedID new Map(); } // 生成全量ID并洗牌入池 ForcedRandomIds.prototype.generateIDs function (from, to) { const temp []; for(let ifrom;ito;i) temp.push(i); UnitTools.wash_array(temp); // 数组洗牌 temp.forEach(vthis.unUsedID.set(v,1)); return temp; }; // 取出一个ID池空返回null ForcedRandomIds.prototype.getID function () { const iter this.unUsedID.entries().next(); if(iter.done) return null; const id iter.value[0]; this.unUsedID.delete(id); return id; }; // ID回收复用 ForcedRandomIds.prototype.reUseID function (id) { this.unUsedID.set(id,1); };使用示例const fid new ForcedRandomIds(); fid.generateIDs(1,50000); // 生成1~50000洗牌ID池 const id fid.getID(); fid.reUseID(id); // 回收落地建议✅ 推荐服务运行周期固定编号、中等量级一次性预分配 ID❌ 禁止百万以上超大区间全量数组 Map 会堆内存溢出三、方案三IdsPool磁盘持久化 ID 池生产首选设计思路超大区间 ID 无法全量驻留内存将 ID 持久化到本地文本一行一个 ID配套.pos点位文件记录已消费行数设置固定大小内存缓冲区默认 10000缓冲区耗尽后分片从磁盘续读。首次初始化分批次生成 ID、分批洗牌、分批写入磁盘避免超大数组占内存重启恢复读取.pos偏移值跳过已消费行从断点继续取 ID保证永不重复取 ID优先从内存 buffer 拿数据buffer 空则分片加载磁盘数据每次消费 ID 自动落地更新 pos 点位。关键配置与核心逻辑const CHUNK_SIZE 10000; // 内存缓冲区上限 const BATCH 100000; // 磁盘批量生成分片大小 function IdsPool() { this.file_path null; this.offset_file null; // xxx.txt.pos 偏移记录文件 this.buffer []; // 内存缓冲 this._consumed 0; // 全局已消费行数 }1文件初始化 / 自动创建read_or_create_file判断 ID 文件是否存在已存在读取 pos 偏移→加载缓冲不存在分块生成洗牌 ID 写入文件初始化 pos0。2分块落地 ID_generate_file按每批 10w 拆分区间分批生成数组、洗牌、写入磁盘生成完即销毁数组杜绝大对象常驻内存。3分片加载缓冲_fill_buffer采用 Buffer 二进制分片读取文件单次 64KB按换行切割文本跳过_consumed已消费行填充至 buffer 至上限剩余数据留存换行碎片避免丢行。4消费 IDget_and_delete_one_id运行IdsPool.prototype.get_and_delete_one_id function () { if(this.buffer.length 0) this._fill_buffer(); if(this.buffer.length 0) return null; // ID全部耗尽 const id this.buffer.shift(); this._consumed 1; this._save_offset(); // 落地最新点位 return id; };使用示例运行const pool new IdsPool(); // 生成/加载 ./data/ids.txt 区间10000000~99999999持久ID池 pool.read_or_create_file(./data/ids.txt,10000000,99999999); const id pool.get_and_delete_one_id();落地建议✅ 推荐千万级超大 ID 段、生产长期服务、重启不能重复的业务编号优化方向可替换本地文件为 Redis List、RocksDB提升 IO 性能因为笔者做的是小量级的游戏房间服务千万级已经足够所以用本地文件的解决方案。四、三种方案选型决策表区间量级 1w、临时使用→ RandomIds极简零初始化成本区间 1w~50w、进程常驻、需要均匀随机→ ForcedRandomIds内存最优随机方案区间 50w、生产环境、服务会重启、海量 ID→ IdsPool磁盘持久化兜底补充如需分布式多实例共用 ID 池基于 IdsPool 改造pos 与 ID 文件托管到 Redis由 Redis 统一维护消费偏移。五、源码导出与项目接入// 模块导出 module.exports { RandomIds, ForcedRandomIds, IdsPool };项目中统一引入业务按需切换实现统一调用语义get_id/getID/get_and_delete_one_id后期可无缝替换 ID 生成策略。六、拓展优化方向RandomIds 优化超过最大重试次数后自动降级为扩容或切换 ForcedRandomIdsForcedRandomIds 优化超大区间拆分多文件懒加载实现伪磁盘 内存混合池IdsPool 优化新增 ID 回收文件回收 ID 单独写入备用池优先消耗回收 ID替换 fs 同步 API 为 promise 异步适配高并发 IO 场景pos 点位定时落盘 内存缓存减少高频小文件写入损耗。当然这种算法只能用于单服务器组用于多服务器组的时候上面的方法会有问题但这次项目已经够用。另外业界常用的还有雪花算法这是后端最常用、最经典、分布式环境下必用的唯一 ID 生成算法可以用于多服务器组全称Twitter Snowflake 分布式 ID 生成算法雪花算法能在分布式多台服务器上生成全局唯一、趋势递增、纯数字的 ID它生成的 ID 长这样19 位纯数字1419223456789012345你之前的 ID 生成器只能单机用重启可能重复多台服务器会冲突雪花算法解决分布式多机器不重复趋势递增数据库索引友好纯数字、长度固定不依赖数据库 / Redis每秒能生成几十万百万 ID三、雪花 ID 的结构一个标准雪花 ID 是64bit二进制分成 5 部分[1位符号][41位时间戳][5位机器ID][5位数据中心ID][12位序列号] 固定0 毫秒级时间 机器标识 机房标识 自增序号1 位符号位固定 0保证 ID 是正数。41 位时间戳从某个固定时间开始算毫秒数能用 69 年。10 位机器标识哪台机器、哪个机房生成的保证分布式不重复。12 位序列号同一毫秒内自增同一毫秒能生成 4096 个 ID。四、雪花算法的特点✅ 优点全局唯一多台机器绝不重复趋势递增数据库索引性能极好纯数字存储、排序、索引都舒服高性能单机每秒生成几十万 ID不依赖第三方不依赖数据库 / Redis❌ 缺点依赖系统时间服务器时间回拨会导致重复 ID只能用 69 年但足够用到一般性的业务退休不是绝对连续只是趋势递增五、雪花算法 VS 之前的 ID 生成器场景你的 ID 生成器雪花算法单机小量 ID✅ 很好用没必要分布式多服务器❌ 会重复✅ 完美海量高并发❌ 顶不住✅ 支持重启不丢失✅ IdsPool 支持✅ 天然支持生产环境大规模一般标准方案六、什么时候用雪花算法只要满足下面任意一条必须用雪花算法分布式系统多台服务器订单 ID、用户 ID、流水号高并发需要长期存储、索引性能好全球唯一不重复七、最简单 Node.js 雪花算法代码可直接复制class Snowflake { constructor(workerId 0, datacenterId 0) { this.twepoch 1577836800000n; // 2020-01-01 时间起点 this.workerId BigInt(workerId); // 机器ID 0~31 this.datacenterId BigInt(datacenterId); // 机房ID 0~31 this.sequence 0n; // 序列号 this.maxSequence 4095n; this.lastTimestamp -1n; } // 生成ID nextId() { let timestamp BigInt(Date.now()); // 时间回拨处理 if (timestamp this.lastTimestamp) { throw new Error(时间回拨无法生成ID); } if (timestamp this.lastTimestamp) { this.sequence (this.sequence 1n) this.maxSequence; if (this.sequence 0n) { while (Date.now() Number(this.lastTimestamp)) {} timestamp BigInt(Date.now()); } } else { this.sequence 0n; } this.lastTimestamp timestamp; return ((timestamp - this.twepoch) 22n) | (this.datacenterId 17n) | (this.workerId 12n) | this.sequence; } } // 使用 const snow new Snowflake(1, 1); console.log(snow.nextId().toString());