統計在線人數是網站運營基礎指標。本文解析基於 Session + 文件存儲的經典實現,指出其併發與擴展瓶頸,提供 Redis 現代化改造方案與多維度統計擴展思路。
$lastTime) {
fwrite($fp, "{$sessionId}|{$lastTime}\n");
}
flock($fp, LOCK_UN);
}
fclose($fp);
}
// 6. 輸出供 JS 調用(文檔寫入)
echo "document.write('當前在線:{$total_online} 人');";
?>
| 維度 | 問題 | 影響 |
|---|---|---|
| 併發性能 | 文件獨寫鎖(LOCK_EX)序列化所有請求 | QPS 受限 ~100-200,高併發阻塞嚴重 |
| 擴展性 | 單機文件,多服務器無法共享 | 負載均衡場景下數據不一致 |
| 準確性 | 依賴 Cookie,用戶禁用/清除 Cookie 導致重複計數 | 統計偏差大,無法識別同一用戶多設備 |
| 清理機制 | 每請求掃描全文件清理過期 | 文件增大時 IO 開銷線性增長 |
| 數據結構 | 純文本行解析,無索引、無聚合 | 無法按頁面/地域/設備維度統計 |
class OnlineCounter {
private \Redis $redis;
private int $ttl = 600; // 10分鐘在線窗口
public function __construct(\Redis $redis) {
$this->redis = $redis;
}
/** 記錄用戶活躍(每請求調用) */
public function recordActivity(string $userId, array $metadata = []): void {
$key = "online:users:{$userId}";
$now = time();
// Pipeline 批量操作,減少 RTT
$pipe = $this->redis->pipeline();
// 1. 更新用戶最後活躍時間(Sorted Set,score=timestamp)
$pipe->zAdd("online:active", $now, $userId);
// 2. 設置用戶元數據(Hash:IP、UA、頁面、地域等)
if ($metadata) {
$pipe->hSet("online:meta:{$userId}", $metadata);
$pipe->expire("online:meta:{$userId}", $this->ttl * 2);
}
// 3. 頁面級統計(可選)
if (isset($metadata['page'])) {
$pipe->zIncrBy("online:page:{$metadata['page']}", 1, $userId);
}
// 4. 設置用戶級 TTL(自動過期清理)
$pipe->expire($key, $this->ttl);
$pipe->execute();
}
/** 獲取全站在線人數 */
public function getTotalOnline(): int {
return $this->redis->zCount("online:active", time() - $this->ttl, '+inf');
}
/** 獲取指定頁面在線人數 */
public function getPageOnline(string $page): int {
return $this->redis->zCount("online:page:{$page}", time() - $this->ttl, '+inf');
}
/** 獲取在線用戶列表(分頁) */
public function getOnlineUsers(int $page = 1, int $perPage = 50): array {
$start = ($page - 1) * $perPage;
$userIds = $this->redis->zRevRange("online:active", $start, $start + $perPage - 1);
$users = [];
foreach ($userIds as $uid) {
$meta = $this->redis->hGetAll("online:meta:{$uid}");
$meta['id'] = $uid;
$meta['last_active'] = $this->redis->zScore("online:active", $uid);
$users[] = $meta;
}
return $users;
}
/** 定時任務:清理過期數據(每分鐘跑一次) */
public function cleanupExpired(): int {
$removed = $this->redis->zRemRangeByScore("online:active", 0, time() - $this->ttl);
// 元數據由 TTL 自動過期,無需手動清理
return $removed;
}
}
# nginx.conf http 段
lua_shared_dict online_users 10m;
# 在 location / 內
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(10)
red:connect("127.0.0.1", 6379)
local user_id = ngx.var.cookie_user_id or ngx.md5(ngx.var.remote_addr .. ngx.var.http_user_agent)
local now = ngx.time()
local ttl = 600
-- 更新全站活躍集合
red:zadd("online:active", now, user_id)
red:expire("online:active", ttl * 2)
-- 更新頁面統計
local page = ngx.var.uri
red:zincrby("online:page:" .. page, 1, user_id)
red:expire("online:page:" .. page, ttl * 2)
red:set_keepalive(10000, 100)
}
| 維度 | Redis Key 設計 | 統計指標 | 應用場景 |
|---|---|---|---|
| 全站總覽 | online:active (ZSET) | 總在線、峰值、趨勢圖 | 運營大屏、實時監控 |
| 頁面熱度 | online:page:{uri} (ZSET) | 頁面在線、停留分布 | 內容優化、廣告定價 |
| 地域分布 | online:geo:{country}/{province} (ZSET) | 地理分佈、CDN 調度 | 多機房部署、合規審計 |
| 設備類型 | online:device:{mobile/desktop/tablet} (ZSET) | 終端佔比、適配優先級 | 響應式設計、App 推廣 |
| 用戶層級 | online:level:{vip/normal/guest} (ZSET) | 付費用戶活躍度 | 運營活動、流失預警 |
| 實時地圖 | online:geoip (GEOADD) | 經緯度熱力圖 | 大屏展示、應急響應 |
maxmemory-policy allkeys-lru,防內存溢出pconnect 或 Swoole 協程池,避免連接風暴從文件存儲到 Redis 再到 Nginx Lua 邊緣統計,架構演進的核心是將狀態從計算節點剝離到共享存儲,再下沉到流量入口。根據業務規模選擇合適層級:日均萬級用戶用 Redis 方案即可;億級流量需邊緣統計 + 流式計算(Flink/ClickHouse)實現秒級多維聚合。