1. 程式人生 > >用redis的scan命令代替keys命令,以及在spring-data-redis中遇到的問題

用redis的scan命令代替keys命令,以及在spring-data-redis中遇到的問題

有一個 arr 問題 public 條件 列表 position cannot clas

摘要

本文主要是介紹使用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中遇到的問題