Node.js游戏服务器项目移植 5-唯一 ID 生成方案
本文针对游戏后端业务中需要区间内不重复数字ID的场景,提出了三种ID生成方案:1)轻量级随机ID生成器RandomIds,适用于小量临时ID;2)预洗牌内存ID池ForcedRandomIds,适合中等量级内存随机ID;3)磁盘持久化ID池IdsPool,支持海量ID且服务重启不丢失。文章详细分析了各方案的实现原理、适用场景及优缺点,并提供了源码示例和选型建议。其中RandomIds简单但性能受限
前言
在游戏后端业务开发中,临时会话 ID、任务编号、房间 ID、临时资源编号等场景频繁需要区间内不重复数字 ID。一般常用雪花、UUID,但部分业务需要「限定起止区间、纯数字、随机无序、支持 ID 回收复用」,雪花、自增主键无法满足需求。
本文分享三策略 ID 生成思路,分别适配小量临时 ID、中量级内存随机 ID、海量落地持久 ID 三种业务场景,无第三方依赖。
源码设计思路:按需取舍内存开销、随机均匀性、持久化能力,分层设计避免过度设计。
整体功能一览
| 实现类 | 实现原理 | 存储介质 | 最佳适用场景 | 优缺点 |
|---|---|---|---|---|
| RandomIds | 区间随机数 + 重复递归重试,对象记录已占用 ID | 进程内存 | 小区间临时 ID(千级以内,如 1000~9999) | 优点:无需预生成、极简;缺点:ID 临近占满时递归爆炸、性能骤降 |
| ForcedRandomIds | 全量预生成 ID 数组→洗牌打乱→Map 缓存,弹出消耗、支持回收放回 | 进程内存 | 几万~几十万 ID,进程生命周期内使用,需要均匀随机 | 优点:无冲突、随机均匀、无重试损耗;缺点:全量驻留内存,超大区间内存溢出 |
| IdsPool | ID 落地磁盘文件 + 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 i=from;i<=to;i++) temp.push(i);
UnitTools.wash_array(temp); // 数组洗牌
temp.forEach(v=>this.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 写入文件,初始化 pos=0。
2)分块落地 ID _generate_file
按每批 10w 拆分区间,分批生成数组、洗牌、写入磁盘,生成完即销毁数组,杜绝大对象常驻内存。
3)分片加载缓冲 _fill_buffer
采用 Buffer 二进制分片读取文件(单次 64KB),按换行切割文本,跳过_consumed已消费行,填充至 buffer 至上限,剩余数据留存换行碎片避免丢行。
4)消费 ID get_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 优化:超过最大重试次数后自动降级为扩容或切换 ForcedRandomIds;
- ForcedRandomIds 优化:超大区间拆分多文件懒加载,实现伪磁盘 + 内存混合池;
- 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());
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)