计算机是完全确定性的机器,严格来说没有能力产生"真正的随机"——但浏览器里生成的抽奖号码、测试数据看起来确实毫无规律。理解这背后的伪随机算法和真随机来源的区别,能帮你判断什么场景该用什么工具。
大多数编程语言内置的随机函数(比如 JavaScript 的 Math.random())都是伪随机数生成器(PRNG):给定一个初始"种子",通过固定的数学算法一步步推算出一串看起来毫无规律的数字序列。关键在于这个过程完全是确定性的——同样的种子,永远会生成完全相同的一串"随机"数。这类生成器速度快、实现简单,适合游戏、模拟、图形特效这类不需要抵抗恶意预测的场景,但不适合用在密码、密钥、抽奖这类需要抵抗被人预测或复现的安全敏感场景,因为如果攻击者能猜到或获取种子,就能完全预测出后续生成的所有"随机"结果。
现代浏览器和操作系统提供了另一套随机数接口——crypto.getRandomValues(),它属于密码学安全的伪随机数生成器(CSPRNG)。它的随机性来源不再是一个固定的初始种子,而是持续从操作系统收集的真实物理噪声(鼠标移动的时间间隔、磁盘读写的微小时序抖动、网络数据包到达的随机延迟等)里提取熵,作为随机源驱动生成算法——即使掌握了之前生成过的所有输出,也无法预测下一个输出会是什么。这也是为什么生成密码、密钥、抽奖号码这类场景,都应该使用 crypto.getRandomValues() 而不是 Math.random()。
如果想用一个 0-255 的随机字节,生成 0-9 之间的随机数字,最直接的想法是对字节值取 10 的余数(byte % 10)。但这个做法藏着一个不易察觉的偏差:256 除以 10 等于 25 余 6,也就是说 0-255 这 256 个可能的字节值里,余数 0-5 各对应 26 个字节值,余数 6-9 各对应 25 个字节值——余数 0-5 出现的概率会比 6-9 略高一点(这个偏差已经用代码验证:256 个字节按 10 取模统计,0-5 各出现 26 次,6-9 各出现 25 次)。
严谨的随机数生成需要用拒绝采样来消除这个偏差:只用能被 10 整除的字节值范围(比如 0-249,刚好是 25×10)参与取模计算,一旦生成的字节落在剩下的"多余"范围(250-255),就直接丢弃重新生成一个,直到落在能整除的范围内为止——这样保证 0-9 每个数字出现的概率完全相等,不会有任何偏差。
虽然安全场景需要不可预测的真随机,但有些场景恰恰相反,需要"随机但可复现"——比如生成一批用于测试的 Mock 数据,如果这次生成的数据和上次完全不同,调试和对比问题会很麻烦;这类场景通常会让用户手动指定一个"随机种子",同一个种子配合同一套伪随机算法,每次都能生成完全一致的数据集,既保留了数据"看起来随机、覆盖各种情况"的特性,又保证了结果可以复现、方便团队协作时对照同一份测试数据。