阅读 543

当你SET的时候,Redis到底在SET些什么

准备过互联网公司的服务端岗位面试的人,对Redis中的5种数据类型想必是如数家珍。而网上很多面试题里也会出现这道题目

来自https://blog.csdn.net/ThinkWon/article/details/103522351

来自https://juejin.cn/post/6844903982066827277

来自https://mikechen.cc/3313.html

随着行业曲率的增大,光是知道有这些数据类型已经不够了,还得知道同一个类型也有不同的底层数据结构。例如同样是string类型,不同内容或不同长度会采用不同的编码方式:

127.0.0.1:6379> SET key1 "1"
OK
127.0.0.1:6379> SET key2 "value"
OK
127.0.0.1:6379> SET key3 "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp."
OK
127.0.0.1:6379> TYPE key1
string
127.0.0.1:6379> TYPE key2
string
127.0.0.1:6379> TYPE key3
string
127.0.0.1:6379> OBJECT ENCODING key1
"int"
127.0.0.1:6379> OBJECT ENCODING key2
"embstr"
127.0.0.1:6379> OBJECT ENCODING key3
"raw"
复制代码

hash类型也有两种底层实现

127.0.0.1:6379>  HSET myhash field1 "Hello"
(integer) 1
127.0.0.1:6379>  HSET myhash2 field1 "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp."
(integer) 1
127.0.0.1:6379> OBJECT ENCODING myhash
"ziplist"
127.0.0.1:6379> OBJECT ENCODING myhash2
"hashtable"
复制代码

不知道你是否曾经好奇过,上文中的key1key2key3myhash,以及myhash2这些键,与它们各自的值(前三个为string,后两个为hash)之间的关系又是存储在什么数据结构中的呢?

答案在意料之外,情理之中:键与值的关系,也是存储在一张哈希表中的,并且正是上文中的hashtable

求证的办法当然是阅读Redis的源代码。

Redis命令的派发逻辑

阅读Redis的源码是比较轻松愉快的,一是因为其源码由简单易懂的C语言编写,二是因为源码仓库的README.md中对内部实现做了一番高屋建瓴的介绍。在README.mdserver.c一节中,道出了有关命令派发的两个关键点

call() is used in order to call a given command in the context of a given client.

The global variable redisCommandTable defines all the Redis commands, specifying the name of the command, the function implementing the command, the number of arguments required, and other properties of each command.

位于文件src/server.c中的变量redisCommandTable定义了所有可以在Redis中使用的命令——为什么一个C语言项目里要用camelCase这种格格不入的命名风格呢——它的元素的类型为struct redisCommand,其中:

  • name存放命令的名字;
  • proc存放实现命令的C函数的指针;

比如高频使用的GET命令在redisCommandTable中就是这样定义的

    {"get",getCommand,2,
     "read-only fast @string",
     0,NULL,1,1,1,0,0,0},
复制代码

身为一名老解释器爱好者,对这种套路的代码当然是不会陌生的。我也曾在写过的、跑不起来的玩具解释器上用过类似的手法

Redis收到一道需要执行的命令后,根据命令的名字用lookupCommand找到一个命令(是个struct redisCommand类型的结构体),然后call函数做的事情就是调用它的proc成员所指向的函数而已

    c->cmd->proc(c);
复制代码

那么接下来,就要看看SET命令对应的C函数究竟做了些什么了。

SET命令的实现

redisCommonTable中下标为2的元素正是SET命令的定义

    /* Note that we can't flag set as fast, since it may perform an
     * implicit DEL of a large key. */
    {"set",setCommand,-3,
     "write use-memory @string",
     0,NULL,1,1,1,0,0,0},
复制代码

其中函数setCommand定义在文件t_string.c中,它根据参数中是否有传入NXXXEX等选项计算出一个flags后,便调用setGenericCommand——顾名思义,这是一个通用的SET命令,它同时被SETSETNXSETEX,以及PSETEX四个Redis命令的实现函数所共用。

setGenericCommand调用了genericSetKey,后者定义在文件db.c中。尽管该函数上方的注释写着

All the new keys in the database should be created via this interface.

人生不如意事十之八九事实并非如此。例如在命令RPUSH的实现函数rpushCommand中,调用了pushGenericCommand,后者直接调用了dbAdd往Redis中存入键和列表对象的关系。

言归正传。根据键存在与否,genericSetKey会调用dbAdddbOverwrite。而在dbAdd中,最终调用了dictAdd将键与值存入数据库中。

/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}
复制代码

现在我们知道了,使用SET命令时传入的keyvalue,是存储在一个dict类型的数据结构中。

HSET命令的实现

依葫芦画瓢,Redis的HSET命令由位于文件t_hash.c中的函数hsetCommand实现,它会尝试转换要操作的hash值的编码方式。

    hashTypeTryConversion(o,c->argv,2,c->argc-1);
复制代码

如果hashTypeTryConversion发现要写入哈希表的任何一个键或者值的长度超过了server.hash_max_ziplist_value所规定的值,就会将hash类型的编码从ziplist转换为hashtableserver.hash_max_ziplist_value的值在文件config.c中通过宏设置,默认值为64——这正是上文中myhash2所对应的值的编码为hashtable的原因。

将思绪拉回到函数hsetCommand中。做完编码的转换后,它调用函数hashTypeSet,在编码为hashtable的世界线中,同样调用了dictAdd实现往哈希表中写入键值对。

殊途同归

结论

因此,在Redis中用以维持每一个键与其对应的值——这些值也许是string,也许是list,也许是hash——的关系的数据结构,与Redis中的一系列操作哈希表的命令——也许是HSET、也许HGET,也许是HDEL——所用的数据结构,不能说是毫不相关,起码是一模一样。

阅读原文

文章分类
后端
文章标签