基于crypto.getRandomValues()的8位Base36 ID生成器重复问题:概率评估与函数缺陷排查
基于crypto.getRandomValues()的8位Base36 ID生成器重复问题:概率评估与函数缺陷排查
嘿,这个问题我做项目时也纠结过,来帮你一步步拆解清楚:
一、先聊重复概率:生日悖论是关键
你算的2.8万亿总可能性没错,但实际重复概率不能靠“1/总可能性”直觉判断,生日悖论才是这里的核心——当你不断生成ID时,重复概率的增长速度会比你想的快得多。
用近似公式算的话:当生成n个ID时,重复概率≈1 - e^(-n²/(2*N)),其中N是总可能数(2.8万亿)。给你几个直观的数值参考:
- 生成10万个ID:重复概率≈0.18%(千分之1.8,很低)
- 生成100万个ID:重复概率≈16%(每6次大规模生成就可能出一次重复)
- 生成500万个ID:重复概率≈75%(大概率会出现重复)
所以如果最近几天你的系统生成了几十万甚至上百万个ID,出现重复的概率就不是天方夜谭了;但如果只是几千几万次,那概率几乎可以忽略,得从其他地方找原因。
二、你的生成函数有没有缺陷?
仔细看了你的代码,确实有个小问题,但不是导致重复的主要元凶:
1. 模运算导致的分布不均匀
crypto.getRandomValues返回的是0-255的随机数,而255 % 36 = 3,这意味着:
- 0-251的数(共252个):每个余数0-35会刚好出现7次(252=36*7)
- 252-255的数(共4个):对应的余数是0、1、2、3,这会让字符
0、1、2、3的出现概率比其他字符高一点(8/256 vs 7/256)
这个偏差很小,对重复概率的影响微乎其微,但确实破坏了“完全均匀随机”的假设。
2. 函数的其他潜在问题?
除了分布不均,你的函数逻辑本身没大问题:
- 用了
crypto.getRandomValues而不是Math.random,这是对的——前者是密码学安全的随机源,不会像后者那样有可预测性问题 - 循环8次生成8位字符,逻辑正确
所以这个函数本身不会“轻易”生成重复,除非生成量真的很大。
三、那重复错误可能来自哪里?
如果生成量不大却出现重复,大概率不是函数的锅,得排查这些点:
- 数据库并发竞态:两个请求同时生成了相同的ID,又同时插入数据库,这时候数据库的唯一约束可能没来得及生效(比如隔离级别问题),导致其中一个报错
- ID的重复使用:比如删除的记录ID被误重新插入,或者缓存里的旧ID被返回给了新请求
- 其他生成源:有没有其他地方也在生成同格式的8位Base36 ID?比如旧系统、第三方服务,不小心把重复的ID写入了同一个表
四、怎么解决?
给你几个实际可用的方案:
- 修正分布不均的问题:把模运算改成“拒绝采样”——如果随机数>=252(36*7),就重新取数,这样每个字符的概率完全一致:
export function generateId(): string { const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; // 一次取对应长度的随机数,过滤不符合条件的,不够再补 while (result.length < 8) { const randomBytes = new Uint8Array(8 - result.length); crypto.getRandomValues(randomBytes); for (const val of randomBytes) { if (val < 252) { // 只取能被36整除范围内的数,保证分布均匀 result += chars[val % 36]; } } } return result; } - 数据库层面加保险:给ID字段加唯一约束,并且在插入时处理冲突——比如捕获
INVALID_RECORD错误后,自动重试生成新ID再插入 - 统计生成量:查一下最近几天系统总共生成了多少个ID,用刚才的生日悖论公式算一遍,看看概率是否匹配,能帮你快速定位问题根源
- 增加ID长度:如果业务允许的话,把ID从8位改成10位,总可能性会跳到36^10≈3.6e15,重复概率会降到可以忽略的程度,一劳永逸




