需求:從 1-100 萬中抽取 100 個不重複隨機數。本文對比 shuffle、array_unique、array_flip 三種經典寫法的性能與適用範圍,並給出生產環境推薦的**密碼學安全隨機數**方案(random_int、openssl_random_pseudo_bytes)。
<?php
function uniqueRandShuffle(int $min, int $max, int $count): array {
if ($count > $max - $min + 1) throw new InvalidArgumentException('Count exceeds range');
$arr = range($min, $max); // 記憶體:O(N)
shuffle($arr); // 時間:O(N)
return array_slice($arr, 0, $count);
}
// 1-10 抽 5 個
print_r(uniqueRandShuffle(1, 10, 5));
// 優點:代碼簡單、真隨機分布
// 缺點:範圍大時記憶體爆炸(1-100萬需 8MB 陣列,尚可;1-10億則不可行)
<?php
function uniqueRandUnique(int $min, int $max, int $count): array {
$result = [];
while (count($result) < $count) {
$result[] = random_int($min, $max); // PHP 7+ 密碼學安全
$result = array_unique($result); // 去重,鍵名保留
}
return array_values($result); // 重置鍵
}
// 優點:記憶體 O(K),不依賴範圍大小
// 缺點:隨抽取數量接近範圍,衝突率指數上升,極慢
<?php
function uniqueRandFlip(int $min, int $max, int $count): array {
$result = [];
while (count($result) < $count) {
$result[] = random_int($min, $max);
$result = array_flip(array_flip($result)); // 利用鍵唯一性去重
}
return array_values($result);
}
<?php
$min=1; $max=100000; $count=1000;
$t=microtime(true); $r=uniqueRandShuffle($min,$max,$count); echo "shuffle: ".(microtime(true)-$t)."s\n"; // ~0.008s
$t=microtime(true); $r=uniqueRandUnique($min,$max,$count); echo "unique: ".(microtime(true)-$t)."s\n"; // ~0.012s
$t=microtime(true); $r=uniqueRandFlip($min,$max,$count); echo "flip: ".(microtime(true)-$t)."s\n"; // ~0.010s
// 結論:範圍可控時 shuffle 最快;範圍極大抽取少時 unique/flip 更省記憶體
?>
<?php
/**
* 從不可知大小的迭代器/生成器中等概率抽取 $k 個元素
* 記憶體 O(K),時間 O(N),單次遍歷,適合大文件、數據庫游標、API 分頁
*/
function reservoirSample(iterable $source, int $k): array {
$reservoir = [];
$i = 0;
foreach ($source as $item) {
$i++;
if ($i <= $k) {
$reservoir[] = $item; // 前 k 個直接放入
} else {
$j = random_int(1, $i); // [1, i] 隨機整數
if ($j <= $k) {
$reservoir[$j - 1] = $item; // 以 k/i 概率替換
}
}
}
return $reservoir;
}
// 示例:從 1000 萬行 CSV 中抽 100 行
$csv = fopen('huge.csv', 'r');
$sample = reservoirSample(new class($csv) implements Iterator {
private $fh; public function __construct($fh){$this->fh=$fh;}
public function current(){return fgetcsv($this->fh);}
public function next(){fgetcsv($this->fh);}
public function key(){return ftell($this->fh);}
public function valid(){return !feof($this->fh);}
public function rewind(){rewind($this->fh);}
}, 100);
fclose($csv);
print_r($sample);
| 函數 | 用途 | 安全性 |
|---|---|---|
| random_int($min, $max) | 整數範圍隨機 | ✅ CSPRNG (系統熵源) |
| random_bytes($length) | 二進制隨機字節 | ✅ CSPRNG |
| openssl_random_pseudo_bytes($len, &$crypto_strong) | OpenSSL 隨機字節 | ✅ 若 $crypto_strong=true |
| mt_rand() / rand() | 舊版偽隨機 | ❌ 可預測,嚴禁用於 Token、密碼、加密 |
| shuffle() / array_rand() | 內部用 mt_rand | ❌ 同左 |
<?php
function generateToken(int $length = 32): string {
// Base64URL 安全字符集(無 + / =)
$bytes = random_bytes($length);
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
function generatePassword(int $length = 16, bool $requireSpecial = true): string {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$special = '!@#$%^&*()-_=+[]{}|;:,.<>?';
$pool = $chars . ($requireSpecial ? $special : '');
$bytes = random_bytes($length);
$pwd = '';
for ($i = 0; $i < $length; $i++) {
$pwd .= $pool[$bytes[$i] % strlen($pool)];
}
// 確保包含各類字符(簡化:重試直到滿足)
if ($requireSpecial && !preg_match('/[!@#$%^&*]/', $pwd)) return generatePassword($length, true);
return $pwd;
}
function generateApiKey(string $prefix = 'sk_'): string {
return $prefix . bin2hex(random_bytes(24)); // 48 字符十六進制
}
// 使用
echo "Token: " . generateToken() . PHP_EOL;
echo "Password: " . generatePassword() . PHP_EOL;
echo "API Key: " . generateApiKey() . PHP_EOL;
?>
需要不重複隨機數? ├─ 範圍小 (≤ 100萬) 且 需全排列/大量抽取 → shuffle(range(...)) ├─ 範圍極大 (≥ 1億) 或 抽取量極小 (≤ 0.1%範圍) → array_unique/flip + random_int() ├─ 數據源為流/生成器/不可知大小 → Reservoir Sampling └─ 安全敏感場景 → 全程使用 random_int()/random_bytes()