實戰中使用 Redis 統計線上使用者人數
在構建應用的時候, 我們經常需要對使用者的一舉一動進行記錄, 而其中一個比較重要的操作, 就是對線上的使用者進行記錄。
本文將介紹四種使用 Redis 對線上使用者進行記錄的方案, 這些方案雖然都可以對線上使用者的數量進行統計, 但每個方案都有一些自己特有的操作, 並且各個方案的效能特徵以及資源消耗也各有不同。
方案 1 :使用有序集合
每當一個使用者上線時, 我們就執行 ZADD 命令, 將這個使用者以及它的線上時間新增到指定的有序集合中:ZADD "online_users" <user_id> <current_timestamp>
通過使用 ZSCORE 命令檢查指定的使用者 ID 在有序集合中是否有相關聯的分值, 我們可以知道該使用者是否線上:
ZSCORE "online_users" <user_id>
而通過執行 ZCARD 命令, 我們可以知道總共有多使用者線上:
ZCARD "online_users"
使用有序集合儲存線上使用者的強大之處在於, 它是本文介紹的所有方案當中, 能夠執行最多聚合操作的一個方案, 原因在於, 這一方案既可以通過有序集合的成員(也即是使用者的 ID)進行聚合操作, 也可以根據有序集合的分值(也即是使用者的登入時間)進行聚合操作。
首先, 通過 ZINTERSTORE 和 ZUNIONSTORE 命令, 我們可以對多個記錄了線上使用者的有序集合進行聚合計算:
# 計算出 7 天之內都有上線的使用者,並將它儲存到 7_days_both_online_users 有序集合當中 ZINTERSTORE 7_days_both_online_users 7 "day_1_online_users" "day_2_online_users" ... "day_7_online_users" # 計算出 7 天之內總共有多少人上線了 ZUNIONSTORE 7_days_total_online_users 7 "day_1_online_users" ... "day_7_online_users"
此外, 通過 ZCOUNT 命令, 我們可以統計出在指定的時間段之內有多少使用者線上, 而 ZRANGEBYSCORE 命令則可以讓我們獲取到這些使用者的名單:
# 統計指定時間段內上線的使用者數量
ZCOUNT "online_users" <start_timestamp> <end_timestamp>
# 獲取指定時間段內上線的使用者名稱單
ZRANGEBYSCORE "online_users" <start_timestamp> <end_timestamp> WITHSCORES
通過這一方法, 我們可以知道網站在不同時間段的上線人數以及上線使用者名稱單, 比如說, 我們可以用這個方法來分別獲知網站在早晨、上午、中午、下午和夜晚的上線人數。
方案 2 :使用集合
正如上一節所說, 使用有序集合能夠同時儲存線上使用者的名單以及各個使用者的上線時間, 但如果我們只想要記錄線上使用者的名單, 而不想要儲存使用者的上線時間, 那麼也可以使用集合來代替有序集合, 對線上的使用者進行記錄。
在這種情況下, 每當一個使用者上線時, 我們就執行以下 SADD 命令, 將它新增到線上使用者名稱單當中:
SADD "online_users" <user_id>
通過使用 SISMEMBER 命令, 我們可以檢查一個指定的使用者當前是否線上:
SISMEMBER "online_users" <user_id>
而統計線上人數的工作則可以通過執行 SCARD 命令來完成:
SCARD "online_users"
通過集合運算操作, 我們可以像有序集合方案一樣, 對不同時間段或者日期的線上使用者名稱單進行聚合計算。 比如說, 通過 SINTER 或者 SINTERSTORE 命令, 我們可以計算出一週都有線上的使用者:
SINTER "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
此外, 通過 SUNION 命令或者 SUNIONSTORE 命令, 我們可以計算出一週內線上使用者的總數量:
SUNION "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
而通過執行 SDIFF 命令或者 SDIFFSTORE 命令, 我們可以知道哪些使用者今天上線了, 但是昨天沒有上線:
SDIFF "today_online_users" "yesterday_online_users"
又或者工作日上線了, 但是假日沒有上線:
# 計算工作日上線名單
SINTERSTORE "weekday_online_users" "monday_online_users" "tuesday_online_users" ... "friday_online_users"
# 計算假日上線名單
SINTERSTORE "holiday_online_users" "saturday_online_users" "sunday_online_users"
# 計算工作日上線但是假日未上線的名單
SDIFF "weekday_online_users" "holiday_online_users"
方案 3 :使用 HyperLogLog
雖然使用有序集合和集合能夠很好地完成記錄線上人數的工作, 但以上這兩個方案都有一個明顯的缺點, 那就是, 這兩個方案耗費的記憶體會隨著被統計使用者數量的增多而增多: 如果你的網站使用者數量比較多, 又或者你需要記錄多天/多個時段的線上使用者名稱單並進行聚合計算, 那麼這兩個方案可能會消耗你大量記憶體。
另一方面, 在有些情況下, 我們只想要知道線上使用者的人數, 而不需要知道具體的線上使用者名稱單, 這時有序集合和集合儲存的資訊就會顯得多餘了。
在需要儘可能地節約記憶體並且只需要知道線上使用者數量的情況下, 我們可以使用 HyperLogLog 來對線上使用者進行統計: HyperLogLog 是一個概率演算法, 它可以對元素的基數進行估算, 並且每個 HyperLogLog 只需要耗費 12 KB 記憶體, 對於使用者數量非常多但是記憶體卻非常緊張的系統, 這一方案無疑是最佳之選。
在這一方案下, 我們使用 PFADD 命令去記錄線上的使用者:
PFADD "online_users" <user_id>
使用 PFCOUNT 命令獲取線上人數:
PFCOUNT "online_users"
因為 HyperLogLog 也提供了計算交集的 PFMERGE 命令, 所以我們也可以用這個命令計算出多個給定時間段或日期之內, 上線的總人數:
# 統計 7 天之內總共有多少人上線了
PFMERGE "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
PFCOUNT "7_days_both_online_users"
方案 4 :使用點陣圖(bitmap)
回顧上面介紹的三個方案, 我們可以得出以上結論:
- 使用有序集合或者集合能夠儲存具體的線上使用者名稱單, 但是卻需要消耗大量的記憶體;
- 而使用 HyperLogLog 雖然能夠有效地減少統計線上使用者所需的記憶體, 但是它卻沒辦法準確地記錄具體的線上使用者名稱單。
那麼是否存在一種既能夠獲得線上使用者名稱單, 又可以儘量減少記憶體消耗的方法存在呢? 這種方法的確存在 —— 使用 Redis 的點陣圖就可以辦到。
Redis 的點陣圖就是一個由二進位制位組成的陣列, 通過將陣列中的每個二進位制位與使用者 ID 進行一一對應, 我們可以使用點陣圖去記錄每個使用者是否線上。
當一個使用者上線時, 我們就使用 SETBIT 命令, 將這個使用者對應的二進位制位設定為 1 :
# 此處的 user_id 必須為數字,因為它會被用作索引
SETBIT "online_users" <user_id> 1
通過使用 GETBIT 命令去檢查一個二進位制位的值是否為 1 , 我們可以知道指定的使用者是否線上:
GETBIT "online_users" <user_id>
而通過 BITCOUNT 命令, 我們可以統計出點陣圖中有多少個二進位制位被設定成了 1 , 也即是有多少個使用者線上:
BITCOUNT "online_users"
跟集合一樣, 使用者也能夠對多個位圖進行聚合計算 —— 通過 BITOP 命令, 使用者可以對一個或多個位圖執行邏輯並、邏輯或、邏輯異或或者邏輯非操作:
# 計算出 7 天都線上的使用者
BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
# 計算出 7 在的線上使用者總人數
BITOP "OR" "7_days_total_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
# 計算出兩天當中只有其中一天線上的使用者
BITOP "XOR" "only_one_day_online" "day_1_online_users" "day_2_online_users"
HyperLogLog 方案記錄一個使用者是否線上需要花費 1 個二進位制位, 對於使用者數為 100 萬的網站來說, 使用這一方案只需要耗費 125 KB 記憶體, 而對於使用者數為 1000 萬的網站來說, 使用這一方案也只需要花費 1.25 MB 記憶體。
雖然點陣圖節約記憶體的效果不及 HyperLogLog 那麼顯著, 但是使用點陣圖可以準確地判斷一個使用者是否上線, 並且能夠像集合和有序集合一樣, 對線上使用者名稱單進行聚合計算。 因此對於想要儘量節約記憶體, 但又需要準確地知道使用者是否線上, 又或者需要對使用者的線上名單進行聚合計算的應用來說, 使用點陣圖可以說是最佳之選。
總結
以下表格總結了以上四個方案的特點:
方案 | 特點 |
---|---|
有序集合 | 能夠同時儲存線上使用者的名單以及使用者的上線時間,能夠執行非常多的聚合計算操作,但是耗費的記憶體也非常多。 |
集合 | 能夠儲存線上使用者的名單,也能夠執行聚合計算,消耗的記憶體比有序集合少,但是跟有序集合一樣,這個方案消耗的記憶體也會隨著使用者數量的增多而增多。 |
HyperLogLog | 無論需要統計的使用者有多少,只需要耗費 12 KB 記憶體,但由於概率演算法的特性,只能給出線上人數的估算值,並且也無法獲取準確的線上使用者名稱單。 |
點陣圖 | 在儘可能節約記憶體的情況下,記錄線上使用者的名單,並且能夠對這些名單執行聚合操作。 |