百度、騰訊、360 等億級用戶平台,面臨:
核心原則:**壓力不在 PHP(無狀態可水平擴展),瓶頸在 MySQL 寫入與熱點查詢**。
採用 **Hash 取模分 100 表**,鍵值分離,極小行寬:
CREATE TABLE `user_login_00` (
`login_key` VARBINARY(64) NOT NULL COMMENT 'sha256(login_type#login_value)',
`login_type` TINYINT UNSIGNED NOT NULL COMMENT '1=username,2=email,3=phone',
`login_value` VARCHAR(120) NOT NULL COMMENT '原始值,供驗證/找回',
`uid` BIGINT UNSIGNED NOT NULL,
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常,0=禁用',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`login_key`),
KEY `idx_uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登錄映射分表 00';
login_key = substr(hash('sha256', $type . '#' . $value), 0, 16) → 16 字節二進位,節省空間、索引極快。$table = 'user_login_' . substr($login_key, -2) (取最後 2 位十六進位,00-ff,此處簡化為 100 表)。email#user@domain.com、phone#13800000000。同樣按 UID 分 100 表:
CREATE TABLE `user_profile_00` (
`uid` BIGINT UNSIGNED NOT NULL PRIMARY KEY,
`password_hash` CHAR(60) NOT NULL COMMENT 'password_hash() 結果',
`salt` CHAR(16) NOT NULL COMMENT '額外鹽值(可選)',
`nickname` VARCHAR(50),
`avatar` VARCHAR(255),
`gender` TINYINT,
`birthday` DATE,
`reg_ip` INT UNSIGNED,
`last_login_at` DATETIME,
`last_login_ip` INT UNSIGNED,
`status` TINYINT NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
MySQL 自增在分表環境下會產生重複 UID。方案:
INCR global:uid,單機 10 萬 QPS 足夠,配合 Sentinel/Cluster 高可用。REPLACE INTO uid_seq (stub) VALUES ('a') 取 LAST_INSERT_ID(),簡單但寫入單點。推薦:Snowflake (PHP 端生成) 或 Redis INCR (服務端生成)。
function register(string $loginType, string $loginValue, string $password): int
{
// 1. 格式驗證
if (!validate($loginType, $loginValue)) throw new Exception('格式錯誤');
// 2. 產生 UID
$uid = UidGenerator::nextId(); // Snowflake 或 Redis INCR
// 3. 組合登錄鍵
$loginKey = hash('sha256', $loginType . '#' . $loginValue, true);
$loginKeyHex = bin2hex($loginKey);
$tableIdx = substr($loginKeyHex, -2); // 00-ff
$loginTable = "user_login_{$tableIdx}";
// 4. 寫入登錄映射 (唯一鍵衝突即表示已註冊)
$sql = "INSERT INTO {$loginTable} (login_key, login_type, login_value, uid) VALUES (UNHEX(?), ?, ?, ?)";
try {
$db->execute($sql, [$loginKeyHex, $loginType, $loginValue, $uid]);
} catch (UniqueConstraintViolation $e) {
throw new Exception('帳號已存在');
}
// 5. 寫入核心資料
$profileTable = "user_profile_" . ($uid % 100);
$pwdHash = password_hash($password, PASSWORD_ARGON2ID); // PHP 7.2+
$sql = "INSERT INTO {$profileTable} (uid, password_hash, salt, reg_ip) VALUES (?, ?, ?, ?)";
$db->execute($sql, [$uid, $pwdHash, random_bytes(16), ip2long($_SERVER['REMOTE_ADDR'])]);
// 6. 非同步寫入擴展資料、發送歡迎信、風控記錄 (MQ)
MessageQueue::push('user.registered', ['uid' => $uid, 'type' => $loginType]);
return $uid;
}
function login(string $loginValue, string $password): ?string
{
// 1. 嘗試三種類型查找 UID (並行查詢或按優先級)
$loginTypes = [2 => 'email', 3 => 'phone', 1 => 'username']; // 郵箱/手機優先,唯一性高
$uid = null;
foreach ($loginTypes as $type => $label) {
$key = hash('sha256', $type . '#' . $loginValue, true);
$table = "user_login_" . substr(bin2hex($key), -2);
$sql = "SELECT uid, status FROM {$table} WHERE login_key = UNHEX(?)";
$row = $db->fetch($sql, [bin2hex($key)]);
if ($row && $row['status'] == 1) {
$uid = $row['uid'];
break;
}
}
if (!$uid) return null; // 帳號不存在
// 2. 讀取密碼雜湊
$profileTable = "user_profile_" . ($uid % 100);
$sql = "SELECT password_hash, status FROM {$profileTable} WHERE uid = ?";
$user = $db->fetch($sql, [$uid]);
if (!$user || $user['status'] != 1) return null;
// 3. 驗證密碼
if (!password_verify($password, $user['password_hash'])) {
// 記錄失敗次數、風控 (Redis 計數器)
return null;
}
// 4. 更新最後登錄時間 (非同步)
MessageQueue::push('user.login', ['uid' => $uid, 'ip' => $_SERVER['REMOTE_ADDR']]);
// 5. 簽發 Token (JWT 或 Session)
return TokenService::issue($uid);
}
| 層面 | 策略 |
|---|---|
| 查詢路由 | PHP 端計算分表名,直連對應分片,避免中間件轉發延遲 |
| 熱點保護 | 登錄映射表極小,全量放入 Redis Cluster 熱點快取 (TTL 1h),登錄先查 Redis 再落庫 |
| 密碼驗證 | Argon2id 成本參數適中 (time=3, memory=65536, threads=2),防暴力破解不阻塞 |
| 連線池 | Swoole/RoadRunner 長連線 + PDO 連線池,消除 TCP/SSL 握手開銷 |
| 非同步化 | 寫入登錄日誌、更新最後登錄、風控記錄全部推入 MQ (Kafka/RocketMQ) 異步處理 |
| 讀寫分離 | 登錄查詢走從庫 (延遲 < 10ms 可接受),註冊寫入走主庫 |
user_oauth 表 (provider, openid, uid),同樣分片邏輯。user_device、login_risk_log 表,不污染核心路徑。億級登錄系統的核心在於:**鍵值分離、Hash 分片、UID 全域唯一、熱點快取、非同步寫入、無狀態 PHP 水平擴展**。MySQL 單表控制在 2000 萬行以內,單分片 QPS 萬級無壓力。PHP 層只負責路由計算與協議轉換,所有有狀態操作下沉至 Redis/MySQL/MQ,實現線性擴展。