Redis源码剖析】 - Redis之数据库redisDb Redis源码剖析】 - Redis之数据库redisDb

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我们看到:

(1)、redisDb中的dict *dict成员就是将key和具体的对象(可能是string、list、set、zset、hash中任意类型之一)关联起来,存储着该数据库中所有的键值对数据。该字段又称为键空间key space。 
(2)、expires成员用来存放key的过期时间。 
(3)、id成员是数据库的编号,以整型表示。

接下来我们主要基于上面3个redisDb的成员来展开讲解。其余几个成员在前面我们已经介绍过,大家可以参看前面的几篇博客,这里给出给相关文章链接:

  1. blocking_keys、ready_keys: 【Redis源码剖析】 - Redis数据类型之列表List
  2. 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
  • 1
  • 2
  • 3
  • 4

所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中,而redisClient中存在一个名叫db的指针指向当前使用的数据库(默认为0号数据库)。

typedef struct redisClient {
    ...
    // 当前所使用的数据库
    redisDb *db;
    ...
} redisClient;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Redis提供了select 命令用于切换到指定的数据库,该命令的具体格式为:

SELECT db_index 
  • 1

其中数据库索引号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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

由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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这些操作并没有什么难点,大家可以参看后文提供的加以注释的源码进行学习,这里就不一一解释每个函数的实现。

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

有了过期时间戳我们就很容易判断某个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)
  • 1

其中:

参数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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

在expireIfNeeded函数中,我们看到对于过期key,Redis主(master)节点和附属(slave)节点有不同的处理策略,具体如下:

  1. 如果当前Redis服务器是主节点,即if (server.masterhost != NULL)语句判断为false,那么当它发现一个过期key后,会调用propagateExpire函数向所有附属节点发送一个 DEL 命令,然后再删除该key。这种做法使得对key的过期操作可以集中在一个地方处理。
  2. 如果当前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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
/*  该函数是为写操作而从数据库db中取出指定key的对象。
    如果敢函数执行成功则返回目标对象,否则返回NULL。*/
robj *lookupKeyWrite(redisDb *db, robj *key) {
    // 如果key已过期,删除该key
    expireIfNeeded(db,key);
    // 从数据库db中找到指定key的对象
    return lookupKey(db,key);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面就是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

原创作品,转载请标明: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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我们看到:

(1)、redisDb中的dict *dict成员就是将key和具体的对象(可能是string、list、set、zset、hash中任意类型之一)关联起来,存储着该数据库中所有的键值对数据。该字段又称为键空间key space。 
(2)、expires成员用来存放key的过期时间。 
(3)、id成员是数据库的编号,以整型表示。

接下来我们主要基于上面3个redisDb的成员来展开讲解。其余几个成员在前面我们已经介绍过,大家可以参看前面的几篇博客,这里给出给相关文章链接:

  1. blocking_keys、ready_keys: 【Redis源码剖析】 - Redis数据类型之列表List
  2. 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
  • 1
  • 2
  • 3
  • 4

所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中,而redisClient中存在一个名叫db的指针指向当前使用的数据库(默认为0号数据库)。

typedef struct redisClient {
    ...
    // 当前所使用的数据库
    redisDb *db;
    ...
} redisClient;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Redis提供了select 命令用于切换到指定的数据库,该命令的具体格式为:

SELECT db_index 
  • 1

其中数据库索引号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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

由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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这些操作并没有什么难点,大家可以参看后文提供的加以注释的源码进行学习,这里就不一一解释每个函数的实现。

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

有了过期时间戳我们就很容易判断某个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)
  • 1

其中:

参数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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

在expireIfNeeded函数中,我们看到对于过期key,Redis主(master)节点和附属(slave)节点有不同的处理策略,具体如下:

  1. 如果当前Redis服务器是主节点,即if (server.masterhost != NULL)语句判断为false,那么当它发现一个过期key后,会调用propagateExpire函数向所有附属节点发送一个 DEL 命令,然后再删除该key。这种做法使得对key的过期操作可以集中在一个地方处理。
  2. 如果当前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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
/*  该函数是为写操作而从数据库db中取出指定key的对象。
    如果敢函数执行成功则返回目标对象,否则返回NULL。*/
robj *lookupKeyWrite(redisDb *db, robj *key) {
    // 如果key已过期,删除该key
    expireIfNeeded(db,key);
    // 从数据库db中找到指定key的对象
    return lookupKey(db,key);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面就是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

猜你喜欢

转载自blog.csdn.net/TuxedoLinux/article/details/80892820