經典面試題【Redis】

(一)如何訪問 Redis 中的海量數據,服務纔不會掛掉?


1、事故產生
因爲我們的用戶token緩存是採用了【user_token:userid】格式的key,保存用戶的token的值。我們運維爲了幫助開發小夥伴們查一下線上現在有多少登錄用戶。直接操作上線 redis,執行 keys * wxdb(此處省略)cf8* 這樣的命令,導致redis鎖住,導致 CPU 飆升,引起所有支付鏈路卡住,等十幾秒結束後,所有的請求流量全部擠壓到了 rds 數據庫中,使數據庫產生了雪崩效應,發生了數據庫宕機事件。

我們線上的登錄用戶有幾百萬,數據量比較多;keys算法是遍歷算法,複雜度是O(n),也就是數據越多,時間越高。數據量達到幾百萬,keys這個指令就會導致 Redis 服務卡頓,因爲 Redis 是單線程程序,順序執行所有指令,其它指令必須等到當前的 keys 指令執行完了纔可以繼續

2、哪些危險命令
keys:客戶端可查詢出所有存在的鍵。
flushdb:刪除 Redis 中當前所在數據庫中的所有記錄,並且此命令從不會執行失敗。
flushall:刪除 Redis 中所有數據庫中的所有記錄,不只是當前所在數據庫,並且此命令從不會執行失敗。
config:客戶端可修改 Redis 配置。

3、禁用或重命名危險命令
看下 redis.conf 默認配置文件,找到 SECURITY 區域,如以下所示。

看說明,添加 rename-command 配置即可達到安全目的。

1)禁用命令

rename-command KEYS     ""
rename-command FLUSHALL ""
rename-command FLUSHDB  ""
rename-command CONFIG   ""

2)重命名命令

rename-command KEYS     "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
rename-command FLUSHALL "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
rename-command FLUSHDB  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
rename-command CONFIG   "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

上面的 XX 可以定義新命令名稱,或者用隨機字符代替。經過以上的設置之後,危險命令就不會被客戶端執行了。

4、解決方案

(1)、我們可以採用Redis的另一個命令scan。

我們看一下scan的特點:

  • 複雜度雖然也是 O(n),但是它是通過遊標分步進行的,不會阻塞線程

  • 提供 count 參數,不是結果數量,是Redis單次遍歷字典槽位數量(約等於)

  • 同 keys 一樣,它也提供模式匹配功能;

  • 服務器不需要爲遊標保存狀態,遊標的唯一狀態就是 scan 返回給客戶端的遊標整數;

  • 返回的結果可能會有重複,需要客戶端去重複,這點非常重要;

  • 單次返回的結果是空的並不意味着遍歷結束,而要看返回的遊標值是否爲零

 scan命令格式 

 命令解釋 

scan 遊標 MATCH <返回和給定模式相匹配的元素> count 每次迭代所返回的元素數量。

  • SCAN命令是增量的循環,每次調用只會返回一小部分的元素。所以不會讓Redis假死;

  • SCAN命令返回的是一個遊標,從0開始遍歷,到0結束遍歷;

 舉例 

從0開始遍歷,返回了遊標6,又返回了數據,繼續scan遍歷,就要從6開始;

(2)、使用有序集合

每當一個用戶上線時, 我們就執行 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

通過這一方法, 我們可以知道網站在不同時間段的上線人數以及上線用戶名單, 比如說, 我們可以用這個方法來分別獲知網站在早晨、上午、中午、下午和夜晚的上線人數。

(3)、使用集合

如果我們只想要記錄在線用戶的名單, 而不想要儲存用戶的上線時間, 那麼也可以使用集合來代替有序集合, 對在線的用戶進行記錄。

在這種情況下, 每當一個用戶上線時, 我們就執行以下 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"

(4)、使用 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"

(五)、使用位圖(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 內存,但由於概率算法的特性,只能給出在線人數的估算值,並且也無法獲取準確的在線用戶名單。
位圖 在儘可能節約內存的情況下,記錄在線用戶的名單,並且能夠對這些名單執行聚合操作。

因爲 Redis 同時支持多種數據結構, 所以一個問題常常可以在 Redis 裏面找多種不同的解法, 並且每種解法都有各自的優點和缺點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章