xref: /redis-3.2.3/src/db.c (revision 22c9c407)
1 #include "redis.h"
2 
3 #include <signal.h>
4 #include <ctype.h>
5 
6 void SlotToKeyAdd(robj *key);
7 void SlotToKeyDel(robj *key);
8 
9 /*-----------------------------------------------------------------------------
10  * C-level DB API
11  *----------------------------------------------------------------------------*/
12 
13 robj *lookupKey(redisDb *db, robj *key) {
14     dictEntry *de = dictFind(db->dict,key->ptr);
15     if (de) {
16         robj *val = dictGetVal(de);
17 
18         /* Update the access time for the aging algorithm.
19          * Don't do it if we have a saving child, as this will trigger
20          * a copy on write madness. */
21         if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
22             val->lru = server.lruclock;
23         return val;
24     } else {
25         return NULL;
26     }
27 }
28 
29 robj *lookupKeyRead(redisDb *db, robj *key) {
30     robj *val;
31 
32     expireIfNeeded(db,key);
33     val = lookupKey(db,key);
34     if (val == NULL)
35         server.stat_keyspace_misses++;
36     else
37         server.stat_keyspace_hits++;
38     return val;
39 }
40 
41 robj *lookupKeyWrite(redisDb *db, robj *key) {
42     expireIfNeeded(db,key);
43     return lookupKey(db,key);
44 }
45 
46 robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {
47     robj *o = lookupKeyRead(c->db, key);
48     if (!o) addReply(c,reply);
49     return o;
50 }
51 
52 robj *lookupKeyWriteOrReply(redisClient *c, robj *key, robj *reply) {
53     robj *o = lookupKeyWrite(c->db, key);
54     if (!o) addReply(c,reply);
55     return o;
56 }
57 
58 /* Add the key to the DB. It's up to the caller to increment the reference
59  * counte of the value if needed.
60  *
61  * The program is aborted if the key already exists. */
62 void dbAdd(redisDb *db, robj *key, robj *val) {
63     sds copy = sdsdup(key->ptr);
64     int retval = dictAdd(db->dict, copy, val);
65 
66     redisAssertWithInfo(NULL,key,retval == REDIS_OK);
67     if (server.cluster_enabled) SlotToKeyAdd(key);
68  }
69 
70 /* Overwrite an existing key with a new value. Incrementing the reference
71  * count of the new value is up to the caller.
72  * This function does not modify the expire time of the existing key.
73  *
74  * The program is aborted if the key was not already present. */
75 void dbOverwrite(redisDb *db, robj *key, robj *val) {
76     struct dictEntry *de = dictFind(db->dict,key->ptr);
77 
78     redisAssertWithInfo(NULL,key,de != NULL);
79     dictReplace(db->dict, key->ptr, val);
80 }
81 
82 /* High level Set operation. This function can be used in order to set
83  * a key, whatever it was existing or not, to a new object.
84  *
85  * 1) The ref count of the value object is incremented.
86  * 2) clients WATCHing for the destination key notified.
87  * 3) The expire time of the key is reset (the key is made persistent). */
88 void setKey(redisDb *db, robj *key, robj *val) {
89     if (lookupKeyWrite(db,key) == NULL) {
90         dbAdd(db,key,val);
91     } else {
92         dbOverwrite(db,key,val);
93     }
94     incrRefCount(val);
95     removeExpire(db,key);
96     signalModifiedKey(db,key);
97 }
98 
99 int dbExists(redisDb *db, robj *key) {
100     return dictFind(db->dict,key->ptr) != NULL;
101 }
102 
103 /* Return a random key, in form of a Redis object.
104  * If there are no keys, NULL is returned.
105  *
106  * The function makes sure to return keys not already expired. */
107 robj *dbRandomKey(redisDb *db) {
108     struct dictEntry *de;
109 
110     while(1) {
111         sds key;
112         robj *keyobj;
113 
114         de = dictGetRandomKey(db->dict);
115         if (de == NULL) return NULL;
116 
117         key = dictGetKey(de);
118         keyobj = createStringObject(key,sdslen(key));
119         if (dictFind(db->expires,key)) {
120             if (expireIfNeeded(db,keyobj)) {
121                 decrRefCount(keyobj);
122                 continue; /* search for another key. This expired. */
123             }
124         }
125         return keyobj;
126     }
127 }
128 
129 /* Delete a key, value, and associated expiration entry if any, from the DB */
130 int dbDelete(redisDb *db, robj *key) {
131     /* Deleting an entry from the expires dict will not free the sds of
132      * the key, because it is shared with the main dictionary. */
133     if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
134     if (dictDelete(db->dict,key->ptr) == DICT_OK) {
135         if (server.cluster_enabled) SlotToKeyDel(key);
136         return 1;
137     } else {
138         return 0;
139     }
140 }
141 
142 long long emptyDb() {
143     int j;
144     long long removed = 0;
145 
146     for (j = 0; j < server.dbnum; j++) {
147         removed += dictSize(server.db[j].dict);
148         dictEmpty(server.db[j].dict);
149         dictEmpty(server.db[j].expires);
150     }
151     return removed;
152 }
153 
154 int selectDb(redisClient *c, int id) {
155     if (id < 0 || id >= server.dbnum)
156         return REDIS_ERR;
157     c->db = &server.db[id];
158     return REDIS_OK;
159 }
160 
161 /*-----------------------------------------------------------------------------
162  * Hooks for key space changes.
163  *
164  * Every time a key in the database is modified the function
165  * signalModifiedKey() is called.
166  *
167  * Every time a DB is flushed the function signalFlushDb() is called.
168  *----------------------------------------------------------------------------*/
169 
170 void signalModifiedKey(redisDb *db, robj *key) {
171     touchWatchedKey(db,key);
172 }
173 
174 void signalFlushedDb(int dbid) {
175     touchWatchedKeysOnFlush(dbid);
176 }
177 
178 /*-----------------------------------------------------------------------------
179  * Type agnostic commands operating on the key space
180  *----------------------------------------------------------------------------*/
181 
182 void flushdbCommand(redisClient *c) {
183     server.dirty += dictSize(c->db->dict);
184     signalFlushedDb(c->db->id);
185     dictEmpty(c->db->dict);
186     dictEmpty(c->db->expires);
187     addReply(c,shared.ok);
188 }
189 
190 void flushallCommand(redisClient *c) {
191     signalFlushedDb(-1);
192     server.dirty += emptyDb();
193     addReply(c,shared.ok);
194     if (server.rdb_child_pid != -1) {
195         kill(server.rdb_child_pid,SIGKILL);
196         rdbRemoveTempFile(server.rdb_child_pid);
197     }
198     if (server.saveparamslen > 0) {
199         /* Normally rdbSave() will reset dirty, but we don't want this here
200          * as otherwise FLUSHALL will not be replicated nor put into the AOF. */
201         int saved_dirty = server.dirty;
202         rdbSave(server.rdb_filename);
203         server.dirty = saved_dirty;
204     }
205     server.dirty++;
206 }
207 
208 void delCommand(redisClient *c) {
209     int deleted = 0, j;
210 
211     for (j = 1; j < c->argc; j++) {
212         if (dbDelete(c->db,c->argv[j])) {
213             signalModifiedKey(c->db,c->argv[j]);
214             server.dirty++;
215             deleted++;
216         }
217     }
218     addReplyLongLong(c,deleted);
219 }
220 
221 void existsCommand(redisClient *c) {
222     expireIfNeeded(c->db,c->argv[1]);
223     if (dbExists(c->db,c->argv[1])) {
224         addReply(c, shared.cone);
225     } else {
226         addReply(c, shared.czero);
227     }
228 }
229 
230 void selectCommand(redisClient *c) {
231     int id = atoi(c->argv[1]->ptr);
232 
233     if (server.cluster_enabled && id != 0) {
234         addReplyError(c,"SELECT is not allowed in cluster mode");
235         return;
236     }
237     if (selectDb(c,id) == REDIS_ERR) {
238         addReplyError(c,"invalid DB index");
239     } else {
240         addReply(c,shared.ok);
241     }
242 }
243 
244 void randomkeyCommand(redisClient *c) {
245     robj *key;
246 
247     if ((key = dbRandomKey(c->db)) == NULL) {
248         addReply(c,shared.nullbulk);
249         return;
250     }
251 
252     addReplyBulk(c,key);
253     decrRefCount(key);
254 }
255 
256 void keysCommand(redisClient *c) {
257     dictIterator *di;
258     dictEntry *de;
259     sds pattern = c->argv[1]->ptr;
260     int plen = sdslen(pattern), allkeys;
261     unsigned long numkeys = 0;
262     void *replylen = addDeferredMultiBulkLength(c);
263 
264     di = dictGetIterator(c->db->dict);
265     allkeys = (pattern[0] == '*' && pattern[1] == '\0');
266     while((de = dictNext(di)) != NULL) {
267         sds key = dictGetKey(de);
268         robj *keyobj;
269 
270         if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
271             keyobj = createStringObject(key,sdslen(key));
272             if (expireIfNeeded(c->db,keyobj) == 0) {
273                 addReplyBulk(c,keyobj);
274                 numkeys++;
275             }
276             decrRefCount(keyobj);
277         }
278     }
279     dictReleaseIterator(di);
280     setDeferredMultiBulkLength(c,replylen,numkeys);
281 }
282 
283 void dbsizeCommand(redisClient *c) {
284     addReplyLongLong(c,dictSize(c->db->dict));
285 }
286 
287 void lastsaveCommand(redisClient *c) {
288     addReplyLongLong(c,server.lastsave);
289 }
290 
291 void typeCommand(redisClient *c) {
292     robj *o;
293     char *type;
294 
295     o = lookupKeyRead(c->db,c->argv[1]);
296     if (o == NULL) {
297         type = "none";
298     } else {
299         switch(o->type) {
300         case REDIS_STRING: type = "string"; break;
301         case REDIS_LIST: type = "list"; break;
302         case REDIS_SET: type = "set"; break;
303         case REDIS_ZSET: type = "zset"; break;
304         case REDIS_HASH: type = "hash"; break;
305         default: type = "unknown"; break;
306         }
307     }
308     addReplyStatus(c,type);
309 }
310 
311 void shutdownCommand(redisClient *c) {
312     int flags = 0;
313 
314     if (c->argc > 2) {
315         addReply(c,shared.syntaxerr);
316         return;
317     } else if (c->argc == 2) {
318         if (!strcasecmp(c->argv[1]->ptr,"nosave")) {
319             flags |= REDIS_SHUTDOWN_NOSAVE;
320         } else if (!strcasecmp(c->argv[1]->ptr,"save")) {
321             flags |= REDIS_SHUTDOWN_SAVE;
322         } else {
323             addReply(c,shared.syntaxerr);
324             return;
325         }
326     }
327     if (prepareForShutdown(flags) == REDIS_OK) exit(0);
328     addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
329 }
330 
331 void renameGenericCommand(redisClient *c, int nx) {
332     robj *o;
333     long long expire;
334 
335     /* To use the same key as src and dst is probably an error */
336     if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) {
337         addReply(c,shared.sameobjecterr);
338         return;
339     }
340 
341     if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
342         return;
343 
344     incrRefCount(o);
345     expire = getExpire(c->db,c->argv[1]);
346     if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
347         if (nx) {
348             decrRefCount(o);
349             addReply(c,shared.czero);
350             return;
351         }
352         /* Overwrite: delete the old key before creating the new one with the same name. */
353         dbDelete(c->db,c->argv[2]);
354     }
355     dbAdd(c->db,c->argv[2],o);
356     if (expire != -1) setExpire(c->db,c->argv[2],expire);
357     dbDelete(c->db,c->argv[1]);
358     signalModifiedKey(c->db,c->argv[1]);
359     signalModifiedKey(c->db,c->argv[2]);
360     server.dirty++;
361     addReply(c,nx ? shared.cone : shared.ok);
362 }
363 
364 void renameCommand(redisClient *c) {
365     renameGenericCommand(c,0);
366 }
367 
368 void renamenxCommand(redisClient *c) {
369     renameGenericCommand(c,1);
370 }
371 
372 void moveCommand(redisClient *c) {
373     robj *o;
374     redisDb *src, *dst;
375     int srcid;
376 
377     if (server.cluster_enabled) {
378         addReplyError(c,"MOVE is not allowed in cluster mode");
379         return;
380     }
381 
382     /* Obtain source and target DB pointers */
383     src = c->db;
384     srcid = c->db->id;
385     if (selectDb(c,atoi(c->argv[2]->ptr)) == REDIS_ERR) {
386         addReply(c,shared.outofrangeerr);
387         return;
388     }
389     dst = c->db;
390     selectDb(c,srcid); /* Back to the source DB */
391 
392     /* If the user is moving using as target the same
393      * DB as the source DB it is probably an error. */
394     if (src == dst) {
395         addReply(c,shared.sameobjecterr);
396         return;
397     }
398 
399     /* Check if the element exists and get a reference */
400     o = lookupKeyWrite(c->db,c->argv[1]);
401     if (!o) {
402         addReply(c,shared.czero);
403         return;
404     }
405 
406     /* Return zero if the key already exists in the target DB */
407     if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
408         addReply(c,shared.czero);
409         return;
410     }
411     dbAdd(dst,c->argv[1],o);
412     incrRefCount(o);
413 
414     /* OK! key moved, free the entry in the source DB */
415     dbDelete(src,c->argv[1]);
416     server.dirty++;
417     addReply(c,shared.cone);
418 }
419 
420 /*-----------------------------------------------------------------------------
421  * Expires API
422  *----------------------------------------------------------------------------*/
423 
424 int removeExpire(redisDb *db, robj *key) {
425     /* An expire may only be removed if there is a corresponding entry in the
426      * main dict. Otherwise, the key will never be freed. */
427     redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
428     return dictDelete(db->expires,key->ptr) == DICT_OK;
429 }
430 
431 void setExpire(redisDb *db, robj *key, long long when) {
432     dictEntry *kde, *de;
433 
434     /* Reuse the sds from the main dict in the expire dict */
435     kde = dictFind(db->dict,key->ptr);
436     redisAssertWithInfo(NULL,key,kde != NULL);
437     de = dictReplaceRaw(db->expires,dictGetKey(kde));
438     dictSetSignedIntegerVal(de,when);
439 }
440 
441 /* Return the expire time of the specified key, or -1 if no expire
442  * is associated with this key (i.e. the key is non volatile) */
443 long long getExpire(redisDb *db, robj *key) {
444     dictEntry *de;
445 
446     /* No expire? return ASAP */
447     if (dictSize(db->expires) == 0 ||
448        (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
449 
450     /* The entry was found in the expire dict, this means it should also
451      * be present in the main dict (safety check). */
452     redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
453     return dictGetSignedIntegerVal(de);
454 }
455 
456 /* Propagate expires into slaves and the AOF file.
457  * When a key expires in the master, a DEL operation for this key is sent
458  * to all the slaves and the AOF file if enabled.
459  *
460  * This way the key expiry is centralized in one place, and since both
461  * AOF and the master->slave link guarantee operation ordering, everything
462  * will be consistent even if we allow write operations against expiring
463  * keys. */
464 void propagateExpire(redisDb *db, robj *key) {
465     robj *argv[2];
466 
467     argv[0] = shared.del;
468     argv[1] = key;
469     incrRefCount(argv[0]);
470     incrRefCount(argv[1]);
471 
472     if (server.aof_state != REDIS_AOF_OFF)
473         feedAppendOnlyFile(server.delCommand,db->id,argv,2);
474     if (listLength(server.slaves))
475         replicationFeedSlaves(server.slaves,db->id,argv,2);
476 
477     decrRefCount(argv[0]);
478     decrRefCount(argv[1]);
479 }
480 
481 int expireIfNeeded(redisDb *db, robj *key) {
482     long long when = getExpire(db,key);
483 
484     if (when < 0) return 0; /* No expire for this key */
485 
486     /* Don't expire anything while loading. It will be done later. */
487     if (server.loading) return 0;
488 
489     /* If we are running in the context of a slave, return ASAP:
490      * the slave key expiration is controlled by the master that will
491      * send us synthesized DEL operations for expired keys.
492      *
493      * Still we try to return the right information to the caller,
494      * that is, 0 if we think the key should be still valid, 1 if
495      * we think the key is expired at this time. */
496     if (server.masterhost != NULL) {
497         return mstime() > when;
498     }
499 
500     /* Return when this key has not expired */
501     if (mstime() <= when) return 0;
502 
503     /* Delete the key */
504     server.stat_expiredkeys++;
505     propagateExpire(db,key);
506     return dbDelete(db,key);
507 }
508 
509 /*-----------------------------------------------------------------------------
510  * Expires Commands
511  *----------------------------------------------------------------------------*/
512 
513 /* Given an string object return true if it contains exactly the "ms"
514  * or "MS" string. This is used in order to check if the last argument
515  * of EXPIRE, EXPIREAT or TTL is "ms" to switch into millisecond input/output */
516 int stringObjectEqualsMs(robj *a) {
517     char *arg = a->ptr;
518     return tolower(arg[0]) == 'm' && tolower(arg[1]) == 's' && arg[2] == '\0';
519 }
520 
521 void expireGenericCommand(redisClient *c, long long offset, int unit) {
522     dictEntry *de;
523     robj *key = c->argv[1], *param = c->argv[2];
524     long long milliseconds;
525 
526     if (getLongLongFromObjectOrReply(c, param, &milliseconds, NULL) != REDIS_OK)
527         return;
528 
529     if (unit == UNIT_SECONDS) milliseconds *= 1000;
530     milliseconds -= offset;
531 
532     de = dictFind(c->db->dict,key->ptr);
533     if (de == NULL) {
534         addReply(c,shared.czero);
535         return;
536     }
537     /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
538      * should never be executed as a DEL when load the AOF or in the context
539      * of a slave instance.
540      *
541      * Instead we take the other branch of the IF statement setting an expire
542      * (possibly in the past) and wait for an explicit DEL from the master. */
543     if (milliseconds <= 0 && !server.loading && !server.masterhost) {
544         robj *aux;
545 
546         redisAssertWithInfo(c,key,dbDelete(c->db,key));
547         server.dirty++;
548 
549         /* Replicate/AOF this as an explicit DEL. */
550         aux = createStringObject("DEL",3);
551         rewriteClientCommandVector(c,2,aux,key);
552         decrRefCount(aux);
553         signalModifiedKey(c->db,key);
554         addReply(c, shared.cone);
555         return;
556     } else {
557         long long when = mstime()+milliseconds;
558         setExpire(c->db,key,when);
559         addReply(c,shared.cone);
560         signalModifiedKey(c->db,key);
561         server.dirty++;
562         return;
563     }
564 }
565 
566 void expireCommand(redisClient *c) {
567     expireGenericCommand(c,0,UNIT_SECONDS);
568 }
569 
570 void expireatCommand(redisClient *c) {
571     expireGenericCommand(c,mstime(),UNIT_SECONDS);
572 }
573 
574 void pexpireCommand(redisClient *c) {
575     expireGenericCommand(c,0,UNIT_MILLISECONDS);
576 }
577 
578 void pexpireatCommand(redisClient *c) {
579     expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
580 }
581 
582 void ttlGenericCommand(redisClient *c, int output_ms) {
583     long long expire, ttl = -1;
584 
585     expire = getExpire(c->db,c->argv[1]);
586     if (expire != -1) {
587         ttl = expire-mstime();
588         if (ttl < 0) ttl = -1;
589     }
590     if (ttl == -1) {
591         addReplyLongLong(c,-1);
592     } else {
593         addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
594     }
595 }
596 
597 void ttlCommand(redisClient *c) {
598     ttlGenericCommand(c, 0);
599 }
600 
601 void pttlCommand(redisClient *c) {
602     ttlGenericCommand(c, 1);
603 }
604 
605 void persistCommand(redisClient *c) {
606     dictEntry *de;
607 
608     de = dictFind(c->db->dict,c->argv[1]->ptr);
609     if (de == NULL) {
610         addReply(c,shared.czero);
611     } else {
612         if (removeExpire(c->db,c->argv[1])) {
613             addReply(c,shared.cone);
614             server.dirty++;
615         } else {
616             addReply(c,shared.czero);
617         }
618     }
619 }
620 
621 /* -----------------------------------------------------------------------------
622  * API to get key arguments from commands
623  * ---------------------------------------------------------------------------*/
624 
625 int *getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, int *numkeys) {
626     int j, i = 0, last, *keys;
627     REDIS_NOTUSED(argv);
628 
629     if (cmd->firstkey == 0) {
630         *numkeys = 0;
631         return NULL;
632     }
633     last = cmd->lastkey;
634     if (last < 0) last = argc+last;
635     keys = zmalloc(sizeof(int)*((last - cmd->firstkey)+1));
636     for (j = cmd->firstkey; j <= last; j += cmd->keystep) {
637         redisAssert(j < argc);
638         keys[i++] = j;
639     }
640     *numkeys = i;
641     return keys;
642 }
643 
644 int *getKeysFromCommand(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
645     if (cmd->getkeys_proc) {
646         return cmd->getkeys_proc(cmd,argv,argc,numkeys,flags);
647     } else {
648         return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
649     }
650 }
651 
652 void getKeysFreeResult(int *result) {
653     zfree(result);
654 }
655 
656 int *noPreloadGetKeys(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
657     if (flags & REDIS_GETKEYS_PRELOAD) {
658         *numkeys = 0;
659         return NULL;
660     } else {
661         return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
662     }
663 }
664 
665 int *renameGetKeys(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
666     if (flags & REDIS_GETKEYS_PRELOAD) {
667         int *keys = zmalloc(sizeof(int));
668         *numkeys = 1;
669         keys[0] = 1;
670         return keys;
671     } else {
672         return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
673     }
674 }
675 
676 int *zunionInterGetKeys(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
677     int i, num, *keys;
678     REDIS_NOTUSED(cmd);
679     REDIS_NOTUSED(flags);
680 
681     num = atoi(argv[2]->ptr);
682     /* Sanity check. Don't return any key if the command is going to
683      * reply with syntax error. */
684     if (num > (argc-3)) {
685         *numkeys = 0;
686         return NULL;
687     }
688     keys = zmalloc(sizeof(int)*num);
689     for (i = 0; i < num; i++) keys[i] = 3+i;
690     *numkeys = num;
691     return keys;
692 }
693 
694 /* Slot to Key API. This is used by Redis Cluster in order to obtain in
695  * a fast way a key that belongs to a specified hash slot. This is useful
696  * while rehashing the cluster. */
697 void SlotToKeyAdd(robj *key) {
698     unsigned int hashslot = keyHashSlot(key->ptr,sdslen(key->ptr));
699 
700     zslInsert(server.cluster.slots_to_keys,hashslot,key);
701     incrRefCount(key);
702 }
703 
704 void SlotToKeyDel(robj *key) {
705     unsigned int hashslot = keyHashSlot(key->ptr,sdslen(key->ptr));
706 
707     zslDelete(server.cluster.slots_to_keys,hashslot,key);
708 }
709 
710 unsigned int GetKeysInSlot(unsigned int hashslot, robj **keys, unsigned int count) {
711     zskiplistNode *n;
712     zrangespec range;
713     int j = 0;
714 
715     range.min = range.max = hashslot;
716     range.minex = range.maxex = 0;
717 
718     n = zslFirstInRange(server.cluster.slots_to_keys, range);
719     while(n && n->score == hashslot && count--) {
720         keys[j++] = n->obj;
721         n = n->level[0].forward;
722     }
723     return j;
724 }
725