☰
PHP8.2的Random扩展怎么替代rand函数
2026/10/1 4:21:27 网站建设 项目流程

前言

rand()和mt_rand()用了二十年,问题一直存在但很少有人细究:它们的随机性来自一个全局的、进程级的状态,任何一处代码调用mt_srand()都会影响全局;它们的算法是可预测的梅森旋转(Mersenne Twister,MT),从输出反推出内部状态在密码学上并不困难;shuffle()、array_rand()、str_shuffle()又都悄悄依赖同一个全局状态,模块之间互相干扰。

PHP 8.2 引入了 Random 扩展(Random extension),提供了一套面向对象的新 API:Random\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_types=1); 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_types=1); 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_types=1); 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_types=1); 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那几行每次运行都会一模一样,这正是它的用途。

常见坑点


  1. ❌ 抽奖、验证码、令牌继续用rand()/mt_rand():MT 是可预测的算法,观察到足够多的连续输出就能反推内部状态,进而预测后续结果。


✅安全相关的随机一律用new Randomizer(new Secure()),或直接用random_int()/random_bytes()。


  1. ❌ 以为mt_srand()会影响Randomizer:两者的状态完全独立,播种了mt_srand()却发现Randomizer的结果没变化,白排查半天。


✅要在Randomizer上复现,就给它传种子:new Randomizer(new Mt19937(42))。


  1. ❌ 照抄旧教程里的nextInt():那是 PHP 8.2 测试版时期的命名,正式版里叫getInt(),写旧的会报Call to undefined method。


✅统一用getInt()。


  1. ❌ 用shuffleArray()像shuffle()那样不接收返回值:$r->shuffleArray($arr);之后$arr顺序没变,因为shuffleArray()返回的是新数组。


✅$arr = $r->shuffleArray($arr);,或者改用返回值赋给新变量,反而更清晰。


  1. ❌ 用%自己造区间:ord($r->getBytes(1)) % 6 + 1这类写法存在取模偏差,低位结果出现频率不均,抽奖统计会慢慢对不上。


✅用getInt($min, $max),扩展内部已做无偏采样。


  1. ❌ 把Secure引擎实例序列化缓存:安全引擎不可播种、也不支持序列化,用它缓存随机数本身就是逻辑错误——缓存下来就不是随机了。


✅Secure只在需要时即时取用;要复现就换成Mt19937并接受"可预测"这一前提。


  1. ❌ 用固定种子生成生产环境的密钥或令牌:new Mt19937(20240101)的结果是完全确定的,任何人都能重放。


✅生产随机的种子交给系统,不要显式播种;需要"可复现"的只有测试和模拟数据。


  1. ❌ 假定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 起,依然是 CSPRNG


Random 扩展替代rand()的价值不在于"更快",而在于把随机源变成可以显式选择、可以注入、可以互不干扰的对象:安全的归Secure,可复现的归Mt19937,谁都不会因为别人调了一次mt_srand()而受影响。迁移时只要记住两个语义差异——getInt()是闭区间、shuffleArray()返回新数组——其他的按表和旧代码一一对应替换即可。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询