數據表設計
CREATE TABLE user_online (
uid INT UNSIGNED NOT NULL,
session_id CHAR(32) NOT NULL,
login_time INT UNSIGNED NOT NULL,
logout_time INT UNSIGNED NOT NULL,
PRIMARY KEY (uid, session_id)
);
方案一:客戶端定時心跳
// 登錄時插入
$stmt = $pdo->prepare('INSERT INTO user_online (uid, session_id, login_time, logout_time) VALUES (?, ?, ?, ?)');
$stmt->execute([$uid, $sid, $now, $now + 600]);
// 頁面 JS 每 10 分鐘發送心跳
setInterval(() => fetch('/api/heartbeat'), 10 * 60 * 1000);
// 後端更新
$pdo->prepare('UPDATE user_online SET logout_time = ? WHERE uid = ? AND session_id = ?')
->execute([time() + 600, $uid, $sid]);
- 優點:準確度高(可控間隔)。
- 缺點:依賴 JS,刷新/關頁即停止;AJAX 較多的頁面(如 WebGame)適用,傳統多頁應用不適用。
方案二:服務端定時掃描
// Crontab 每 5 分鐘執行
// php /path/scan_online.php
$stmt = $pdo->query('SELECT uid, session_id FROM user_online WHERE logout_time > ' . time());
while ($row = $stmt->fetch()) {
// 檢查 session 文件是否存在
if (file_exists(session_save_path() . '/sess_' . $row['session_id'])) {
$pdo->prepare('UPDATE user_online SET logout_time = ? WHERE uid = ? AND session_id = ?')
->execute([time() + 300, $row['uid'], $row['session_id']]);
} else {
// session 已失效,可選刪除記錄
}
}
- 優點:不依賴前端,準確反映 session 存活。
- 缺點:需服務器控制權(crontab/systemd timer),虛擬主機無法使用;掃描大量記錄有性能開銷。
方案三:用戶活動時更新(推薦通用)
// 每次頁面請求(公共入口文件/中間件)
if (!empty($_SESSION['uid'])) {
$pdo->prepare('UPDATE user_online SET logout_time = ? WHERE uid = ? AND session_id = ?')
->execute([time() + 600, $_SESSION['uid'], session_id()]);
}
// 登出時
$pdo->prepare('DELETE FROM user_online WHERE uid = ? AND session_id = ?')
->execute([$_SESSION['uid'], session_id()]);
- 優點:實現簡單、無額外基礎設施、適用大多數項目、準確度可接受(±10 分鐘)。
- 缺點:增加每請求一次 DB 寫入(可用 Redis 緩存批量落庫優化)。
優化建議
- Redis 緩存寫入:請求時只更新 Redis
HSET online:{uid} logout_time {timestamp},定時腳本每分鐘批量同步到 MySQL。
- 複合索引:
INDEX idx_logout (logout_time) 加速掃描/清理。
- 在線人數統計:
SELECT COUNT(*) FROM user_online WHERE logout_time > UNIX_TIMESTAMP()。