Redis Scan的使用方式以及Spring redis的坑


SpringRedisTemplate針對這個Scan進行了封裝,示例使用(針對最新庫spring-data-redis-1.8.1.RELEASE):

Set<Object> execute = redisTemplate.execute(new RedisCallback<Set<Object>>() {

    @Override
    public Set<Object> doInRedis(RedisConnection connection) throws DataAccessException {

        Set<Object> binaryKeys = new HashSet<>();

        Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("test*").count(1000).build());
        while (cursor.hasNext()) {
            binaryKeys.add(new String(cursor.next()));
        }
        return binaryKeys;
    }
});

注意Cursor一定不能關閉,在之前的版本中,這里Cursor需要手動關閉,但是從1.8.0開始,不能手動關閉!否則會報異常。

ScanOptions有兩個參數,一個是match,另一個是count,分別對應scan命令的兩個參數。

Scan命令源碼:

 /* Handle the case of a hash table. */
    ht = NULL;
    if (o == NULL) {//鍵掃描
        ht = c->db->dict;
    } else if (o->type == REDIS_SET && o->encoding == REDIS_ENCODING_HT) {
        ht = o->ptr;
    } else if (o->type == REDIS_HASH && o->encoding == REDIS_ENCODING_HT) {
        ht = o->ptr;
        count *= 2; /* We return key / value for this type. */
    } else if (o->type == REDIS_ZSET && o->encoding == REDIS_ENCODING_SKIPLIST) {
        zset *zs = o->ptr;
        ht = zs->dict;
        count *= 2; /* We return key / value for this type. */
    }
//由於redis的ziplist, intset等類型數據量挺少,所以可用一次返回的。下面的else if 做這個事情。全部返回一個key 。
    if (ht) {//一般的存儲,不是intset, ziplist
        void *privdata[2];

        /* We pass two pointers to the callback: the list to which it will
         * add new elements, and the object containing the dictionary so that
         * it is possible to fetch more data in a type-dependent way. */
        privdata[0] = keys;
        privdata[1] = o;
        do {
            //一個個掃描,從cursor開始,然后調用回調函數將數據設置到keys返回數據集里面。
            cursor = dictScan(ht, cursor, scanCallback, privdata);
        } while (cursor && listLength(keys) < count);     } else if (o->type == REDIS_SET) {
        int pos = 0;
        int64_t ll;

        while(intsetGet(o->ptr,pos++,&ll))//將這個set里面的數據全部返回,因為它是壓縮的intset,會很小的。
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));
        cursor = 0;
    } else if (o->type == REDIS_HASH || o->type == REDIS_ZSET) {//那么一定是ziplist了,字符串表示的數據結構,不會太大。
        unsigned char *p = ziplistIndex(o->ptr,0);
        unsigned char *vstr;
        unsigned int vlen;
        long long vll;

        while(p) {//掃描整個鍵,然后全部返回這一條。並且返回cursor為0表示沒東西了。其實這個就等於沒有遍歷
            ziplistGet(p,&vstr,&vlen,&vll);
            listAddNodeTail(keys,
                 (vstr != NULL) ? createStringObject((char*)vstr,vlen) : createStringObjectFromLongLong(vll));
            p = ziplistNext(o->ptr,p);
        }
        cursor = 0;
    } else {
        redisPanic("Not handled encoding in SCAN.");
    }

可以看出,Redis的SCAN操作由於其整體的數據設計,無法提供特別准的scan操作,僅僅是一個“can ‘ t guarantee , just do my best”的實現:

提供鍵空間的遍歷操作,支持游標,復雜度O(1), 整體遍歷一遍只需要O(N);
提供結果模式匹配;
支持一次返回的數據條數設置,但僅僅是個hints,有時候返回的會多;
弱狀態,所有狀態只需要客戶端需要維護一個游標;
無法提供完整的快照遍歷,也就是中間如果有數據修改,可能有些涉及改動的數據遍歷不到;
每次返回的數據條數不一定,極度依賴內部實現;
返回的數據可能有重復,應用層必須能夠處理重入邏輯;上面的示例代碼中,redisTemplate.execute方法是個Set,相當於已經對於返回的key去重
count是每次掃描的key個數,並不是結果集個數。count要根據掃描數據量大小而定,Scan雖然無鎖,但是也不能保證在超過百萬數據量級別搜索效率;count不能太小,網絡交互會變多,count要盡可能的大。在搜索結果集1萬以內,建議直接設置為與所搜集大小相同


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM