Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
1809 字
9 分钟
一种随机且唯一的uid生成算法
2025-11-16
统计加载中...

今天在刷小黑盒时,无意间看到篇关于推测小黑盒 UID 生成算法的文章——
这让我想起,一年半前做 Minecraft 地图那会儿,我也曾对着 UID 发过呆:

“能不能搞个非自增式的 UID 系统?……虽然但……好像和自增也没啥本质区别,还更麻烦——
但!它好看啊 (✧∀✧)”

当时能力不够,硬是想破头也没整出个像样的。倒是误打误撞,搞了个「基于 UID + 时间戳」的伪随机运气值算法:
每日固定、无需存储,一人一号一天一运——灵感来自一个 QQ 机器人(记得是“QQ 号 × 时间戳 % 101”大概)。
效果嘛,真·算命:你今天的欧气,是数学算出来的,不是抽出来的…


如今回看,当年卡住我的,其实是同一个问题的不同侧面:

  • 运气值:要的是「确定性伪随机」——输入(uid+时间)→ 输出(固定运气)
  • UID 生成:要的是「无状态唯一性」——输入(上一个 uid)→ 输出(下一个 uid),且不重、不小、不溢出

而这次,我想把它真正走通。


限制 & 初始思路#

mc 原版的计分板,是我给自己设的限制:
运算只有 + - × ÷ % 交换 比较,没有数组、没有数据库、不能遍历、不能存历史——
换言之:你只能用上一个 UID,和几个常数,算出下一个

那最朴素的骨架自然浮现:

next_uid = (current × A) % M

其中:

  • M 很好定:int32 正整数上限 = 2311=21474836472^{31} - 1 = 2147483647(一个质数,记作 pp
  • 但是 A 不能随便选,选错了序列可能:
    • 很快撞车(周期短)
    • 卡在 0 不动(死循环)
    • 或者……前几百个看着“随机”,一千个后就开始跳 1,2,3……(周期性崩坏)

于是问题转为:

什么样的 A,能让这个乘法模运算是“走最长的路还不回头”的?


从“试试看”到“为什么”#

我一开始也想:要不……写个脚本枚举 A=2,3,4… 看哪个周期长?
懒是一回事,更怕的是:就算试出一个“周期够 5 万”,我也不知道它为什么好,哪天崩了也说不出原因。

得往数学深处瞄一眼 🧐

模数 p=2311p = 2^{31} - 1 是一个梅森质数
这很关键——在模质数 pp 下,集合 {1,2,...,p1}\{1, 2, ..., p-1\} 对乘法构成一个循环群
而其中,存在一些特殊的数,叫“原根”,它的一个幂次,就能跑遍整个群:

a1,a2,a3,,ap1modpa^1, a^2, a^3, \dotsc, a^{p-1} \mod p

这串数,刚好是 1 到 p1p-1 的一个排列,周期 = p1=2147483646p-1 = 2147483646

换句话说:
如果 App 的原根,且初始 UID ≠ 0,
那么 next_uid = (current × A) % p 生成的序列,会在 21 亿次内绝不重复

而实际的玩家数?满打满算不超过 5 万。相当于你有一条绕地球 50 圈的跑道,却只打算跑 1 公里,稳得一塌糊涂~


找那个“对的人”#

现在问题缩成:p=2147483647p = 2147483647 下,哪个小整数是原根?

理论上,原根存在,且数量不少(约 ϕ(p1)5×108\phi(p-1) \approx 5 \times 10^8 个),但:

  • 太大的数(比如 1e9 级)乘法开销高
  • 太小的数(2, 3, 5)常因阶太小被排除
  • 得找一个 又小、又快、又有名有姓

答案早已写在历史里:16807

它等于 757^5,是 1988 年 Park & Miller 提出的「最小标准随机数生成器」的核心乘子 ——
一个被写进教科书、用在早期 rand() 实现中的经典选择。

验证它?只需检查对 p1=2×33×7×11×31×151×331p-1 = 2×3^3×7×11×31×151×331 的每个质因子 qq,都满足:

16807(p1)/q≢1(modp)16807^{(p-1)/q} \not\equiv 1 \pmod{p}

这个验证在 1988 年 Park & Miller 的论文中已被严格完成;现代工具(如 Python 的 sympy)甚至毫秒内就能复现。
所以说实话,我们不必每次都亲手推一遍轮子,就像没人会在调用 rand() 前先证明一遍 LCG 的周期性:有些信任,是站在巨人肩上的效率。


好看在哪?#

seed = 1 开始:

stepUID备注
01初始种子
1168071×168071 × 16807
2282475249168072modp16807^2 \bmod p
31622650073
41996312012
51033006068
2147483645最后一个新值下一次会回到1开始下一周期

没有“10001, 10002”那样的工整,也没有“1, 3, 9, 27…”那样的幂次感——
它像一条看似曲折、实则永不回头的单行道


⚠ 声明#

先说清楚:此方案安全性几乎为零,不可用于任何真实系统,现成的成熟安全方案一抓一大把。
它存在的唯一理由,是回应我当初的执念:在基岩版原版的限制下,生成一个“看起来不单调、实际上不重复”的 UID。

换句话说:

这不是一个“好”的通用 UID 方案,它只是我试了所有路之后,在原版 MC 的规则夹缝里,唯一能跑通的那条小径。


在 Minecraft 基岩版中实现#

问题来了:直接套用原公式,第 4 次就会溢出成负数(实测得到 -1199696159)。
理论上可以用拆位累加法防溢出,但太麻烦了。

于是思路一转:既然用户远不到 21 亿,何必硬扛 21 亿的模数?
→ 改用更轻量的组合,上限100w:

  • M=1000003M = 1000003(大于 10⁶ 的最小质数)
  • A=2A = 2(是模 1000003 的原根,周期 = 1000002,刚好覆盖需求)

推荐把初始种子(即第一个 uid 的值)设为 48573 ——
这是 2²⁰ % 1000003 的结果,能让序列从“折返点”开始,避开前面那串像 2 的幂次的单调增长,观感更好

注意

这只是核心生成逻辑;计分板创建、虚拟玩家(如 "2"m)的初始化等基础步骤需自行补充。

实际只需两行命令

scoreboard players operation uid uid *= "2" num
# 将 uid 乘以 2(即 current × A)
# 前提:num 计分板中,虚拟玩家 "2" 的分数为 2
scoreboard players operation uid uid %= m num
# 对结果取余 1000003(即 (current × A) % M)
# 前提:num 计分板中,虚拟玩家 m 的分数为 1000003
示例
示 例

写在最后:为什么还值得折腾?#

你说得对——它和「自增+映射」本质差不多。
但是,当玩家看到自己 UID 是 1033006068 而不是 5
他第一反应不是「哦,第 5 个注册」,而是:

“这数字……怎么来的?”
“用的random?怎么避免重复的?”

那一瞬的好奇,就能让我轻哼起来~

算法可以朴素,但思考不该潦草。
在限制中跳舞,在确定中造“随机”——
这大概,是我对“好看”的执念吧


参考资料 & 延伸推荐#


一种随机且唯一的uid生成算法
https://blog.lonzov.top/posts/modwalk/
作者
浪小舟
发布于
2025-11-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
加载中…
正在加载中……
封面
加载中…
正在加载中……
0:00 / 0:00