准备过互联网公司的服务端岗位面试的人,对Redis中的5种数据类型想必是如数家珍。而网上很多面试题里也会出现这道题目
随着行业曲率的增大,光是知道有这些数据类型已经不够了,还得知道同一个类型也有不同的底层数据结构。例如同样是string
类型,不同内容或不同长度会采用不同的编码方式:
1 | 127.0.0.1:6379> SET key1 "1" |
而hash
类型也有两种底层实现
1 | 127.0.0.1:6379> HSET myhash field1 "Hello" |
不知道你是否曾经好奇过,上文中的key1
、key2
、key3
、myhash
,以及myhash2
这些键,与它们各自的值(前三个为string
,后两个为hash
)之间的关系又是存储在什么数据结构中的呢?
答案在意料之外,情理之中:键与值的关系,也是存储在一张哈希表中的,并且正是上文中的hashtable
。
求证的办法当然是阅读Redis的源代码。
Redis命令的派发逻辑
阅读Redis的源码是比较轻松愉快的,一是因为其源码由简单易懂的C语言编写,二是因为源码仓库的README.md
中对内部实现做了一番高屋建瓴的介绍。在README.md
的server.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
中就是这样定义的
1 | {"get",getCommand,2, |
身为一名老解释器爱好者,对这种套路的代码当然是不会陌生的。我也曾在写过的、跑不起来的玩具解释器上用过类似的手法
Redis收到一道需要执行的命令后,根据命令的名字用lookupCommand
找到一个命令(是个struct redisCommand
类型的结构体),然后call
函数做的事情就是调用它的proc
成员所指向的函数而已
1 | c->cmd->proc(c); |
那么接下来,就要看看SET
命令对应的C函数究竟做了些什么了。
SET
命令的实现
redisCommonTable
中下标为2的元素正是SET
命令的定义
1 | /* Note that we can't flag set as fast, since it may perform an |
其中函数setCommand
定义在文件t_string.c
中,它根据参数中是否有传入NX
、XX
、EX
等选项计算出一个flags
后,便调用setGenericCommand
——顾名思义,这是一个通用的SET
命令,它同时被SET
、SETNX
、SETEX
,以及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
会调用dbAdd
或dbOverwrite
。而在dbAdd
中,最终调用了dictAdd
将键与值存入数据库中。
1 | /* Add an element to the target hash table */ |
现在我们知道了,使用SET
命令时传入的key
和value
,是存储在一个dict
类型的数据结构中。
HSET
命令的实现
依葫芦画瓢,Redis的HSET
命令由位于文件t_hash.c
中的函数hsetCommand
实现,它会尝试转换要操作的hash
值的编码方式。
1 | hashTypeTryConversion(o,c->argv,2,c->argc-1); |
如果hashTypeTryConversion
发现要写入哈希表的任何一个键或者值的长度超过了server.hash_max_ziplist_value
所规定的值,就会将hash
类型的编码从ziplist
转换为hashtable
。server.hash_max_ziplist_value
的值在文件config.c
中通过宏设置,默认值为64——这正是上文中myhash2
所对应的值的编码为hashtable
的原因。
将思绪拉回到函数hsetCommand
中。做完编码的转换后,它调用函数hashTypeSet
,在编码为hashtable
的世界线中,同样调用了dictAdd
实现往哈希表中写入键值对。
殊途同归
结论
因此,在Redis中用以维持每一个键与其对应的值——这些值也许是string
,也许是list
,也许是hash
——的关系的数据结构,与Redis中的一系列操作哈希表的命令——也许是HSET
、也许HGET
,也许是HDEL
——所用的数据结构,不能说是毫不相关,起码是一模一样。