用redis的scan命令代替keys命令,以及在spring-data-redis中遇到的問題
摘要
本文主要是介紹使用redis scan命令遇到的一些問題總結,scan命令本身沒有什麽問題,主要是spring-data-redis的問題。
需求
需要遍歷redis中key,找到符合某些pattern的所有keys。第一反應當然是
KEYS "ABC*
可以找到前綴是ABC的所有KEYS,時間復雜度O(N)。可以使用,但是在生產環境中,這麽使用肯定是不行的,因為生產環境的key的數量比較多,一次查詢會block其他操作。而更重要的是一次性返回這麽多的key,數據量比較大,網絡傳輸成本高。所以一般生產環境中去找符合某些條件的KEYS一般使用SCAN 或 Sets。
集合來操作比較好理解,一個個的pop出來,但是相當於在原有的數據結構上多了一個keys的set集合。SCAN的不需要多維護這份列表。
SCAN 命令
SCAN命令的有SCAN,SSCAN,HSCAN,ZSCAN。
SCAN的話就是遍歷所有的keys
其他的SCAN命令的話是SCAN選中的集合。
SCAN命令是增量的循環,每次調用只會返回一小部分的元素。所以不會有KEYS
命令的坑。
SCAN命令返回的是一個遊標,從0開始遍歷,到0結束遍歷。
scan 0 1) "655" 2) 1) "test1" 2) "test2"
返回值一個array,一個是下次循環的cursorId,一個是元素數組。SCAN命令不能保證每次返回的值都是有序的,另外同一個key有可能返回多次,不做區分,需要應用程序去處理。
另外SCAN命令可以指定COUNT,默認是10。但是這個並不是指定多少,就能返回多少,這只是一個提示,並不能保證一定返回這麽多條。
spring-data-redis SCAN命令的坑
拋出NoSuchElementException 錯誤
RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection(); Cursor c = redisConnection.scan(scanOptions); while (c.hasNext()) { c.next(); } java.util.NoSuchElementException at java.util.Collections$EmptyIterator.next(Collections.java:4189) at org.springframework.data.redis.core.ScanCursor.moveNext(ScanCursor.java:215) at org.springframework.data.redis.core.ScanCursor.next(ScanCursor.java:202)
這個錯誤發生在spring-data-redis-1.6版本中。已經被修掉了,
https://github.com/spring-projects/spring-data-redis/pull/154
看到最後comments 1.5.x 和1.6.x中都修復了,但是不知道為什麽1.6.0沒有修復。
看下ScanCursor.java 源碼,異常時next()方法拋出來的,產生的原因是沒有next的元素了。在前面介紹過,SCAN命令返回兩個一個cursorId,一個是值數組。即使你指定了返回多少條(COUNT),也不能保證實際會返回多少條,當然包括返回0條。這種情況不會經常發生,當你redis server中有大量小的集合時,而掃描時又掃不到匹配的keys,就會返回0個結果,但這並不表示掃描結束,掃描結束的唯一判斷依據是掃描結果返回的cursor = 0
@Override public T next() { assertCursorIsOpen(); if (!hasNext()) { throw new NoSuchElementException("No more elements available for cursor " + cursorId + "."); } T next = moveNext(delegate); position++; return next; }
這個錯誤最好的解決辦法是升級spring-data-redis版本。如果沒法升級,只能在程序中捕獲這個異常,再發一次scan請求。而不是依賴spring-data-redis中的scan請求發送。
多線程環境使用的坑
返回這種錯誤,
java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.List at redis.clients.jedis.Connection.getRawObjectMultiBulkReply(Connection.java:230) at redis.clients.jedis.Connection.getObjectMultiBulkReply(Connection.java:236)
或者unknown reply錯誤。
這個的原因是在一次full 掃描期間,發送一次scan請求,返回遊標結果,connection釋放掉了,再發送scan請求時,又拿到一個新的連接。這個在單線程環境下,沒有問題,但是在多線程環境下,一般來說沒有問題,因為scan 命令server沒有狀態,只有一個cursorId。一個線程scan一次完了,釋放掉連接,再發送時,拿到一個新的連接,沒有問題,但是如果拿到其他線程的連接就會出現上述問題。
這個問題在spring-data-redis 1.8 RC1 版本修復。就是每個scan操作的cursor維護一個connection。
如果低版本需要修復的話,就是連接不要交給spring-data-redis管理了,獲取一個連接,自己維護。
用redis的scan命令代替keys命令,以及在spring-data-redis中遇到的問題