diff options
author | antirez <antirez@gmail.com> | 2018-06-15 13:14:57 +0200 |
---|---|---|
committer | antirez <antirez@gmail.com> | 2019-06-18 12:31:10 +0200 |
commit | e9bb30fd859ed4e9e3e6434207dedbc251086858 (patch) | |
tree | 0ac8972fefe6911dee8c7376e14c611fcc105e1c /src/expire.c | |
parent | fd0ee469ab165d0e005e9fe1fca1c4f5c604cd56 (diff) | |
download | redis-new-keyspace.tar.gz |
Experimental: new keyspace and expire algorithm.new-keyspace
This is an alpha quality implementation of a new keyspace representation
and a new expire algorithm for Redis.
This work is described here:
https://gist.github.com/antirez/b2eb293819666ee104c7fcad71986eb7
Diffstat (limited to 'src/expire.c')
-rw-r--r-- | src/expire.c | 452 |
1 files changed, 319 insertions, 133 deletions
diff --git a/src/expire.c b/src/expire.c index 0b92ee3fe..87c87bccd 100644 --- a/src/expire.c +++ b/src/expire.c @@ -38,9 +38,103 @@ * When keys are accessed they are expired on-access. However we need a * mechanism in order to ensure keys are eventually removed when expired even * if no access is performed on them. + * + * In order to accomplish this every key with an expire is represented in + * two data structures: + * + * 1. The main dictionary of keys, server.db[x]->dict, is an hash table that + * represents the keyspace of a given Redis database. The keys stored + * in the hash table are redisKey structures (typedef 'rkey'). When + * a key has an expire set, the key->flags have the KEY_FLAG_EXPIRE set, + * and the key->expire is populated with the milliseconds unix time at + * which the key will no longer be valid. + * + * 2. Redis also takes a radix tree that is composed only of keys that have + * an expire set, lexicographically sorted by the expire time. Basically + * each key in the radix tree is composed as follows: + * + * [8 bytes expire unix time][8 bytes key object pointer] + * + * Such tree is stored in server.db[x]->expire. + * + * The first field, the unix time, is the same stored in the key->expire of + * the corresponding key in the hash table, however it is stored in big endian + * so that sorting the time lexicographically in the tree, will also make the + * tree sorted by numerical expire time (from the smallest unix time to the + * greatest one). + * + * Then we store the key pointer, this time in native endianess, because how + * it is sorted does not matter, being after the unix time. If Redis is running + * as a 32 bit system, the last 4 bytes of the pointer are just zeroes, so + * we can assume a 16 bytes key in every architecture. Note that from the + * pointer we can retrieve the key name, lookup it in the main dictionary, and + * delete the key. + * + * On the other hand, when we modify the expire time of some key, we need to + * update the tree accordingly. At every expire cycle, what we need to do is + * conceptually very simple: we run the tree and expire keys as long as we + * find keys that are already logically expired (expire time > current time). + * *----------------------------------------------------------------------------*/ -/* Helper function for the activeExpireCycle() function. +#define EXPIRE_KEY_LEN 16 /* Key length in the radix tree of expires. */ + +/* Populate the buffer 'buf', that should be at least EXPIRE_KEY_LEN bytes, + * with the key to store such key in the expires radix tree. See the comment + * above to see the format. */ +void encodeExpireKey(unsigned char *buf, rkey *key) { + uint64_t expire = htonu64(key->expire); + uint64_t ptr = (uint64_t) key; /* The pointer may be 32 bit, cast to 64. */ + memcpy(buf,&expire,sizeof(expire)); + memcpy(buf+8,&ptr,sizeof(ptr)); +} + +/* This is the reverse of encodeExpireKey(): given the key will return a + * pointer to an rkey and the expire value. */ +void decodeExpireKey(unsigned char *buf, uint64_t *expireptr, rkey **keyptrptr) { + uint64_t expire; + uint64_t keyptr; + memcpy(&expire,buf,sizeof(expire)); + expire = ntohu64(expire); + memcpy(&keyptr,buf+8,sizeof(keyptr)); + *expireptr = expire; + *keyptrptr = (rkey*)(unsigned long)keyptr; +} + +/* Populate the expires radix tree with the specified key. */ +void addExpireToTree(redisDb *db, rkey *key) { + unsigned char expirekey[EXPIRE_KEY_LEN]; + encodeExpireKey(expirekey,key); + int retval = raxTryInsert(db->expires,expirekey,EXPIRE_KEY_LEN,NULL,NULL); + serverAssert(retval != 0); +} + +/* Remove the specified key from the expires radix tree. */ +void removeExpireFromTree(redisDb *db, rkey *key) { + unsigned char expirekey[EXPIRE_KEY_LEN]; + encodeExpireKey(expirekey,key); + int retval = raxRemove(db->expires,expirekey,EXPIRE_KEY_LEN,NULL); + serverAssert(retval != 0); +} + +/* Delete a key that is found expired by the expiration cycle. We need to + * propagate the key too, send the notification event, and take a few + * stats. */ +void deleteExpiredKey(redisDb *db, rkey *key) { + robj *keyname = createStringObject(key->name,key->len); + + propagateExpire(db,keyname,server.lazyfree_lazy_expire); + if (server.lazyfree_lazy_expire) + dbAsyncDelete(db,keyname); + else + dbSyncDelete(db,keyname); + notifyKeyspaceEvent(NOTIFY_EXPIRED, + "expired",keyname,db->id); + decrRefCount(keyname); + server.stat_expiredkeys++; +} + +/* Helper function for the expireSlaveKeys() function. * This function will try to expire the key that is stored in the hash table * entry 'de' of the 'expires' hash table of a Redis database. * @@ -51,21 +145,10 @@ * * The parameter 'now' is the current time in milliseconds as is passed * to the function to avoid too many gettimeofday() syscalls. */ -int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) { - long long t = dictGetSignedIntegerVal(de); +int activeExpireCycleTryExpire(redisDb *db, rkey *key, long long now) { + long long t = key->expire; if (now > t) { - sds key = dictGetKey(de); - robj *keyobj = createStringObject(key,sdslen(key)); - - propagateExpire(db,keyobj,server.lazyfree_lazy_expire); - if (server.lazyfree_lazy_expire) - dbAsyncDelete(db,keyobj); - else - dbSyncDelete(db,keyobj); - notifyKeyspaceEvent(NOTIFY_EXPIRED, - "expired",keyobj,db->id); - decrRefCount(keyobj); - server.stat_expiredkeys++; + deleteExpiredKey(db,key); return 1; } else { return 0; @@ -101,7 +184,8 @@ void activeExpireCycle(int type) { static int timelimit_exit = 0; /* Time limit hit in previous call? */ static long long last_fast_cycle = 0; /* When last fast cycle ran. */ - int j, iteration = 0; + int j; + unsigned long iteration = 0; int dbs_per_call = CRON_DBS_PER_CALL; long long start = ustime(), timelimit, elapsed; @@ -129,25 +213,20 @@ void activeExpireCycle(int type) { if (dbs_per_call > server.dbnum || timelimit_exit) dbs_per_call = server.dbnum; - /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time - * per iteration. Since this function gets called with a frequency of - * server.hz times per second, the following is the max amount of - * microseconds we can spend in this function. */ + /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of + * CPU time per iteration. Since this function gets called with a + * frequency of server.hz times per second, the following is the max + * amount of microseconds we can spend in this function. */ timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; timelimit_exit = 0; if (timelimit <= 0) timelimit = 1; + /* If it's a fast cycle, override the time limit with our fixed + * time limit (defaults to 1 millisecond). */ if (type == ACTIVE_EXPIRE_CYCLE_FAST) timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */ - /* Accumulate some global stats as we expire keys, to have some idea - * about the number of keys that are already logically expired, but still - * existing inside the database. */ - long total_sampled = 0; - long total_expired = 0; - for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { - int expired; redisDb *db = server.db+(current_db % server.dbnum); /* Increment the DB now so we are sure if we run out of time @@ -155,92 +234,59 @@ void activeExpireCycle(int type) { * distribute the time evenly across DBs. */ current_db++; - /* Continue to expire if at the end of the cycle more than 25% - * of the keys were expired. */ - do { - unsigned long num, slots; - long long now, ttl_sum; - int ttl_samples; + /* If there is nothing to expire try next DB ASAP, avoiding the + * cost of seeking the radix tree iterator. */ + if (raxSize(db->expires) == 0) continue; + + /* The main collection cycle. Run the tree and expire keys that + * are found to be already logically expired. */ + long long now = mstime(); + raxIterator ri; + raxStart(&ri,db->expires); + raxSeek(&ri,"^",NULL,0); + + /* Enter the loop expiring keys for this database. Inside this + * loop there are two stop conditions: + * + * 1. The time limit. + * 2. The loop will exit if in this DB there are no more keys + * that are logically expired. + * + * Moreover the loop naturally terminates when there are no longer + * elements in the radix tree. */ + while(raxNext(&ri)) { + rkey *key; + uint64_t expire; + decodeExpireKey(ri.key,&expire,&key); + + /* First stop condition: no keys to expire here. */ + if (expire >= (uint64_t)now) break; + + printf("DEL %.*s -> %llu\n", (int)key->len, key->name, expire); + deleteExpiredKey(db,key); + + /* Second stop condition: the time limit. */ iteration++; - - /* If there is nothing to expire try next DB ASAP. */ - if ((num = dictSize(db->expires)) == 0) { - db->avg_ttl = 0; - break; - } - slots = dictSlots(db->expires); - now = mstime(); - - /* When there are less than 1% filled slots getting random - * keys is expensive, so stop here waiting for better times... - * The dictionary will be resized asap. */ - if (num && slots > DICT_HT_INITIAL_SIZE && - (num*100/slots < 1)) break; - - /* The main collection cycle. Sample random keys among keys - * with an expire set, checking for expired ones. */ - expired = 0; - ttl_sum = 0; - ttl_samples = 0; - - if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) - num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; - - while (num--) { - dictEntry *de; - long long ttl; - - if ((de = dictGetRandomKey(db->expires)) == NULL) break; - ttl = dictGetSignedIntegerVal(de)-now; - if (activeExpireCycleTryExpire(db,de,now)) expired++; - if (ttl > 0) { - /* We want the average TTL of keys yet not expired. */ - ttl_sum += ttl; - ttl_samples++; - } - total_sampled++; - } - total_expired += expired; - - /* Update the average TTL stats for this database. */ - if (ttl_samples) { - long long avg_ttl = ttl_sum/ttl_samples; - - /* Do a simple running average with a few samples. - * We just use the current estimate with a weight of 2% - * and the previous estimate with a weight of 98%. */ - if (db->avg_ttl == 0) db->avg_ttl = avg_ttl; - db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50); - } - - /* We can't block forever here even if there are many keys to - * expire. So after a given amount of milliseconds return to the - * caller waiting for the other active expire cycle. */ - if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */ - elapsed = ustime()-start; + if ((iteration & 0xff) == 0) { + now = ustime(); + elapsed = now-start; + now /= 1000; /* Convert back now to milliseconds. */ if (elapsed > timelimit) { timelimit_exit = 1; + printf("LIMIT (%llu) type:%d [elapsed=%llu]\n", timelimit, type, elapsed); server.stat_expired_time_cap_reached_count++; break; } } - /* We don't repeat the cycle if there are less than 25% of keys - * found expired in the current DB. */ - } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); + /* Reseek the iterator: the node we were on is now + * deleted. */ + raxSeek(&ri,"^",NULL,0); + } + raxStop(&ri); } elapsed = ustime()-start; latencyAddSampleIfNeeded("expire-cycle",elapsed/1000); - - /* Update our estimate of keys existing but yet to be expired. - * Running average with this sample accounting for 5%. */ - double current_perc; - if (total_sampled) { - current_perc = (double)total_expired/total_sampled; - } else - current_perc = 0; - server.stat_expired_stale_perc = (current_perc*0.05)+ - (server.stat_expired_stale_perc*0.95); } /*----------------------------------------------------------------------------- @@ -269,8 +315,8 @@ void activeExpireCycle(int type) { /* The dictionary where we remember key names and database ID of keys we may * want to expire from the slave. Since this function is not often used we * don't even care to initialize the database at startup. We'll do it once - * the feature is used the first time, that is, when rememberSlaveKeyWithExpire() - * is called. + * the feature is used the first time, that is, when the function + * rememberSlaveKeyWithExpire() is called. * * The dictionary has an SDS string representing the key as the hash table * key, while the value is a 64 bit unsigned integer with the bits corresponding @@ -300,11 +346,12 @@ void expireSlaveKeys(void) { while(dbids && dbid < server.dbnum) { if ((dbids & 1) != 0) { redisDb *db = server.db+dbid; - dictEntry *expire = dictFind(db->expires,keyname); + rkey *key = dictFetchValue(db->dict,keyname); + if (!(key->flags & KEY_FLAG_EXPIRE)) key = NULL; int expired = 0; - if (expire && - activeExpireCycleTryExpire(server.db+dbid,expire,start)) + if (key && + activeExpireCycleTryExpire(server.db+dbid,key,start)) { expired = 1; } @@ -313,7 +360,7 @@ void expireSlaveKeys(void) { * corresponding bit in the new bitmap we set as value. * At the end of the loop if the bitmap is zero, it means we * no longer need to keep track of this key. */ - if (expire && !expired) { + if (key && !expired) { noexpire++; new_dbids |= (uint64_t)1 << dbid; } @@ -341,13 +388,15 @@ void expireSlaveKeys(void) { /* Track keys that received an EXPIRE or similar command in the context * of a writable slave. */ -void rememberSlaveKeyWithExpire(redisDb *db, robj *key) { +void rememberSlaveKeyWithExpire(redisDb *db, rkey *key) { if (slaveKeysWithExpire == NULL) { static dictType dt = { - dictSdsHash, /* hash function */ + dictSdsHash, /* lookup hash function */ + dictSdsHash, /* stored hash function */ NULL, /* key dup */ NULL, /* val dup */ - dictSdsKeyCompare, /* key compare */ + dictSdsKeyCompare, /* loopkup key compare */ + dictSdsKeyCompare, /* stored key compare */ dictSdsDestructor, /* key destructor */ NULL /* val destructor */ }; @@ -355,13 +404,15 @@ void rememberSlaveKeyWithExpire(redisDb *db, robj *key) { } if (db->id > 63) return; - dictEntry *de = dictAddOrFind(slaveKeysWithExpire,key->ptr); - /* If the entry was just created, set it to a copy of the SDS string - * representing the key: we don't want to need to take those keys - * in sync with the main DB. The keys will be removed by expireSlaveKeys() - * as it scans to find keys to remove. */ - if (de->key == key->ptr) { - de->key = sdsdup(key->ptr); + sds skey = sdsnewlen(key->name,key->len); + dictEntry *de = dictAddOrFind(slaveKeysWithExpire,skey); + /* If the entry was already there, free the SDS string we used to lookup. + * Note that we don't care to take those keys in sync with the + * main DB. The keys will be removed by expireSlaveKeys() as it scans to + * find keys to remove. */ + if (de->key != skey) { + sdsfree(skey); + } else { dictSetUnsignedIntegerVal(de,0); } @@ -392,6 +443,137 @@ void flushSlaveKeysWithExpireList(void) { } /*----------------------------------------------------------------------------- + * Expires API + *----------------------------------------------------------------------------*/ + +/* Remove the expire from the key making it persistent. */ +int removeExpire(redisDb *db, rkey *key) { + if (!(key->flags & KEY_FLAG_EXPIRE)) return 0; + removeExpireFromTree(db,key); + key->flags &= ~KEY_FLAG_EXPIRE; + key->expire = 0; /* Not needed but better to leave the object clean. */ + return 1; +} + +/* Set an expire to the specified key. If the expire is set in the context + * of an user calling a command 'c' is the client, otherwise 'c' is set + * to NULL. The 'when' parameter is the absolute unix time in milliseconds + * after which the key will no longer be considered valid. */ +void setExpire(client *c, redisDb *db, rkey *key, long long when) { + /* Reuse the sds from the main dict in the expire dict */ + if (key->flags & KEY_FLAG_EXPIRE) removeExpireFromTree(db,key); + key->flags |= KEY_FLAG_EXPIRE; + key->expire = when; + addExpireToTree(db,key); + + int writable_slave = server.masterhost && server.repl_slave_ro == 0; + if (c && writable_slave && !(c->flags & CLIENT_MASTER)) + rememberSlaveKeyWithExpire(db,key); +} + +/* Return the expire time of the specified key, or -1 if no expire + * is associated with this key (i.e. the key is non volatile) */ +long long getExpire(rkey *key) { + return (key->flags & KEY_FLAG_EXPIRE) ? key->expire : -1; +} + +/* Propagate expires into slaves and the AOF file. + * When a key expires in the master, a DEL operation for this key is sent + * to all the slaves and the AOF file if enabled. + * + * This way the key expiry is centralized in one place, and since both + * AOF and the master->slave link guarantee operation ordering, everything + * will be consistent even if we allow write operations against expiring + * keys. */ +void propagateExpire(redisDb *db, robj *key, int lazy) { + robj *argv[2]; + + argv[0] = lazy ? shared.unlink : shared.del; + argv[1] = key; + incrRefCount(argv[0]); + incrRefCount(argv[1]); + + if (server.aof_state != AOF_OFF) + feedAppendOnlyFile(server.delCommand,db->id,argv,2); + replicationFeedSlaves(server.slaves,db->id,argv,2); + + decrRefCount(argv[0]); + decrRefCount(argv[1]); +} + +/* Check if the key is expired. */ +int keyIsExpired(rkey *key) { + mstime_t when = getExpire(key); + + if (when < 0) return 0; /* No expire for this key */ + + /* Don't expire anything while loading. It will be done later. */ + if (server.loading) return 0; + + /* If we are in the context of a Lua script, we pretend that time is + * blocked to when the Lua script started. This way a key can expire + * only the first time it is accessed and not in the middle of the + * script execution, making propagation to slaves / AOF consistent. + * See issue #1525 on Github for more information. */ + mstime_t now = server.lua_caller ? server.lua_time_start : mstime(); + + return now > when; +} + +/* This function is called when we are going to perform some operation + * in a given key, but such key may be already logically expired even if + * it still exists in the database. The main way this function is called + * is via lookupKey*() family of functions. + * + * The behavior of the function depends on the replication role of the + * instance, because slave instances do not expire keys, they wait + * for DELs from the master for consistency matters. However even + * slaves will try to have a coherent return value for the function, + * so that read commands executed in the slave side will be able to + * behave like if the key is expired even if still present (because the + * master has yet to propagate the DEL). + * + * In masters as a side effect of finding a key which is expired, such + * key will be evicted from the database. Also this may trigger the + * propagation of a DEL/UNLINK command in AOF / replication stream. + * + * The return value of the function is 0 if the key is still valid, + * otherwise the function returns 1 if the key is expired. */ +int expireIfNeeded(redisDb *db, robj *keyname, rkey *key) { + if (!keyIsExpired(key)) return 0; + + /* If we are running in the context of a slave, instead of + * evicting the expired key from the database, we return ASAP: + * the slave key expiration is controlled by the master that will + * send us synthesized DEL operations for expired keys. + * + * Still we try to return the right information to the caller, + * that is, 0 if we think the key should be still valid, 1 if + * we think the key is expired at this time. */ + if (server.masterhost != NULL) return 1; + + /* Delete the key */ + server.stat_expiredkeys++; + propagateExpire(db,keyname,server.lazyfree_lazy_expire); + notifyKeyspaceEvent(NOTIFY_EXPIRED, + "expired",keyname,db->id); + return server.lazyfree_lazy_expire ? dbAsyncDelete(db,keyname) : + dbSyncDelete(db,keyname); +} + +/* Sometimes we have just the name of the key, because we have still to + * lookup it. In such cases this function is more handy compared to + * expireIfNeeded(): just a wrapper performing the lookup first. */ +int expireIfNeededByName(redisDb *db, robj *keyname) { + rkey *key; + robj *val = lookupKey(db,keyname,&key,LOOKUP_NOTOUCH); + if (!val) return 0; + return expireIfNeeded(db,keyname,key); +} + + + +/*----------------------------------------------------------------------------- * Expires Commands *----------------------------------------------------------------------------*/ @@ -403,7 +585,8 @@ void flushSlaveKeysWithExpireList(void) { * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for * the argv[2] parameter. The basetime is always specified in milliseconds. */ void expireGenericCommand(client *c, long long basetime, int unit) { - robj *key = c->argv[1], *param = c->argv[2]; + robj *keyname = c->argv[1], *param = c->argv[2]; + rkey *key; long long when; /* unix time in milliseconds when the key will expire. */ if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK) @@ -413,7 +596,7 @@ void expireGenericCommand(client *c, long long basetime, int unit) { when += basetime; /* No key, return zero. */ - if (lookupKeyWrite(c->db,key) == NULL) { + if (lookupKeyWrite(c->db,keyname,&key) == NULL) { addReply(c,shared.czero); return; } @@ -427,23 +610,24 @@ void expireGenericCommand(client *c, long long basetime, int unit) { if (when <= mstime() && !server.loading && !server.masterhost) { robj *aux; - int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) : - dbSyncDelete(c->db,key); - serverAssertWithInfo(c,key,deleted); + int deleted = server.lazyfree_lazy_expire ? + dbAsyncDelete(c->db,keyname) : + dbSyncDelete(c->db,keyname); + serverAssertWithInfo(c,keyname,deleted); server.dirty++; /* Replicate/AOF this as an explicit DEL or UNLINK. */ aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del; - rewriteClientCommandVector(c,2,aux,key); - signalModifiedKey(c->db,key); - notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id); + rewriteClientCommandVector(c,2,aux,keyname); + signalModifiedKey(c->db,keyname); + notifyKeyspaceEvent(NOTIFY_GENERIC,"del",keyname,c->db->id); addReply(c, shared.cone); return; } else { setExpire(c,c->db,key,when); addReply(c,shared.cone); - signalModifiedKey(c->db,key); - notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id); + signalModifiedKey(c->db,keyname); + notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",keyname,c->db->id); server.dirty++; return; } @@ -472,15 +656,16 @@ void pexpireatCommand(client *c) { /* Implements TTL and PTTL */ void ttlGenericCommand(client *c, int output_ms) { long long expire, ttl = -1; + rkey *key; /* If the key does not exist at all, return -2 */ - if (lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH) == NULL) { + if (lookupKeyReadWithFlags(c->db,c->argv[1],&key,LOOKUP_NOTOUCH) == NULL) { addReplyLongLong(c,-2); return; } /* The key exists. Return -1 if it has no expire, or the actual * TTL value otherwise. */ - expire = getExpire(c->db,c->argv[1]); + expire = getExpire(key); if (expire != -1) { ttl = expire-mstime(); if (ttl < 0) ttl = 0; @@ -504,8 +689,9 @@ void pttlCommand(client *c) { /* PERSIST key */ void persistCommand(client *c) { - if (lookupKeyWrite(c->db,c->argv[1])) { - if (removeExpire(c->db,c->argv[1])) { + rkey *key; + if (lookupKeyWrite(c->db,c->argv[1],&key)) { + if (removeExpire(c->db,key)) { addReply(c,shared.cone); server.dirty++; } else { @@ -520,7 +706,7 @@ void persistCommand(client *c) { void touchCommand(client *c) { int touched = 0; for (int j = 1; j < c->argc; j++) - if (lookupKeyRead(c->db,c->argv[j]) != NULL) touched++; + if (lookupKeyRead(c->db,c->argv[j],NULL) != NULL) touched++; addReplyLongLong(c,touched); } |