一,服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,而db数组的每一项都是一个redis.h/redisDb结构,每个redisDB就代表一个数据库。

下面是来自

的代码:(这是一位将Redis源码进行了详尽分析的大神的博客)

struct redisServer {
    redisDb *db;
    int dbnum;                      /* Total number of configured DBs */
};

初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,也就是根据dbnum来创建一个相应长度的redisDb类型数组。(dbnum属性值由服务器配置的database选项决定,默认情况下该选项为16,因此dbnum也为16自然的数据库就会有16个)。 

typedef struct redisDb {
    // 键值对字典,保存数据库中所有的键值对
    dict *dict;                 /* The keyspace for this DB */
    // 过期字典,保存着设置过期的键和键的过期时间
    dict *expires;              /* Timeout of keys with a timeout set */
    // 保存着 所有造成客户端阻塞的键和被阻塞的客户端
    dict *blocking_keys;        /*Keys with clients waiting for data (BLPOP) */
    // 保存着 处于阻塞状态的键,value为NULL
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    // 事物模块,用于保存被WATCH命令所监控的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    // 当内存不足时,Redis会根据LRU算法回收一部分键所占的空间,而该eviction_pool是一个长为16数组,保存可能被回收的键
    // eviction_pool中所有键按照idle空转时间,从小到大排序,每次回收空转时间最长的键
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    // 数据库ID
    int id;                     /* Database ID */
    // 键的平均过期时间
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

二,切换数据库

客户端登录时,默认为0号数据库。而每个Redis客户端都有自己的目标数据库,每当执行读写命令时就会对目标数据库进行操作。

  • 1、我们可以通过SELECT命令来切换目标数据库。(如图,这里默认的为0号数据库,而我们使用 SELECT INDEX命令,选择了1号服务器,从msg “hello redis”这一键值对我们可以看出,的确是更换了服务器。)

redis指定库编号 redis数据库编号_redis数据库

  • 2、切换数据库示意图

redis指定库编号 redis数据库编号_redis_02

  • 3、数据库切换实现代码
// 切换数据库
int selectDb(client *c, int id) {
    // id非法,返回错误
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    // 设置当前client的数据库
    c->db = &server.db[id];
    return C_OK;
}

三、数据库键空间

3.1、键空间的概述

Redis是一个键值对数据库服务器,我们知道服务器中每个数据库都由一个redisDb结构表示,而其中的redisDb结构的dict字典保存了数据库中的所有键值对,这个字典叫做键空间(key space)

1.在redisDb中的代码表示如下:

typedef struct redisDb {
    // 键值对字典,保存数据库中所有的键值对
    dict *dict;

    //省略部分代码

} redisDb;

2,键空间其实不难难以理解,它和我们所见的数据库是对应的。

  • 键空间的键也就是数据库的键,每个键都是一个字符串对象
  • 键空间的值也就是数据库的值,每个字可以为五大对象中的一种(字符串对象,哈希对象,列表对象,集合对象,有序集合对象)

redis指定库编号 redis数据库编号_数据库_03

这时数据库键空间之间的关系图例:

redis指定库编号 redis数据库编号_redis数据库_04

由数据库的键空间为字典的数据结构实现我们可以知道,针对数据库的增删改查都是通过键空间字典进行操作实现的。下面将一一介绍。

 

3.2、添加新键

添加一个新键值对的操作,就是给键空间里面添加一个键值对对象。其实我们经常进行。

redis指定库编号 redis数据库编号_redis数据库_05

这里就添加了一个新的键值对,键对象为msg,值对象为“hello redis”

3.3、删除键

删除数据库中的键,其实是删除键空间里面的键值对对象。

redis指定库编号 redis数据库编号_数据库_06

3.4、更新键

更新键分为两种形式:(出现这种情况的原因是:值对象类型的不同)

1、是对键值空间中已经存在的键值对中的值对象保存的值进行更新。(将值对象保存的“hello redis”改为“update the value”)

redis指定库编号 redis数据库编号_redis数据库_07

2、对于键值空间的键值对,如一个哈希对象的值对象,在其中添加一个新的键值对也是更新操作。(在这里添加一个新的键值对 id 20183000)

redis指定库编号 redis数据库编号_redis数据库_08

3.5、对键取值

顾名思义,对键取值就是对一个数据库键进行取值,实际上就是在键空间中取出键所对应的值对象。

如图:

redis指定库编号 redis数据库编号_惰性删除_09

根据键对象msg取出对应的值对象保存的字符串“update the value”显示。

这里是实现该操作的源码。

// 该函数被lookupKeyRead()和lookupKeyWrite()和lookupKeyReadWithFlags()调用
// 从数据库db中取出key的值对象,如果存在返回该对象,否则返回NULL
// 返回key对象的值对象
robj *lookupKey(redisDb *db, robj *key, int flags) {
    // 在数据库中查找key对象,返回保存该key的节点地址
    dictEntry *de = dictFind(db->dict,key->ptr);
    if (de) {   //如果找到
        robj *val = dictGetVal(de); //取出键对应的值对象

        /* Update the access time for the ageing algorithm.
         * Don't do it if we have a saving child, as this will trigger
         * a copy on write madness. */
        // 更新键的使用时间
        if (server.rdb_child_pid == -1 &&
            server.aof_child_pid == -1 &&
            !(flags & LOOKUP_NOTOUCH))
        {
            val->lru = LRU_CLOCK();
        }
        return val; //返回值对象
    } else {
        return NULL;
    }

3.6、服务器对于读写键空间的维护

在使用Redis命令对数据库进行读写时,服务器不仅对键空间执行指定的读写操作,同时也会进行一些额外的维护操作。

  • 在进行读操作或者写操作读取一个键之后,服务器会根据键是否存在更新键空间的misses(不命中)和hit(命中)信息。(查看方式,INFO stats命令的keyspace_hits属性和keyspace_misses属性查看。)

redis指定库编号 redis数据库编号_数据库_10

  • 在读取一个键后,服务器会更新键的LRU(最后一次使用)时间。这个值可以用于计算键的闲置时间,与键的删除有关系。(查看方式, OBJECT idletime<key>命令查看key键的LRU)

redis指定库编号 redis数据库编号_惰性删除_11

距离上一次的使用时间

  • 还会对已经过期的键进行删除

四、键的生存时间和过期时间以及键的三种删除方式

3.1、键的生存和过期时间的设置

3.1.1、设置键的生存时间

对于Redis中的键,你可以进行设置,使得相应的键在键空间里面生存固定的时间,然后便被服务器从键空间中自动删除。

设置命令(EXPIRE(以秒为单位)命令和PEXPIRE命令(以毫秒为单位)),后 客户端可以以秒或者毫秒为单位的时间精度对数据库中的某个键设置生存时间,超过生存时间(也就是到达生存时间),服务器就会自动删除。

redis指定库编号 redis数据库编号_数据库_12

3.1.2、设置键的过期时间

客户端可以通过命令EXPIREAT命令或PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间。(EXPIREAT命令设置秒为单位,PEXPIREAT命令设置为毫秒为单位)。

想要知道一个被设置了生存时间和过期时间的键距离被自动删除的时间可以使用TTL或PTTL命令。

 

虽然这里有不同的命令设置键的生存时间和删除时间。但是其中的命令EXPIRE,PEXPIRE,EXPIREAT这三个命令最终都将转换为PEXPIREAT命令一样。

3.2、过期时间、过期字典

redisDb结构保存的expires字典保存了数据库中所有键的过期时间。

redis指定库编号 redis数据库编号_redis_13

这个expires字典也被我们叫做过期字典。过期字典的键是一个指针,这个指针指向键空间中的某个键对象。

 

通过这个过期字典我们可以判定一个键是否过期,步骤:

1)检查键是否存在于过期字典中,如果存在过期字典中,取得过期时间。

2)检查当前UNIX时间戳是否大于键的过期时间,如果大于,则键过期。

四、过期键删除的三种策略

4.1、定时删除

实现:在设置键的过期时间时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。

1)定时删除策略对于内存友好,通过定时器的使用可以使得到期的键被及时删除,释放空间,因此对于内存友好。

2)定时删除策略对于CPU不友好,当数据库同时删除大量键时,会十分占有CPU,而定时器如果设置相同的删除时间,就会出现同时删除很多键的情况,十分消耗CPU。CPU大量时间耗费在删除与此时任务无关的删除键的任务上,对于redis的高性能体现较差。

4.2、惰性删除

实现:在取出键值对时对键值对进行过期检查,如果过期了就删除,否则就继续保留。

惰性删除与定时删除策略恰好相反,

1)惰性删除对于CPU十分友好,程序在取出键值对时对键值对进行过期检查,将键的删除操作在必须进行时才进行。就很小几率出现同时需要删除很多键的情况。使得CPU不会在删除其他与程序无关的键上花费任何时间。

2)惰性删除对于内存不友好。惰性删除策略中如果键已经过期了,但是如果不使用它,就一直会保存在内存中不会被删除。就可能出现在内存中依然保存很多已经过期了的键却因为没有使用而不被删除。白白的浪费了内存,出现内存泄漏。

4.3、定期删除

定期删除算是以上两种删除策略的一个折中、整合。

1)定期删除策略和定时删除策略一字只差,差别还是挺大。定期策略没有采用定时器到达过期时间才删除,而是每隔一段时间执行一次删除过期键的操作。(在这过程中,限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。但是如果删除操作执行太频繁,就会使得定期删除退化为定时删除)。

2)定期删除,不会使得过期的键长期存放在内存中不删除,从而节约了内存。但是如果执行删除操作的时间间隔太长,会使得定期删除策略退化为惰性删除。

4.4、redis对于三种删除策略的使用

redis服务器并没用使用全部三种策略。而是使用惰性删除和定期删除两种策略。通过灵活的配合使用这两种删除策略可以使得CPU和内存的使用取得平衡。