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