
前言升级到 PHP 8.x 之后很多人第一次看到Random\Randomizer和四个Random\Engine\*类时都会愣一下以前一个mt_rand()走天下现在让我「选择算法」到底该选哪个先说清楚版本关系因为标题里的版号需要修正Random\Randomizer这套面向对象的随机数 API 是 PHP 8.2 引入的不是 8.3。PHP 8.2 带来了引擎Engine与随机化器Randomizer的分离以及Mt19937、PcgOneseq128XslRr64、Xoshiro256StarStar、Secure四个内置引擎。PHP 8.3 是在此基础上做加法补上了Random\Randomizer::getFloat()、Random\Randomizer::getBytesFromString()和边界枚举Random\IntervalBoundary。所以本文的标题虽然写的是 8.3但一切以 8.2 为基础涉及 8.3 新增的方法会单独标出。真正要解决的问题只有一个这个随机数如果被别人猜到后果有多严重后果严重令牌、验证码、抽奖结果、加密用途就必须用密码学安全的引擎后果不严重洗牌、分流、造测试数据用非安全引擎换取速度和可复现性。本文给出选择标准、一张对照表和可以直接跑的验证脚本。一、引擎与随机化器是两层新的 API 把「产生随机字节」和「把字节变成有用的值」拆成了两层层职责类你需要关心的引擎 Engine源源不断吐出随机字节实现Random\Engine接口安全性、速度、能否设种子随机化器 Randomizer把字节转成整数/浮点/字符串/洗牌结果Random\Randomizer取值的边界、分布是否正确四个内置引擎引擎可设种子密码学安全典型用途Random\Engine\Mt19937可以否与老代码mt_rand()行为对齐、需要固定序列的模拟Random\Engine\PcgOneseq128XslRr64可以否通用非安全场景洗牌、抽样、造数据Random\Engine\Xoshiro256StarStar可以否同上另一套统计特性与状态大小Random\Engine\Secure不可以是所有安全敏感场景也是默认引擎这里最关键的一行是Secure那一行它不可设种子。因为它取的是操作系统提供的密码学安全随机源Windows 上是BCryptGenRandom一类接口类 Unix 上是getrandom(2)或/dev/urandom没有「种子」这个概念。这直接决定了它的用途——能复现的随机数一定不安全安全的随机数一定不能复现。二、怎么选先问「被猜到会怎样」场景后果选择密码重置令牌、邮箱验证码、API Key账号被接管Random\Engine\Secure或random_bytes()抽奖中奖者、优惠券发放顺序可被薅羊毛Random\Engine\Secure加密盐、初始化向量加密体系被破Random\Engine\Secure洗牌、随机排序、A/B 分流用户体验受影响PcgOneseq128XslRr64或Xoshiro256StarStar造测试数据、压测流量无上述任一非安全引擎 显式种子复现某个历史随机序列无Mt19937 与老代码相同的种子三条实操规则不确定就用默认的。new Random\Randomizer()不传引擎时用的是Secure。性能上它不是瓶颈——现代系统的 CSPRNG密码学安全伪随机数生成器本来就是为通用场景设计的真正的开销差异要看你每秒要取多少个值而绝大多数 Web 请求每秒取的随机数用十个手指头数得过来。需要复现就一定不是安全场景。只要你在构造函数里传了种子就说明这个随机序列允许被别人知道那就别拿它生成任何凭证。非安全引擎三选一不用纠结。Mt19937的价值主要是「与mt_rand()一致」用于兼容老代码新写的非安全场景直接用PcgOneseq128XslRr64或Xoshiro256StarStar哪个更快请自己在目标环境上实测不同 PHP 版本与 CPU 架构下的排序并不总是一致。另外要记住换了引擎不等于换了Randomizer的方法。getInt()、shuffleArray()这些方法在哪个引擎下都可用它们的区别只在于底层字节从哪来。三、安全边界这些函数不是密码学安全的新 API 不会自动保护你——只要你还在用老函数风险照旧。函数密码学安全说明random_bytes()/random_int()是PHP 7.0 起提供底层就是 CSPRNGRandom\Engine\Secure是8.2 起new Random\Randomizer()不传引擎是内部用Securerand()/mt_rand()否可预测仅凭少量输出即可还原内部状态shuffle()/str_shuffle()/array_rand()否使用非密码学的内部随机源uniqid()否基于微秒时间戳可枚举lcg_value()否线性同余统计质量差也就是说把mt_rand(100000, 999999)当短信验证码发出去是这套 API 里最危险的一种用法——攻击者拿到几个验证码样本后理论上可以推算出后续序列。四、PHP 8.3 新增的方法在 8.2 的基础上8.3 补齐了几块明显的短板?php declare(strict_types1); // 需要 PHP 8.3 $r new Random\Randomizer(); // 默认使用 Secure 引擎 // 1) 取浮点数并指定边界开闭getFloat(float $min, float $max, IntervalBoundary $boundary) $lat $r-getFloat(-90, 90, Random\IntervalBoundary::ClosedClosed); // [-90, 90] $lng $r-getFloat(-180, 180, Random\IntervalBoundary::OpenClosed); // (-180, 180] // 2) 从指定字符集中取字节组成字符串getBytesFromString(string $string, int $length) $token $r-getBytesFromString(abcdefghijklmnopqrstuvwxyz0123456789, 32);IntervalBoundary枚举的四个取值是把数学区间记法直接搬了过来枚举值区间含义ClosedOpen[min, max)默认值含下界不含上界ClosedClosed[min, max]上下界都可取到OpenClosed(min, max]不含下界含上界OpenOpen(min, max)上下界都取不到为什么浮点数要专门搞个边界参数因为浮点数无法在任意区间里做到「每个值等概率」——需要决定端点是否允许被返回以及如何处理舍入。getFloat()把这个决策显式地摆到参数上而不是像老代码那样靠mt_rand() / mt_getrandmax()自己乘是对的。getBytesFromString()有个很实用的性质字符在源字符串里重复出现就等于给它加权。这在抽奖里非常方便?php declare(strict_types1); // 需要 PHP 8.3 $r new Random\Randomizer(); // a 出现 3 次b 出现 1 次 —— 抽到 a 的概率是 75% $winner $r-getBytesFromString(aaab, 1); // a 或 b注意它是按字节挑选不是按字符。源字符串里如果有中文UTF-8 下一个汉字占 3 字节长度的语义就变成了字节数很容易把一个汉字切成半个。加权字符集请只用 ASCII。五、完整可运行示例把下面这段存成rand_demo.php直接运行需要 PHP 8.3。它包含四个真实场景以及一段让你在自己机器上实测的性能对比?php declare(strict_types1); // 需要 PHP 8.3getFloat / getBytesFromString / IntervalBoundary 均为 8.3 新增 // ---------- 1. 安全场景密码重置令牌 ---------- function passwordResetToken(int $bytes 32): string { // 默认引擎就是 Secure这里显式写出来是为了让审阅者一眼看到安全意图 $r new Random\Randomizer(new Random\Engine\Secure()); return bin2hex($r-getBytes($bytes)); } echo 重置令牌 . passwordResetToken() . \n; // ---------- 2. 安全场景6 位数字验证码避免取模偏差---------- function numericOtp(int $digits 6): string { $r new Random\Randomizer(); $out ; for ($i 0; $i $digits; $i) { // getInt 的上下界都是闭区间 $out . (string) $r-getInt(0, 9); } return $out; } echo 验证码 . numericOtp() . \n; // ---------- 3. 非安全但需要复现固定种子的洗牌 ---------- function seededShuffle(array $items, int $seed): array { $r new Random\Randomizer(new Random\Engine\PcgOneseq128XslRr64($seed)); return $r-shuffleArray($items); } $deck range(1, 10); $a seededShuffle($deck, 20240929); $b seededShuffle($deck, 20240929); printf(种子相同结果相同%s\n, $a $b ? 是 : 否); printf(洗牌结果%s\n, implode(,, $a)); // ---------- 4. 加权抽奖 浮点坐标 ---------- $r new Random\Randomizer(); $prizes $r-getBytesFromString(aaabbc, 1); // a:50% b:33.3% c:16.7% echo 抽奖结果$prizes\n; $lat $r-getFloat(-90, 90, Random\IntervalBoundary::ClosedClosed); $lng $r-getFloat(-180, 180, Random\IntervalBoundary::OpenClosed); printf(随机坐标%.6f, %.6f\n, $lat, $lng); // ---------- 5. 性能对比在你的机器上自己跑我不给结论 ---------- function bench(string $label, callable $fn, int $rounds 200000): void { $t0 hrtime(true); for ($i 0; $i $rounds; $i) { $fn(); } $ms (hrtime(true) - $t0) / 1e6; printf(%-34s %d 次共用 %.1f ms\n, $label, $rounds, $ms); } $secure new Random\Randomizer(new Random\Engine\Secure()); $pcg new Random\Randomizer(new Random\Engine\PcgOneseq128XslRr64(1)); $xoshiro new Random\Randomizer(new Random\Engine\Xoshiro256StarStar(1)); bench(Secure::getInt, static fn () $secure-getInt(0, 1000)); bench(PcgOneseq128XslRr64::getInt, static fn () $pcg-getInt(0, 1000)); bench(Xoshiro256StarStar::getInt, static fn () $xoshiro-getInt(0, 1000)); bench(mt_rand老函数, static fn () mt_rand(0, 1000)); bench(random_int老函数, static fn () random_int(0, 1000));在一台普通开发机上输出大致长这样具体数字每次都不一样请以你自己跑出来的为准重置令牌9f2c1b7e4a08d3... 验证码471902 种子相同结果相同是 洗牌结果6,2,9,1,7,4,10,3,5,8 抽奖结果a 随机坐标-13.482910, 176.201385 Secure::getInt 200000 次共用 ... ms PcgOneseq128XslRr64::getInt 200000 次共用 ... ms Xoshiro256StarStar::getInt 200000 次共用 ... ms mt_rand老函数 200000 次共用 ... ms random_int老函数 200000 次共用 ... ms跑完你就有了自己环境上的真实数据。如果Secure与PcgOneseq128XslRr64的差距在你的业务量级下可以忽略对绝大多数 Web 应用来说是这样那唯一正确的选择就是默认的Secure。常见坑点1. 用mt_rand()生成令牌❌$token md5(mt_rand());——Mt19937不是密码学安全的观察到足够多的输出后可以还原内部状态并预测后续值md5也改变不了这一点。 ✅bin2hex(random_bytes(32))或(new Random\Randomizer())-getBytes(32)。2. 用时间当种子❌new Random\Engine\Mt19937(time());—— 种子空间只有几千万种可能攻击者枚举一遍就能复现你的「随机」序列。 ✅ 需要复现的场景种子来自配置或测试固件需要安全的场景用Secure它压根不给你种子。3. 拿Secure引擎想要「可复现」❌new Random\Randomizer(new Random\Engine\Secure())然后指望两次运行结果一致 ——Secure不接受种子输出永远不可复现。 ✅ 要复现就换成Mt19937/PcgOneseq128XslRr64/Xoshiro256StarStar并显式传种子同时确认这个随机数不用于安全用途。4. 忘了getInt()的上下界都是闭区间❌ 想取一个 0 到 99 的值却写$r-getInt(0, 100)结果偶尔出现 100或者想取[1, 6]的骰子写成getInt(1, 5)。 ✅getInt($min, $max)两端都可取到。要[0, 100)就写getInt(0, 99)$min大于$max会直接抛ValueError。5. 用shuffle()/str_shuffle()生成安全相关的顺序❌ 用str_shuffle(0123456789)取前 6 位做验证码 —— 这些函数用的是非密码学的内部随机源且内部算法是公开的洗牌。 ✅ 用$r-shuffleArray($digits)$r为默认的安全引擎或者直接numericOtp()那样逐位getInt()。6. 用getBytesFromString()处理非 ASCII 字符集❌$r-getBytesFromString(甲乙丙丁, 2)—— 它是按字节挑的会从汉字中间切开产出非法 UTF-8。 ✅ 加权字符集只用 ASCII要处理多字节字符就先按字符数组取索引再映射回字符。7. 把引擎对象序列化进缓存或 Session❌ 把Random\Engine存进 Session 或 Redis指望跨请求延续序列 —— 引擎的内部状态是随机数质量的一部分序列化与共享会带来不可预期的分布问题也容易在并发下互相干扰。 ✅ 引擎只在单个请求/单次计算内创建需要跨请求的「随机」就用数据库或缓存里的持久化值本身。8. 假设随机 API 永远不会失败❌$t random_bytes(16);完全不考虑系统熵源不可用的情况 —— 它会抛异常未捕获就是 500。 ✅ 用try { ... } catch (Throwable $e) { /* 记录并降级为不可发放凭证 */ }包住异常的具体类型随版本变化捕获Throwable最稳。真正要记住的是熵源拿不到时必须拒绝发放凭证而不是退回mt_rand()。总结问题答案Random\Randomizer是哪个版本引入的PHP 8.2标题里的 8.3 只对了后半段PHP 8.3 加了什么getFloat()、getBytesFromString()与边界枚举IntervalBoundarygetBytes()、getInt()、shuffleArray()在 8.2 就有默认引擎是什么Random\Engine\Secure安全场景选哪个Secure或者random_bytes()/random_int()非安全但要快/要复现PcgOneseq128XslRr64/Xoshiro256StarStar/Mt19937可设种子和mt_rand()对齐Random\Engine\Mt19937老函数还能用吗能用但rand/mt_rand/shuffle/array_rand/uniqid都不是密码学安全的选算法这件事说到底只有一句话先判断「被猜到会怎样」再决定用不用安全引擎。判断不了就一律用默认的Secure——它慢不到哪里去而选错的代价是账号被接管。真正需要非安全引擎的场合只有两类需要复现的测试与模拟以及需要与老代码序列对齐的兼容场景。把这两类和业务凭证严格分开选型就不会出错。