小編負責的應用是一個管理後台應用,權限管理使用 Shiro 框架,由於存在多個節點,需要使用分散式 Session,於是這裡使用 Redis 儲存 Session 資訊。
由於 Shiro 並沒有直接提供 Redis 儲存 Session 元件,阿粉不得不使用 Github 一個開源元件 shiro-redis。
由於 Shiro 框架需要定期驗證 Session 是否有效,因此 Shiro 底層將會呼叫 SessionDAO#getActiveSessions
取得所有的 Session 資訊。
而shiro-redis
剛好繼承SessionDAO
這個接口,底層使用用keys
指令來尋找Redis 所有儲存的Session
key。
public Set<byte[]> keys(byte[] pattern){ checkAndInit(); Set<byte[]> keys = null; Jedis jedis = jedisPool.getResource(); try{ keys = jedis.keys(pattern); }finally{ jedis.close(); } return keys; }
找到問題原因,解決方法就比較簡單了,github 上查找到解決方案,升級一下 shiro-redis
到最新版本。
在這個版本,shiro-redis
採用 scan
指令取代 keys
,從而修正這個問題。
public Set<byte[]> keys(byte[] pattern) { Set<byte[]> keys = null; Jedis jedis = jedisPool.getResource(); try{ keys = new HashSet<byte[]>(); ScanParams params = new ScanParams(); params.count(count); params.match(pattern); byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY; ScanResult<byte[]> scanResult; do{ scanResult = jedis.scan(cursor,params); keys.addAll(scanResult.getResult()); cursor = scanResult.getCursorAsBytes(); }while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0); }finally{ jedis.close(); } return keys; }
雖然問題成功解決了,但阿粉心裡還是有點不解。
為什麼keys
指令會導致其他指令執行變慢?
為什麼Keys
指令查詢會這麼慢?
為什麼Scan
指令就沒有問題?
首先我們來看第一個問題,為什麼keys
指令會導致其他指令執行變慢?
站在客戶端的視角,執行一條指令分成三步驟:
發送指令
執行指令
返回結果
但是這僅僅是客戶端自己以為的過程,但是實際上同一時刻,可能存在很多客戶端發送命令給Redis ,而Redis 我們都知道它採用的是單線程模型。
為了處理同一時刻所有的客戶端的請求命令,Redis 內部採用了佇列的方式,排隊執行。
於是客戶端執行一條指令實際上需要四個步驟:
發送指令
指令排隊
#由於Redis 單執行緒執行指令,只能依序從佇列取出任務開始執行。
只要 3 這個流程執行指令速度過慢,佇列其他任務不得不進行等待,這對外部客戶端看來,Redis 好像就被阻塞一樣,一直無法回應。
KEYS 原理接下來開始回答第二個問題,為什麼
指令查詢會這麼慢?
回答這個問題之前,請先回想 Redis 底層儲存結構。
不太清楚朋友的也沒關係,大家可以回看一下之前的文章「阿里面試官:HashMap 熟悉吧?好的,那就來聊聊 Redis 字典吧!」。 keys
指令需要回傳所有的符合給定模式pattern
的 Redis 中鍵,為了實現這個目的,Redis 不得不遍歷字典中ht[ 0]哈希表底層數組,這個時間複雜度為
(N 為Redis 中key 所有的數量)。
即使 Redis 中的鍵數量很少,它仍然會有很快的執行速度。當Redis鍵的數量逐漸增多,達到百萬、千萬,甚至上億級時,它的執行速度會變得非常緩慢。 下面是阿粉本地做的一次實驗,使用lua 腳本往Redis 中增加10W 個key,然後使用
查詢所有鍵,這個查詢大概會阻塞十幾秒的時間。這裡阿粉使用 Docker 部署 Redis,效能可能會稍差。eval "for i=1,100000 do redis.call('set',i,i+1) end" 0登入後複製
SCAN 原理最後我們來看下第三個問題,為什麼
指令就沒有問題? 這是因為 scan指令採用一種黑科技-
。 每次呼叫
指令,Redis 都會向使用者傳回一個新的遊標以及一定數量的 key。下次再想繼續取得剩餘的 key,需要將這個遊標傳入 scan 指令, 以此來延續先前的迭代過程。 簡單來講,
指令使用分頁查詢 redis 。
下面是一個scan 指令的迭代過程範例:
指令使用遊標這個方式,巧妙將一次全量查詢拆分成多次,降低查詢複雜度。 雖然
scan
指令時間複雜度與keys一樣,都是「O(N)」
,但由於
指令只需要回傳少量的key,所以執行速度會很快。 最後,雖然
scan
指令解決
在迭代過程中,有可能會傳回正在增加到 Redis 的元素,或是正在被刪除的元素,也有可能不會。
以上這些缺陷,在我們開發中需要考慮這種情況。
除了scan
以外,redis 還有其他幾個用於增量迭代命令:
sscan
:用於迭代目前資料庫中的資料庫鍵,用於解決smembers
可能產生阻塞問題
hscan
指令用於迭代雜湊鍵中的鍵值對,用於解決hgetall
可能產生阻塞問題。
zscan
:指令用於迭代有序集合中的元素(包括元素成員和元素分數),用於產生zrange
#可能產生阻塞問題。
以上是Redis指令使用實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!