Redis源码剖析 - Redis之数据库redisDb
Posted Fred^_^
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码剖析 - Redis之数据库redisDb相关的知识,希望对你有一定的参考价值。
原创作品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/51321282
Redis源码剖析系列文章汇总:传送门
今天,我们来讨论两点内容:一是Redis是如何存储类型对象的,二是Redis如何实现键的过期操作。
本文介绍的内容主要涉及db.c和redis.h两个文件。
1、redisDb介绍
Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义。我们知道Redis提供string、list、set、zset、hash五种数据类型的存储,在Redis运行时,服务器中会存在许多的不同类型的对象,当我们需要操作某个具体的对象时,首先需要快速定位到该对象。比如往一个list中插入一个元素,第一步先要在众多对象实例中找到该list,然后再进行插入操作。如何快速获取指定对象呢?在我们前面介绍过的基础数据结构中字典dict就可以实现该功能。具体的做法是:Redis每生成一个对象实例都需要关联一个key,利用dict保存key和对象实例之间的映射关系。这样就可以在O(1)的时间复杂度下根据key找到对应的对象,而Redis中的“数据库”redisDb就是对上述过程的实现。下面我们来看看该结构体的定义。
1.1、redisDb的存储结构
redisDb结构体的定义如下:
/* Redis数据库结构体 */
typedef struct redisDb
// 数据库键空间,存放着所有的键值对(键为key,值为相应的类型对象)
dict *dict;
// 键的过期时间
dict *expires;
// 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)
dict *blocking_keys;
// 准备好数据可以解除阻塞状态的键和相应的client
dict *ready_keys;
// 被watch命令监控的key和相应client
dict *watched_keys;
// 数据库ID标识
int id;
// 数据库内所有键的平均TTL(生存时间)
long long avg_ttl;
redisDb;
我们看到:
(1)、redisDb中的dict *dict
成员就是将key和具体的对象(可能是string、list、set、zset、hash中任意类型之一)关联起来,存储着该数据库中所有的键值对数据。该字段又称为键空间key space。
(2)、expires
成员用来存放key的过期时间。
(3)、id
成员是数据库的编号,以整型表示。
接下来我们主要基于上面3个redisDb的成员来展开讲解。其余几个成员在前面我们已经介绍过,大家可以参看前面的几篇博客,这里给出给相关文章链接:
- blocking_keys、ready_keys: 【Redis源码剖析】 - Redis数据类型之列表List
- watched_keys:【Redis源码剖析】 - Redis之事务的实现原理
1.2、数据库的切换操作
当redis 服务器初始化时,会预先分配 16 个数据库,该数字配置在redis.conf配置文件中,如下:
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16
所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中,而redisClient中存在一个名叫db的指针指向当前使用的数据库(默认为0号数据库)。
typedef struct redisClient
...
// 当前所使用的数据库
redisDb *db;
...
redisClient;
Redis提供了select 命令用于切换到指定的数据库,该命令的具体格式为:
SELECT db_index
其中数据库索引号db_ index 用数字值指定,以 0 作为起始索引值。该命令的实现也很简单,当我们选择数据库 select number 时,程序直接通过 redisServer.db[number] 来切换数据库,源码如下:
/* SELECT命令,切换到指定数据库。*/
void selectCommand(redisClient *c)
long id;
// 取得目标数据库id,如果输入值不合法则返回
if (getLongFromObjectOrReply(c, c->argv[1], &id,
"invalid DB index") != REDIS_OK)
return;
// 切换到指定数据库
if (selectDb(c,id) == REDIS_ERR)
addReplyError(c,"invalid DB index");
else
addReply(c,shared.ok);
由selectDb函数执行真正的切换操作:
/* 切换为参数id指定的数据库,如果操作成功返回REDIS_OK,否则返回REDIS_ERR。 */
int selectDb(redisClient *c, int id)
// 验证参数id是否正确,server.dbnum默认值为16
if (id < 0 || id >= server.dbnum)
return REDIS_ERR;
// 切换数据库,就是简单地设置指针
c->db = &server.db[id];
return REDIS_OK;
2、redisDb中的键空间
在【Redis源码剖析】系列文章中,我们前面花了很多篇幅介绍了Redis中内置数据结构和数据类型的实现原理,这里我们就要把它们综合起来,看看如何利用这些数据结构组成一个初步的key-value系统。
2.1、键空间的存储结构
键空间实际就是一个字典dict结构,存储着该库所有的键值对数据,其中字典dict的key是一个字符串对象,字典dict的值可能是string、list、set、zset、hash中任意类型之一的对象实例。弄明白键空间的底层结构,我们不难画出其存储结构:
2.2、键空间的相关操作
Redis提供了许多与key相关的操作命令,这些命令我们先前没有涉及到,接下来简单介绍一下:
命令 | 作用 |
---|---|
del key | 删除key |
exists key | 检查key是否存在 |
randomkey | 从当前数据库中随机返回一个key |
keys pattern | 查找所有符合给定模式的key |
scan cursor | 迭代当前数据库中的key |
dbsize | 返回当前数据库中key的数量 |
type key | 返回key所存储的值的类型 |
rename key newkey | 修改key的名称 |
renamenx key newkey | 当newkey不存在时,修改key的名称为newkey |
move key db | 将当前数据库中的key移动到给定的数据库中 |
由于键空间是一个字典dict结构,所以对键空间的操作基本上就是对字典dict的操作,主要包含下面这些函数:
// 从Redis数据库db中取出指定key的对象
robj *lookupKey(redisDb *db, robj *key);
// 为读操作而从数据库db中取出指定key的对象
robj *lookupKeyRead(redisDb *db, robj *key);
// 为写操作而从数据库db中取出指定key的对象
robj *lookupKeyWrite(redisDb *db, robj *key);
// 带有回复功能的lookupKeyRead函数
robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply);
// 带有回复功能的lookupKeyWrite函数
robj *lookupKeyWriteOrReply(redisClient *c, robj *key, robj *reply);
// 往数据库db中添加一个由参数key和参数val指定的键值对
void dbAdd(redisDb *db, robj *key, robj *val);
// 重写一个key关联的值,即为一个存在的key设置一个新值
void dbOverwrite(redisDb *db, robj *key, robj *val);
// 为一个key设置新值
void setKey(redisDb *db, robj *key, robj *val);
// 判断某个key是否存在
int dbExists(redisDb *db, robj *key);
// 随机从数据库db中取出一个key并返回
robj *dbRandomKey(redisDb *db);
// 从数据库中删除一个给定的key、相应的值value以及该key的过期时间
int dbDelete(redisDb *db, robj *key);
这些操作并没有什么难点,大家可以参看后文提供的加以注释的源码进行学习,这里就不一一解释每个函数的实现。
3、键的过期操作
Redis支持为给定的key设置过期时间,并提供以下命令实现该功能:
命令 | 作用 |
---|---|
expire key seconds | 以秒为单位为给定key设置过期时间 |
expireat key timestamp | 以秒为单位为给定key设置过期时间戳 |
pexpire key milliseconds | 以毫秒为单位设置key的过期时间 |
pexpireat key milliseconds | 以毫秒为单位设置key过期时间戳 |
persist key | 移除key的过期时间,key将持久保存 |
pttl key | 以毫秒为单位返回key的剩余过期时间 |
ttl key | 以秒为单位返回key的剩余生存时间 |
接下来,我们就来介绍一下Redis如果保存key的过期信息和如何删除过期key的。
3.1、底层结构
前面我们讲过,key的过期时间保存在redisDb结构中的expires字段中:
typedef struct redisDb
...
// 键的过期时间
dict *expires;
...
redisDb;
expires字段也是一个字典dict结构,字典的键为key,值为该key对应的过期时间,过期时间为long long类型整数,是以毫秒为单位的过期 UNIX 时间戳。我们可以看看setExpire函数加以佐证,该函数的作用是为指定key设置过期时间。
/* 为指定key设置过期时间 */
void setExpire(redisDb *db, robj *key, long long when)
dictEntry *kde, *de;
/* Reuse the sds from the main dict in the expire dict */
// db->dict和db->expires是共用key字符串对象的
// 取出key
kde = dictFind(db->dict,key->ptr);
redisAssertWithInfo(NULL,key,kde != NULL);
// 取出过期时间
de = dictReplaceRaw(db->expires,dictGetKey(kde));
// 重置key的过期时间
dictSetSignedIntegerVal(de,when);
有了过期时间戳我们就很容易判断某个key是否过期:只要将当前时间戳跟过期时间戳比较一下即可,如果当前时间戳大于过期时间戳显然该key已经过期了。在Redis中,如果没有为一个key设置过期时间,那么该key就不会出现在db->expires字典中。也就是说db->expires字段只保存了设置有过期时间的key。
有一点需要提出来的是:redisDb中的db->dict和db->expires两个字典是共享同一个key的,即它们都指向了同一个key字符串,而不是将同一个key复制两份。这点利用指针很容易实现。
3.2、相关操作
3.2.1、设置过期时间
Redis提供了expire、expireat、pexpire、pexpireat四个命令来设置key的过期时间,这四个命令底层都是通过调用expireGenericCommand函数来实现的,该函数的原型如下:
void expireGenericCommand(redisClient *c, long long basetime, int unit)
其中:
参数basetime用来指明基准时间,对于expireat和pexpireat命令,basetime的值为0,对于expire和pexpire命令,basetime的值为当前时间。参数basetime总是以毫秒为单位的。
参数unit用于指定argv[2]的格式:UNIT_SECONDS或者UNIT_MILLISECONDS,前者指明以秒为单位计算,后者指明以毫秒为单位计算。
从expireGenericCommand函数的实现上我们可以看出,虽然expire、expireat、pexpire、pexpireat四个命令分别使用了不同的单位(秒/毫秒)、不同的形式(过期时间/过期时间戳)来设置key的过期时间,但最后都会转换为统一的形式即以毫秒为单位的UNIX 时间戳作为过期时间戳。
void expireGenericCommand(redisClient *c, long long basetime, int unit)
robj *key = c->argv[1], *param = c->argv[2];
// 以毫秒为单位的unix时间戳
long long when; /* unix time in milliseconds when the key will expire. */
// 获取过期时间
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
return;
// 如果传入的过期时间是以秒为单位,则转换为毫秒为单位
if (unit == UNIT_SECONDS) when *= 1000;
// 加上basetime得到过期时间戳
when += basetime;
/* No key, return zero. */
// 取出key,如果该key不存在直接返回
if (lookupKeyRead(c->db,key) == NULL)
addReply(c,shared.czero);
return;
if (when <= mstime() && !server.loading && !server.masterhost)
// 如果when指定的时间已经过期,而且当前为服务器的主节点,并且目前没有载入数据
robj *aux;
redisAssertWithInfo(c,key,dbDelete(c->db,key));
server.dirty++;
/* Replicate/AOF this as an explicit DEL. */
// 传播一个显式的DEL命令
aux = createStringObject("DEL",3);
rewriteClientCommandVector(c,2,aux,key);
decrRefCount(aux);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c, shared.cone);
return;
else
// 设置key的过期时间(when提供的时间可能已经过期)
setExpire(c->db,key,when);
addReply(c,shared.cone);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
server.dirty++;
return;
3.2.2、删除过期key
对于过期的key,Redis负责将该key删除,为了提高运行效率,Redis采取这么一种处理方式:只有当真正要访问该key时才检查该key是否过期。如果过期就删除,如果没过期就正常访问。通常我们把这种只有在访问时才检查过期的策略叫做“惰性删除”。
具体实现上,Redis对所有读写数据库的命令在执行之前都会调用expireIfNeeded函数来检查对应key是否过期。该函数如下:
int expireIfNeeded(redisDb *db, robj *key)
// 获取key的过期时间
mstime_t when = getExpire(db,key);
mstime_t now;
// 如果该key没有过期时间,返回0
if (when < 0) return 0; /* No expire for this key */
/* Don't expire anything while loading. It will be done later. */
// 如果服务器正在加载操作中,则不进行过期检查,返回0
if (server.loading) return 0;
now = server.lua_caller ? server.lua_time_start : mstime();
// 如果当前程序运行在slave节点,该key的过期操作是由master节点控制的(master节点会发出DEL操作)
// 在这种情况下该函数先返回一个正确值,即如果key未过期返回0,否则返回1。
// 真正的删除操作等待master节点发来的DEL命令后再执行
if (server.masterhost != NULL) return now > when;
/* Return when this key has not expired */
// 如果未过期,返回0
if (now <= when) return 0;
/* Delete the key */
// 如果已过期,删除该key
server.stat_expiredkeys++;
propagateExpire(db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
return dbDelete(db,key);
在expireIfNeeded函数中,我们看到对于过期key,Redis主(master)节点和附属(slave)节点有不同的处理策略,具体如下:
- 如果当前Redis服务器是主节点,即
if (server.masterhost != NULL)
语句判断为false,那么当它发现一个过期key后,会调用propagateExpire函数向所有附属节点发送一个 DEL 命令,然后再删除该key。这种做法使得对key的过期操作可以集中在一个地方处理。 - 如果当前Redis服务器是附属节点,即即
if (server.masterhost != NULL)
语句判断为true,那么它立即向程序返回该key是否已经过期的信息。即便该key已经过期也不会真正的删除该key。直到该节点接到从主节点发来的DEL 命令之后,才会真正执行删除操作。
我们再来看看下面这两个函数。当Redis从数据库db中取出指定key的对象时,总是先调用调用expireIfNeeded函数来检查对应key是否过期,然后再从数据库中查找对象。
/* 该函数是为读操作而从数据库db中取出指定key的对象。
如果敢函数执行成功则返回目标对象,否则返回NULL。
该函数最后还根据是否成功取出对象更新服务器的命中/不命中次数。 */
robj *lookupKeyRead(redisDb *db, robj *key)
robj *val;
// 如果key已过期,删除该key
expireIfNeeded(db,key);
// 从数据库db中找到指定key的对象
val = lookupKey(db,key);
if (val == NULL)
// 更新“未命中”次数
server.stat_keyspace_misses++;
else
// 更新“命中”次数
server.stat_keyspace_hits++;
return val;
/* 该函数是为写操作而从数据库db中取出指定key的对象。
如果敢函数执行成功则返回目标对象,否则返回NULL。*/
robj *lookupKeyWrite(redisDb *db, robj *key)
// 如果key已过期,删除该key
expireIfNeeded(db,key);
// 从数据库db中找到指定key的对象
return lookupKey(db,key);
上面就是Redis中对于过期键的删除策略,但是这并不完整。我们还需要考虑另外一种情况:如果数据库db中有许多过期key,但又没有任何一个客户端访问这些key,那么在使用“惰性删除”策略下它们就永远也不会被删除。
为了解决这个问题Redis还会定期检查过期键并将其删除,这个过程由redis.c文件中的activeExpireCycle函数执行。关于该函数,等以后我们分析到redis.c文件时再解释。
db.c文件的源码:https://github.com/xiejingfa/the-annotated-redis-2.8.24/blob/master/db.c
以上是关于Redis源码剖析 - Redis之数据库redisDb的主要内容,如果未能解决你的问题,请参考以下文章
Redis源码剖析 - Redis数据类型之redisObject
Redis源码剖析 - Redis内置数据结构之字典dict