欢迎光临
我们一直在努力

Node.js游戏服务器项目移植 5-唯一 ID 生成方案

前言

在游戏后端业务开发中,临时会话 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());

    赞(0)
    未经允许不得转载:171主机测评 » Node.js游戏服务器项目移植 5-唯一 ID 生成方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址