2026-07-04 · Calc-Tools 编辑

随机数生成器使用指南:在线生成随机数的多种应用

随机数用起来简单,用对却不简单。同一句"在 1 到 100 之间取一个数",在不同的随机源和不同的实现方式下,公平程度可以相差一倍以上;同一个"打乱顺序"的函数,可能让某些排列出现的概率比别的排列高出 25%。出问题的通常不是"生成器坏了",而是随机源的选择、取模的方式,以及抽样算法本身。

本文不重复工具的按钮说明,只讲工程上真正会踩的四个坑:伪随机与密码学安全随机的分界线在哪里,种子为什么同时是"可复现"和"致命弱点",取模为什么会破坏均匀性、又该怎么修,以及随机结果要如何检验。文中所有概率与分布数字都由脚本精确枚举或模拟得出,可以照着重算。

一、伪随机和真随机之间,还隔着一道工程分界线

把随机数简单分成"真随机"和"伪随机"两类,在工程上是不够用的。真正的分界线在伪随机内部:一边是普通伪随机(PRNG),另一边是密码学安全伪随机(CSPRNG)。两者都能通过基本的均匀性检验,差别不在"看起来随不随机",而在"能不能被预测"。

PRNG 的设计目标是速度和统计均匀性,它的内部状态一旦被观测或反推,后续输出就完全确定。以 JavaScript 的 Math.random() 为例,主流引擎采用的是快速的非密码学算法,规范只要求它返回 [0, 1) 区间上的一个数,并不承诺不可预测。因此用 Math.random() 生成密码重置令牌、抽奖中奖名单或优惠券码,等于把系统的安全边界交给了一个可以被推测的数列。

CSPRNG 的设计目标则是"即使知道前面所有输出,也无法推算出下一个输出"。浏览器里对应 crypto.getRandomValues(),服务端对应操作系统的随机设备或密码学库接口。它更慢,但一次调用通常只要几微秒,用在令牌、密钥、验证码、会话标识上完全够用。

判断标准很简单:结果的不可预测性会不会影响钱、账号或公平性。会,就必须用 CSPRNG;不会——比如模拟掷骰子、生成测试数据、课堂随机点名——用普通 PRNG 完全合理,没必要为每次调用支付密码学开销。

二、种子:可复现与不可预测,是一对无法同时满足的需求

伪随机序列由"种子"决定:种子相同,序列就完全相同。这个性质在两边都极为重要,但方向正好相反。

在测试和仿真里,固定种子是刚需。一个只在某批随机数据下才失败的缺陷,如果种子不固定,就永远无法复现。把种子写进用例、日志或配置,任何一次运行都能原样重放,这也是"随机化测试"能被当作回归测试使用的前提。

在安全场景里,可复现就是灾难。如果种子取自当前时间戳,攻击者只要知道你大概在什么时候启动了服务,就能把这个范围压缩到很小的窗口里逐个尝试;种子空间从天文数字瞬间塌缩成几分钟。多起"令牌可被预测"的问题,根源都不是算法被破解,而是种子太有规律。安全场景的种子必须来自不可预测源,不能是时间戳、进程号、自增计数器这类可枚举的值。

实务上的做法是把两套分开:测试与实验用固定种子的普通 PRNG,保证结果可复算;线上涉及账号、资金、公平性的随机一律走 CSPRNG,种子由系统随机设备提供,业务代码不插手。

三、模偏差:为什么 rand() % n 会不均匀

这是最容易被忽略、也最好修的一个坑。假设随机源均匀地产生 0 到 255 之间的整数(一个字节),你要取 0 到 99 的随机数,于是写成"取到值后除以 100 取余数"。问题立刻出现:256 不是 100 的整数倍。

把 256 个取值按余数分组:余数 0 到 55 各有 3 个来源(例如 0、100、200 都得到 0),余数 56 到 99 只有 2 个来源。于是前 56 个数的概率是 3/256,后 44 个是 2/256——最高与最低相差 1.5 倍。这就是"模偏差"(modulo bias)。

偏差的大小并不固定,它取决于随机源的范围与取值个数之间的整除关系。下表是用脚本精确枚举出来的实际分布:

随机源范围取模目标 n最高概率最低概率比值(最高 ÷ 最低)
0 – 9(10 个取值)620.0000000%10.0000000%2.0000000000
0 – 255(8 位)1001.1718750%0.7812500%1.5000000000
0 – 255(8 位)616.7968750%16.4062500%1.0238095238
0 – 32767(15 位)10000.1007080078%0.0976562500%1.0312500000
0 – 32767(15 位)1001.0009765625%0.9979248047%1.0030581040
0 – 4294967295(32 位)1001.0000000009%0.9999999776%1.0000000233

这张表带来两个反直觉的结论。第一,偏差不是"有或没有",而是"多大":32 位随机源取模 100 时,最高与最低概率之差只有 0.000000023 个百分点,实际使用中完全可以忽略;而 10 个取值的随机源取模 6,最高概率是最低概率的 2 倍。第二,决定偏差的是底层范围的大小,而不是代码里有没有"取模"这个动作——同一个 % 100,接在 8 位随机源后面和接在 32 位随机源后面,后果完全不同。

正确的修法有三种,按代价从低到高排列:

顺带一个实用判断:当取值个数能整除随机源范围时(例如从 0 到 999 里取 10 的整数倍分组),取模是天然无偏的。"在 1 到 100 之间取一个数"之所以基本没问题,正是因为现代随机源的范围远大于 100。

四、四个可以复算的真实算例

算例一:偏差在小样本里根本看不出来。用同一种有偏的取模方式(8 位随机源取模 100)做模拟,只改变样本量:

样本量出现最多的数出现最少的数最高 ÷ 最低
1,000 次20 次(2.0000%)0 次(0.0000%)无法比较
10,000 次148 次(1.4800%)56 次(0.5600%)2.643
1,000,000 次11,946 次(1.1946%)7,672 次(0.7672%)1.557
理论值1.1719%0.7813%1.500

1,000 次时甚至出现了"某个数字一次都没抽到",看起来像严重的实现错误,其实那只是噪声盖住了偏差。样本量升到 100 万次,观测比值才收敛到理论值 1.5 附近。结论:小样本看起来均匀,不代表算法无偏;靠"我抽了几次都挺均匀"来判断随机性,方向就错了。

算例二:错误洗牌让某些顺序多出 25%。把 3 个元素打乱,如果每一轮都从"全体下标"里随机挑一个来交换(而不是从当前位置往后挑),共有 3×3×3 = 27 条等概率路径,但它们落到 6 种排列上的次数并不相等:

排列出现次数(共 27 条路径)概率均匀时应为
0-1-2414.8148%16.6667%
2-0-1414.8148%16.6667%
2-1-0414.8148%16.6667%
0-2-1518.5185%16.6667%
1-0-2518.5185%16.6667%
1-2-0518.5185%16.6667%

最高与最低之比为 1.25。元素越多,这种偏差越难靠肉眼发现:3 个元素还能穷举,30 个元素的排列有 30 的阶乘那么多,偏差被彻底淹没在"看起来挺乱"里。正确做法是 Fisher-Yates 洗牌:第 i 轮只和 i 及之后的元素交换。3 个元素时它正好产生 3×2 = 6 条路径,一一对应 6 种排列,各占 1/6。需要"随机抽取 k 个不重复"时,只洗前 k 轮即可,复杂度是 O(k),不必把整个列表排好序再截取。

算例三:随机数重复是数学必然,不是故障。从 100 个号码中有放回地抽,抽 10 次至少重复一次的概率是 37.18%,抽 12 次就升到 49.68%——接近一半。经典的生日问题同理:23 个人里有两人生日相同的概率是 50.73%。所以"抽奖抽到重复号码"几乎不需要怀疑工具出错,它本来就是大概率事件;要保证不重复,必须靠算法保证(开启不重复模式,或先洗牌再依次取数),而不是靠"抽的次数少"。

算例四:用卡方检验判断"是否均匀"。掷 6 面骰子 6000 次,理论上每面各 1000 次。假设实际观测到 1013、980、1024、961、998、1024 次,卡方统计量为 3.2460;自由度是 5,显著性水平 0.05 对应的临界值是 11.070,统计量小于临界值,不能拒绝"均匀"这一假设。换一组明显有偏的数据——1200、1150、900、850、950、950——卡方统计量达到 100.0000,远超临界值,偏差被判定为真实存在。

五、不同场景需要多强的随机性

把前面的结论落到具体用途上,"该用哪一档随机数"其实有清晰的分层:

场景所需随机强度推荐做法常见错误
密码、密钥、会话令牌、验证码密码学安全,必须不可预测使用 CSPRNG 接口,长度优先于复杂度用普通 PRNG 或时间戳种子生成
抽奖、摇号、随机分组统计均匀 + 过程可核查事先固定范围与数量,留存种子与结果用取模实现且底层范围偏小;事后才定规则
不重复抽样(选出 k 个)统计均匀Fisher-Yates 只洗前 k 轮用随机比较器排序,或每轮从全体下标抽取
测试数据、仿真、随机化用例可复现优先固定种子并把种子写进日志不记录种子,失败用例无法复现
A/B 分组稳定可复现 + 各组均衡用用户标识做哈希后分桶,或按序交错分配每次访问重新随机,同一用户反复换组
结果公示与审计留痕可验证提前公布规则,记录种子与输出只公布结果,不公布过程

表里最值得注意的是 A/B 分组一行:它需要的恰恰是"可复现"而不是"不可预测"。如果每次请求都重新随机分组,同一个用户会在两组之间来回跳,实验数据直接作废。这类场景应该用用户标识的哈希值分桶,保证同一个用户永远落在同一组。

六、随机性可以被检验,但"通过检验"不等于真随机

想确认一个随机源是否可用,最直接的办法是画直方图:把结果按取值分桶统计频次,均匀的随机源各桶高度应当接近。肉眼只能看出明显问题,定量判断要靠卡方检验——自由度等于"桶数减一",把统计量与临界值比较,小于临界值说明偏差还在随机波动范围内。

但必须记住检验的边界:检验只能证伪,不能证明。一个确定性序列完全可以通过均匀性检验,比如把 0 到 99 循环输出一百万次,每个数正好出现一万次——直方图完美平坦,卡方统计量为 0,可它毫无随机性可言。反过来,真正的随机源在小样本下也经常"看起来不均匀",就像算例一里那 1000 次中出现的空档。

所以工程上的判断顺序应该是:先看用途需要哪一档强度,再检查实现有没有用对接口,最后才用统计检验做抽样体检。反过来做——先用直方图判断"随机不随机"——只会得出误导性的结论。

七、常见问题(FAQ)

为什么 Math.random() 不能用来生成验证码或令牌?
它属于非密码学安全的伪随机数生成器,规范只保证取值范围,不承诺不可预测;内部状态一旦被反推,后续输出就完全确定。生成令牌、密钥、验证码应改用 crypto.getRandomValues() 这类密码学安全接口。
在 1 到 100 之间抽奖,取模到底会不会产生偏差?
取决于底层随机源的范围。如果随机源是 32 位或更高精度的均匀整数,取模 100 时最高与最低概率的比值是 1.0000000233,实际使用中可以忽略;真正危险的是底层范围很小的情况,例如 10 个取值取模 6,最高概率是最低概率的 2 倍。
随机数为什么会重复?是不是工具坏了?
重复是概率的必然结果。从 100 个号码中有放回抽 12 次,至少重复一次的概率约 49.68%,接近一半。想要不重复必须靠算法保证,例如开启不重复模式,或先洗牌再依次取数。
固定种子安全吗?
看用途。测试、仿真、随机化用例需要固定种子来复现结果,这是优点;安全场景里种子一旦可枚举就是致命弱点,用时间戳做种子等于把搜索空间压缩到几分钟,必须改用不可预测的随机源。
怎么判断一串随机数是否均匀?
先画直方图看分布形态,再用卡方检验做定量判断:6 面骰子投 6000 次时自由度为 5,临界值约 11.070,统计量低于它就不能判定异常。注意通过检验只说明"没有被证伪",一个循环输出一百万次的确定性序列同样可以完美通过。

八、三个最常见的误区

误区一:随机数"看起来乱"就等于随机性好。人眼极不擅长识别随机性:真正的随机序列里经常出现连续重复,而刻意"打乱"过的序列反而显得更均匀。判断随机性要靠统计检验和代码审查,不能靠观感。

误区二:用 Math.random() 加一点变形就能当安全随机。把结果乘以一个大数、取几位小数、再拼上时间戳,都不会增加任何不可预测性,这些操作完全可被逆向。安全强度取决于随机源的熵,不取决于外围的加工手法。

误区三:抽奖只要"不重复"就公平了。不重复只解决了"同一个人中两次",没有解决"每个号码被抽中的机会是否相等"。如果底层取模有偏,或者先按某种顺序排列再截取,看似不重复的名单同样可以有系统性倾斜。

九、四个实用建议

如果只是需要一个固定范围内的随机整数列表,可以直接用随机数生成器,并注意勾选"不重复"以规避算例三里的重复问题;生成密码、令牌这类高安全需求的字符串,则应该换成密码生成器,让随机性强度与用途匹配。想进一步理解"不可预测"为什么重要,可以接着看密码生成器使用指南与UUID 生成器指南;随机标识在系统间传递时常用的编码方式,可参考 UUID 生成器 与Base64 编解码指南。

在线使用 随机数生成器

无需下载,打开即用

立即使用 随机数生成器

工具