前言

在游戏后端业务开发中,临时会话 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),缓冲区耗尽后分片从磁盘续读。

  1. 首次初始化:分批次生成 ID、分批洗牌、分批写入磁盘,避免超大数组占内存;
  2. 重启恢复:读取.pos偏移值,跳过已消费行,从断点继续取 ID,保证永不重复;
  3. 取 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 性能

因为笔者做的是小量级的游戏房间服务,千万级已经足够,所以用本地文件的解决方案。

四、三种方案选型决策表

  1. 区间量级 <1w、临时使用 → RandomIds(极简零初始化成本)
  2. 区间 1w~50w、进程常驻、需要均匀随机 → ForcedRandomIds(内存最优随机方案)
  3. 区间 > 50w、生产环境、服务会重启、海量 ID → IdsPool(磁盘持久化兜底)

补充:如需分布式多实例共用 ID 池,基于 IdsPool 改造:pos 与 ID 文件托管到 Redis,由 Redis 统一维护消费偏移。

五、源码导出与项目接入

// 模块导出
module.exports = {
    RandomIds,
    ForcedRandomIds,
    IdsPool
};

项目中统一引入,业务按需切换实现,统一调用语义get_id/getID/get_and_delete_one_id,后期可无缝替换 ID 生成策略。

六、拓展优化方向

  1. RandomIds 优化:超过最大重试次数后自动降级为扩容或切换 ForcedRandomIds;
  2. ForcedRandomIds 优化:超大区间拆分多文件懒加载,实现伪磁盘 + 内存混合池;
  3. IdsPool 优化:
    • 新增 ID 回收文件,回收 ID 单独写入备用池,优先消耗回收 ID;
    • 替换 fs 同步 API 为 promise 异步,适配高并发 IO 场景;
    • pos 点位定时落盘 + 内存缓存,减少高频小文件写入损耗。

当然这种算法只能用于单服务器组,用于多服务器组的时候上面的方法会有问题,但这次项目已经够用。

另外业界常用的还有雪花算法

这是后端最常用、最经典、分布式环境下必用的唯一 ID 生成算法,可以用于多服务器组

全称:Twitter Snowflake 分布式 ID 生成算法

 雪花算法能在分布式多台服务器上,生成全局唯一、趋势递增、纯数字的 ID

它生成的 ID 长这样(19 位纯数字):

1419223456789012345

你之前的 ID 生成器:

  • 只能单机用
  • 重启可能重复
  • 多台服务器会冲突

雪花算法解决:

  1. 分布式多机器不重复
  2. 趋势递增(数据库索引友好)
  3. 纯数字、长度固定
  4. 不依赖数据库 / Redis
  5. 每秒能生成几十万~百万 ID

三、雪花 ID 的结构

一个标准雪花 ID 是 64bit(二进制),分成 5 部分:

[1位符号][41位时间戳][5位机器ID][5位数据中心ID][12位序列号]
  固定0    毫秒级时间    机器标识    机房标识    自增序号
  1. 1 位:符号位 固定 0,保证 ID 是正数。

  2. 41 位:时间戳 从某个固定时间开始算毫秒数,能用 69 年

  3. 10 位:机器标识 哪台机器、哪个机房生成的,保证分布式不重复。

  4. 12 位:序列号 同一毫秒内自增,同一毫秒能生成 4096 个 ID


四、雪花算法的特点

✅ 优点

  1. 全局唯一:多台机器绝不重复
  2. 趋势递增:数据库索引性能极好
  3. 纯数字:存储、排序、索引都舒服
  4. 高性能:单机每秒生成几十万 ID
  5. 不依赖第三方(不依赖数据库 / Redis)

❌ 缺点

  1. 依赖系统时间 服务器时间回拨会导致重复 ID
  2. 只能用 69 年 (但足够用到一般性的业务退休)
  3. 不是绝对连续 只是趋势递增

五、雪花算法 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());
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐