Redis 是什么?
Redis 是一个开源的内存数据结构存储器,可以被用做数据库、缓存、消息队列和流处理引擎。Redis 提供了字符串、哈希表、列表、集合、支持范围查询的有序集合、位图、地理信息索引等数据结构。内建支持主从复制、Lua 脚本、LRU 淘汰机制、事务和多种级别的硬盘持久化能力。 —— 摘抄并翻译自 Redis 官网
简单的说,Redis 是一款内存数据库,其管理的所有数据都存储在内存中。Redis 实现了很多内存数据结构以便用户能够在 Redis 上方便的构建各种业务逻辑。
在 Redis 中,最基础的数据结构莫过于字符串(strings)了,在 redis-cli 中,可以这样操作 strings 结构:
> SET mykey somevalue
OK
> GET mykey
"somevalue"
上述命令中,我们使用 SET 命令将 mykey → somevalue 这个键值对存入了 Redis,并使用 GET 命令查询到了 mykey 所对应的值。这也是 Redis 最基础的功能。
从 SET 命令开始探索
SET 命令的用法非常简单,那我们就从 SET 命令一路看下去,看看 SET 命令是如何实现的。
// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/t_string.c#L284-L297
/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
void setCommand(client *c) {
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;
// 顾名思义,解析诸如 NX XX EX PX 等各种参数
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
return;
}
// 进行编码转换
c->argv[2] = tryObjectEncoding(c->argv[2]);
// SET 命令主要逻辑的实现
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
setCommand
函数依次调用了 parseExtendedStringArgumentsOrReply
、tryObjectEncoding
、setGenericCommand
这三个函数。
其中 parseExtendedStringArgumentsOrReply
函数顾名思义,是用于解析 SET 命令的各种参数,如 NX XX 等,这不是 SET 命令的主要逻辑,不深究,继续往下看。
从命名看 tryObjectEncoding
函数似乎是对 c->argv[2] 进行编码转换,显然这也不是主要的逻辑。
至此,只剩下 setGenericCommand
,从命名上看得出 setGenericCommand
应该是主要的函数逻辑,我们跳转到该函数:
// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/t_string.c#L78-L141
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
int found = 0;
int setkey_flags = 0;
if (expire && ...) {
/* 隐去和过期时间相关的处理逻辑 */
}
if (flags & OBJ_SET_GET) {
/* 隐去和 flags 相关的处理逻辑 */
}
found = (lookupKeyWrite(c->db,key) != NULL);
if ((flags & OBJ_SET_NX && found) ||
(flags & OBJ_SET_XX && !found))
{
/* 隐去和 flags 相关的处理逻辑 */
}
// set 的主要逻辑
setKey(c,c->db,key,val,setkey_flags);
server.dirty++;
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
if (expire) {
/* 隐去和过期时间相关的处理逻辑 */
}
if (!(flags & OBJ_SET_GET)) {
/* 隐去和 flags 相关的处理逻辑 */
}
}
隐去一些周边的处理逻辑后,我们能够明显地看到 setKey 是这个函数的主要逻辑,setKey 是将键值对存储在内存中的主要过程,看 setKey:
// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/db.c#L261-L277
void setKey(client *c, redisDb *db, robj *key, robj *val, int flags) {
int keyfound = 0;
// 判断传入的 key 是否已经存在
if (flags & SETKEY_ALREADY_EXIST)
keyfound = 1;
else if (!(flags & SETKEY_DOESNT_EXIST))
keyfound = (lookupKeyWrite(db,key) != NULL);
if (!keyfound) {
// 如果 key 尚未存在,则调用 dbAdd 方法
dbAdd(db,key,val);
} else {
// 如果 key 已经存在,则调用 dbOverwrtie 方法
dbOverwrite(db,key,val);
}
// 使引用计数器自增
incrRefCount(val);
// 处理 key 过期相关问题
if (!(flags & SETKEY_KEEPTTL)) removeExpire(db,key);
if (!(flags & SETKEY_NO_SIGNAL)) signalModifiedKey(c,db,key);
}
在上面的函数中,我们可以看到主要逻辑是先判断 key 是否存在,根据 key 是否存在去调用 dbAdd
函数或 dbOverwrite
函数。这是 set 一个 key 的主要流程,后面对引用计数器的操作和对过期相关问题的处理,暂时忽略。
// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/db.c#L188-L196
void dbAdd(redisDb *db, robj *key, robj *val) {
sds copy = sdsdup(key->ptr);
// 向 dist 中添加这个 key
dictEntry *de = dictAddRaw(db->dict, copy, NULL);
serverAssertWithInfo(NULL, key, de != NULL);
// 向 dist 中增加对应 key 的 value
dictSetVal(db->dict, de, val);
// 告知客户端这个 key 的相关处理工作已经完成
signalKeyAsReady(db, key, val->type);
// cluster 相关,忽略
if (server.cluster_enabled) slotToKeyAddEntry(de, db);
// notify 相关,忽略
notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
}
dbAdd
这个函数主要调用了 dictAddRaw
和 dictSetVal
,将相关的键值对存入 dict 中。看得出 dict 是字典 dictionary 的缩写,是 Redis 中存储键值对的数据结构。
总结
目前,我们一共看到了三个文件中的代码,根据其调用链,他们有这样的层次关系:t_string.c → db.c → dict.c。
不难发现,t_string.c 这个文件是用于处理 strings 这个数据结构相关的逻辑;db.c 是 Redis 主要的管理键值对的逻辑,对 key 的生命周期的管理主要存在于这个文件中;dict.c 是 Redis 中 HashTable 这个数据结构的实现。