baiyan
命令语法
命令含义:判断键是否存在。如果过期则不存在,不过期则存在
命令格式:
EXISTS key1 key2 ... keyN
命令实战:
127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> exists key1
(integer) 1
返回值:存在键的数量
源码分析
del 命令对应的处理函数是 existsCommand():
void existsCommand(client *c) {
long long count = 0;
int j;
for (j = 1; j < c->argc; j++) {
// 去键空间字典寻找给定 key 是否存在,存在则 count++
if (lookupKeyRead(c->db,c->argv[j])) count++;
}
// 返回找到 key 的数量
addReplyLongLong(c,count);
}
同样地,我们使用 gdb - p 来观察 exists 命令的执行过程,gdb 的过程这里不再赘述。我们在 existsCommand 处打一个断点,然后在客户端中执行命令:
127.0.0.1:6379> exists key1
观察服务端,已经执行到我们的断点处了:
Breakpoint 1, existsCommand (c=0x7fb459ca3540) at db.c:496
496 void existsCommand(client *c) {(gdb) n
500 for (j = 1; j < c->argc; j++) {(gdb)
497 long long count = 0;
(gdb)
501 if (lookupKeyRead(c->db,c->argv[j])) count++;
(gdb) s
我们看到,在入口函数 existsCommand()中,遍历了所有的命令参数,即我们传入的 key1,这里会调用 lookupKeyRead()函数,去键空间中查找键是否过期,如果过期,则返回 0,不存在。反之键存在,返回 1。跟进这个函数调用:
lookupKeyRead (key=0x7fb462229ad0, db=0x7fb46221a800) at db.c:144
144 return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
(gdb) s
lookupKeyReadWithFlags (db=0x7fb46221a800, key=0x7fb462229ad0, flags=flags@entry=0) at db.c:100
100 robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {(gdb) n
103 if (expireIfNeeded(db,key) == 1) {(gdb) n
133 val = lookupKey(db,key,flags);
(gdb) n
这个函数直接调用了 lookupKeyReadWithFlags(),然后在 lookupKeyReadWithFlags 函数内部,调用了我们熟悉的 expireIfNeeded(),显然在我们调试过程中,并没有进这个 if,因为我们的键并没有过期,所以肯定返回 0。由于键并没有过期,那么在这里应该直接返回了。但是由于它是一个通用查找函数,还需要返回查找后的值,所以继续调用 lookupKey()函数,去键空间中查找 key1 对应的值 value1。继续往下执行:
133 val = lookupKey(db,key,flags);
(gdb) n
134 if (val == NULL)
(gdb) p val
$1 = (robj *) 0x7fb46220e100
(gdb) p *val
$2 = {type = 0, encoding = 8, lru = 8745377, refcount = 1, ptr = 0x7fb46220e113}
137 server.stat_keyspace_hits++;
(gdb)
139 }
(gdb)
existsCommand (c=0x7fb459ca3540) at db.c:501
501 if (lookupKeyRead(c->db,c->argv[j])) count++;
我们看到,在查找完成之后,会判断是否为 NULL,显然这里不会为 NULL。打印 val 的值,是一个 redisObj 结构,这里 ptr 指向的 sds 就是我们的 value1 了。然后,redis 会统计一个数据,是在键空间中查找到 value 的次数,这里由于我们找到了,将其 ++,然后函数会将查找到的 value1 返回,最外层判断不为 NULL,count++,命令执行结束。
扩展
字典的查找
在 exists 命令执行过程中,一个核心的函数调用就是 lookupKey()。这个函数查找一个键对应的 value 值。如果存在,返回该 value 值,否则返回 NULL:
robj *lookupKey(redisDb *db, robj *key, int flags) {dictEntry *de = dictFind(db->dict,key->ptr); // 找到当前 key-value 对所在的 dictEntry
if (de) {robj *val = dictGetVal(de); // 是一个宏,直接返回 dictEntry 中的 val 字段
...
return val;
} else {return NULL;}
}
这个函数会首先调用 dictFind(),直接返回这个 key 所在 dict 中的 dictEntry。我们跟进这个函数:
static dictEntry *dictFind(dict *ht, const void *key) {
dictEntry *he; // dictEntry 结构的指针
unsigned int h; // 存储哈希值
if (ht->size == 0) return NULL; // 字典为空,直接返回不存在
h = dictHashKey(ht, key) & ht->sizemask; // 计算哈希值
he = ht->table[h]; // 访问字典对应哈希值位置上的元素,它是一个指针,指向 dictEntry 结构
// 遍历链地址法解决冲突的链表
while(he) {if (dictCompareHashKeys(ht, key, he->key)) // 挨个比较当前 dictEntry 的 key 值是否等于我们要找的 key 值
return he; // 找到了,直接返回
he = he->next; // 没找到,继续往后遍历链表
}
return NULL; // 遍历到链表尾部还没有找到,说明没有该元素
}
在说明该函数查找流程之前,我们回顾一下字典的存储结构:
代码中的 he = ht->table[h]访问的就是我们 **table 这个指针。它是一个二级指针,可以看成一个一维数组,每个数组中的元素都是一个 dictEntry 的指针。我们访问到了第 h 个(计算后的哈希值)元素的 bucket 位置上,它也是一个 dictEntry 指针,指向真正存储 key-value 对的结构。由于需要解决哈希冲突问题,所以一个 bucket 后面会以链地址法,通过 next 指针字段,挂接多个 dictEntry,这就是上面代码为什么需要遍历的原因。每遍历一个 dictEntry,我们都要比较一下当前 dictEntry 上的 key 值是否与我们要比较的 key 值是否相等,如果相等就找到了我们要找的 key-value 对。反之如果遍历到链表尾部都没找到,那就说明没有这个 key-value 对了:
我们回顾一下 dictEntry 的结构:
typedef struct dictEntry {
void *key; // 指向存储 key 值的结构
union {
void *val; // 指向存储 value 值的结构
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 链地址法解决冲突的指针,指向下一个 dictEntry 结构
} dictEntry;
在找完之后,该函数会返回一个 dictEntry 结构的指针,我们调用 dictGetVal 宏,就能访问到 dictEntry 中的 value 值啦:
#define dictGetVal(he) ((he)->v.val)
我们看到,这个宏访问了 dictEntry 的 val 字段,成功拿到了当前 key 对应的 value 值。
参考资料
【Redis5 源码学习】2019-04-19 字典 dict