尧图精选

PHP8.2的Random扩展怎么替代rand函数

🕒 发布时间:2026/10/1 4:21:29 📁 来源:尧图网络
前言rand()和mt_rand()用了二十年问题一直存在但很少有人细究它们的随机性来自一个全局的、进程级的状态任何一处代码调用mt_srand()都会影响全局它们的算法是可预测的梅森旋转Mersenne TwisterMT从输出反推出内部状态在密码学上并不困难shuffle()、array_rand()、str_shuffle()又都悄悄依赖同一个全局状态模块之间互相干扰。PHP 8.2 引入了 Random 扩展Random extension提供了一套面向对象的新 APIRandom\Randomizer加上可替换的随机引擎engine。这个版本号是准确的——Random\Randomizer、Random\Engine\Mt19937、Random\Engine\Secure等确实在 PHP 8.2 落地本文全部按 8.2 及以上的语法讲。它解决的问题有三个可复现的测试固定种子每次跑出同一串随机数、不同用途用不同的随机源抽奖用密码学安全的Secure模拟数据用可复现的Mt19937、模块之间互不干扰每个Randomizer实例持有自己独立的引擎状态不再有全局的mt_srand()污染。本文按新旧 API 对照 → 引擎怎么选 → 常用方法 → 完整可运行示例的顺序展开最后列出替代过程中最容易踩的坑。一、新旧 API 对照需求老写法8.2 起的写法取区间整数rand(1, 6)/mt_rand(1, 6)$r-getInt(1, 6)取随机字节random_bytes(16)见说明$r-getBytes(16)打乱数组shuffle($arr)原地修改$r-shuffleArray($arr)返回新数组打乱字符串str_shuffle($s)$r-shuffleBytes($s)播种mt_srand(42)new Randomizer(new Mt19937(42))类型数组 / 整数强类型对象需要说明的两点random_bytes()/random_int()PHP 7.0 起本身就是密码学安全的并不因为 8.2 而作废。它们的定位是我需要安全随机且只想调一个函数而Randomizer的定位是我要一个可注入、可替换、可复现的随机源。两者可以共存业务代码注入Randomizer底层安全引擎就是Secure。rand()/mt_rand()在 8.2 里没有被移除也没有被废弃老代码不会突然报错。替代是主动选择不是被迫迁移。二、引擎Engine怎么选Random 扩展的核心设计是随机源与使用方式分离。Randomizer决定怎么用取整数、洗牌、取字节Engine决定随机数从哪来。引擎类特点适用场景Random\Engine\Mt19937梅森旋转可通过种子复现测试数据、模拟、需要固定种子的场景Random\Engine\PcgOneseq128XslRr64PCG 家族统计性质比 MT 更好同样可播种可复现需要大量随机数、注重统计质量Random\Engine\Xoshiro256StarStar现代快速算法可播种可复现同上的另一种选择Random\Engine\Secure使用操作系统的密码学安全随机源令牌、验证码、抽奖、密码重置Secure引擎实现了Random\CryptoSafeEngine接口该接口继承自Random\Engine语义上就是在提示这个引擎的随机源是安全的。反过来任何CryptoSafeEngine都不能被播种——安全随机源不该让你复现。构造方式?php declare(strict_types1); use Random\Engine\Mt19937; use Random\Engine\Secure; use Random\Engine\Xoshiro256StarStar; use Random\Randomizer; // 可复现同一个种子得到同一串结果 $reproducible new Randomizer(new Mt19937(20240101)); // 不可复现的安全随机 $secure new Randomizer(new Secure()); // 也可以换别的可复现引擎 $fast new Randomizer(new Xoshiro256StarStar(20240101));每个Randomizer实例持有自己的引擎对象状态互不影响。这一点正是老 API 最大的痛点以前mt_srand()是全局的一个库为了可复现而播种会把整个应用的随机序列都改掉。三、Randomizer 的常用方法?php declare(strict_types1); use Random\Engine\Mt19937; use Random\Randomizer; $r new Randomizer(new Mt19937(1234)); $r-getInt(1, 6); // 区间内的整数闭区间两端都能取到 $r-getBytes(16); // 16 字节的二进制串 $r-shuffleArray([1,2,3]); // 返回打乱后的新数组原数组不变 $r-shuffleBytes(abcde); // 返回打乱后的新字符串三点容易记错的语义getInt($min, $max)是闭区间getInt(1, 6)能取到 1 也能取到 6和掷骰子的直觉一致。区间内部由扩展做了无偏采样不会出现取模偏差。shuffleArray()返回新数组不像shuffle()那样原地修改并返回true。写$r-shuffleArray($arr);却不接收返回值数组不会有任何变化。引擎对象本身也是可序列化的Secure除外这意味着可以把一个带着种子和当前状态的引擎存起来下次继续产生可预期的序列——测试里很有用。如果代码需要同时兼容 8.2 以下用class_exists()判定?php declare(strict_types1); function makeRandomizer(): object { if (class_exists(\Random\Randomizer::class)) { return new \Random\Randomizer(new \Random\Engine\Secure()); } // 8.2 以下的退路random_int() 就是 CSPRNG return new class { public function getInt(int $min, int $max): int { return random_int($min, $max); } }; }四、实战完整可运行示例下面这个脚本需要 PHP 8.2 或更高版本Random\Randomizer的最低要求直接保存成.php用命令行运行即可。它演示固定种子的可复现性、不同引擎的差异、以及替代rand()/shuffle()/str_shuffle()的写法。?php declare(strict_types1); use Random\Engine\Mt19937; use Random\Engine\PcgOneseq128XslRr64; use Random\Engine\Secure; use Random\Engine\Xoshiro256StarStar; use Random\Randomizer; if (PHP_VERSION_ID 80200) { exit(本示例需要 PHP 8.2 或更高版本当前为 . PHP_VERSION . \n); } // ---------- 1. 固定种子 → 结果可复现 ---------- echo 1. 可复现性 \n; $a new Randomizer(new Mt19937(20240101)); $b new Randomizer(new Mt19937(20240101)); $sa []; $sb []; for ($i 0; $i 5; $i) { $sa[] $a-getInt(1, 100); $sb[] $b-getInt(1, 100); } echo 序列 A: , implode(,, $sa), PHP_EOL; echo 序列 B: , implode(,, $sb), PHP_EOL; echo 两者相同: , var_export($sa $sb, true), PHP_EOL; // 老 API 的全局污染问题mt_srand() 影响的是全局状态 mt_srand(1); $global1 mt_rand(1, 100); mt_srand(1); $global2 mt_rand(1, 100); echo mt_srand 全局播种两次结果相同: , var_export($global1 $global2, true), PHP_EOL; // mt_srand() 不会影响已经构造好的 Randomizer 实例 mt_srand(999); $c new Randomizer(new Mt19937(20240101)); $cSeq []; for ($i 0; $i 5; $i) { $cSeq[] $c-getInt(1, 100); } echo Randomizer 不受 mt_srand 影响: , var_export($cSeq $sa, true), PHP_EOL; // ---------- 2. 不同引擎的差异 ---------- echo \n 2. 引擎对比 \n; $engines [ new Mt19937(7), new PcgOneseq128XslRr64(7), new Xoshiro256StarStar(7), ]; foreach ($engines as $engine) { $r new Randomizer($engine); printf(%-42s %s\n, get_class($engine), bin2hex($r-getBytes(8))); } $secure new Randomizer(new Secure()); echo str_pad(Random\Engine\Secure (每次不同), 42), bin2hex($secure-getBytes(8)), PHP_EOL; // ---------- 3. 替代 rand()掷骰子统计 ---------- echo \n 3. 替代 rand() \n; $r new Randomizer(new Mt19937(42)); $counts array_fill(1, 6, 0); $rolls 60000; for ($i 0; $i $rolls; $i) { $counts[$r-getInt(1, 6)]; } foreach ($counts as $face $n) { printf(点 %d: %5d 次 (%.2f%%)\n, $face, $n, $n / $rolls * 100); } // ---------- 4. 替代 shuffle() / array_rand() ---------- echo \n 4. 替代 shuffle() \n; $deck range(1, 10); $r new Randomizer(new Mt19937(2024)); $shuffled $r-shuffleArray($deck); // 返回新数组 echo 原数组: , implode(,, $deck), PHP_EOL; echo 打乱后: , implode(,, $shuffled), PHP_EOL; // 取 N 个不重复元素先打乱再取前 N 个 $picked array_slice($r-shuffleArray($deck), 0, 3); echo 随机取 3 个: , implode(,, $picked), PHP_EOL; // ---------- 5. 替代 str_shuffle() ---------- echo \n 5. 替代 str_shuffle() \n; $chars 23456789ABCDEFGHJKLMNPQRSTUVWXYZ; $r new Randomizer(new Secure()); $code ; for ($i 0; $i 8; $i) { $code . $chars[$r-getInt(0, strlen($chars) - 1)]; } echo 随机码: , $code, PHP_EOL; // 用 shuffleBytes 达到同样效果 $suffix substr($r-shuffleBytes($chars), 0, 8); echo shuffleBytes: , $suffix, PHP_EOL; // ---------- 6. 浮点随机注意版本差异 ---------- echo \n 6. 浮点随机 \n; $r new Randomizer(new Mt19937(1)); if (method_exists($r, getFloat)) { printf(getFloat(): %.6f\n, $r-getFloat()); } else { // 8.2 上没有 getFloat()用 getInt() 自己换算 $f $r-getInt(0, 1_000_000) / 1_000_000; printf(getInt() 换算: %.6f\n, $f); }输出示例固定种子的部分每次完全相同 1. 可复现性 序列 A: 83,39,12,... 序列 B: 83,39,12,... 两者相同: true mt_srand 全局播种两次结果相同: true Randomizer 不受 mt_srand 影响: true 2. 引擎对比 Random\Engine\Mt19937 3f0a... Random\Engine\PcgOneseq128XslRr64 91c2... Random\Engine\Xoshiro256StarStar 77de... Random\Engine\Secure (每次不同) a41f... 3. 替代 rand() 点 1: 10042 次 (16.74%) ... 4. 替代 shuffle() 原数组: 1,2,3,4,5,6,7,8,9,10 打乱后: ...具体数值以你自己机器上的输出为准只要种子相同Mt19937那几行每次运行都会一模一样这正是它的用途。常见坑点❌ 抽奖、验证码、令牌继续用rand()/mt_rand()MT 是可预测的算法观察到足够多的连续输出就能反推内部状态进而预测后续结果。✅安全相关的随机一律用new Randomizer(new Secure())或直接用random_int()/random_bytes()。❌ 以为mt_srand()会影响Randomizer两者的状态完全独立播种了mt_srand()却发现Randomizer的结果没变化白排查半天。✅要在Randomizer上复现就给它传种子new Randomizer(new Mt19937(42))。❌ 照抄旧教程里的nextInt()那是 PHP 8.2 测试版时期的命名正式版里叫getInt()写旧的会报Call to undefined method。✅统一用getInt()。❌ 用shuffleArray()像shuffle()那样不接收返回值$r-shuffleArray($arr);之后$arr顺序没变因为shuffleArray()返回的是新数组。✅$arr $r-shuffleArray($arr);或者改用返回值赋给新变量反而更清晰。❌ 用%自己造区间ord($r-getBytes(1)) % 6 1这类写法存在取模偏差低位结果出现频率不均抽奖统计会慢慢对不上。✅用getInt($min, $max)扩展内部已做无偏采样。❌ 把Secure引擎实例序列化缓存安全引擎不可播种、也不支持序列化用它缓存随机数本身就是逻辑错误——缓存下来就不是随机了。✅Secure只在需要时即时取用要复现就换成Mt19937并接受可预测这一前提。❌ 用固定种子生成生产环境的密钥或令牌new Mt19937(20240101)的结果是完全确定的任何人都能重放。✅生产随机的种子交给系统不要显式播种需要可复现的只有测试和模拟数据。❌ 假定Randomizer::getFloat()在所有版本都存在直接调用在缺少该方法的版本上会报Call to undefined method而不是退回默认行为。✅用method_exists($r, getFloat)判断不存在时用getInt()换算示例第 6 节给了写法。总结场景推荐写法说明令牌 / 验证码 / 抽奖new Randomizer(new Secure())密码学安全不可播种测试数据、模拟new Randomizer(new Mt19937($seed))固定种子可复现单元测试断言随机逻辑固定种子的Mt19937断言稳定不 flaky区间整数$r-getInt($min, $max)闭区间无取模偏差打乱数组$r $r-shuffleArray($arr)返回新数组必须接收只想调一个函数random_int()/random_bytes()PHP 7.0 起依然是 CSPRNGRandom 扩展替代rand()的价值不在于更快而在于把随机源变成可以显式选择、可以注入、可以互不干扰的对象安全的归Secure可复现的归Mt19937谁都不会因为别人调了一次mt_srand()而受影响。迁移时只要记住两个语义差异——getInt()是闭区间、shuffleArray()返回新数组——其他的按表和旧代码一一对应替换即可。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →