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萬以內,建議直接設置為與所搜集大小相同