Redis设计 - 数据库与键

2020-08-07  本文已影响0人  家硕先生

前言

开始之前,我们可以设想一下,假设是我们自己要设计一款内存缓存系统,需要用什么结构来保存我们的数据呢?首选的当然就是map结构啦,保存key-value形式的键值对,简单易懂。
其实redis也是一样的,只不过redis有一些额外的数据结构用来支持其他功能(例如过期键,多数据库,RDB持久化),接下来我们来探究Redis中的数据库。其中Redis过期功能的实现,也是面试中的高频考察点之一,相信读者读完此篇之后,会对过期功能有个本质的认识。

服务器中的数据库

Redis服务器将所有的数据库都保存在redisServer结构的db数组中,意味着它不止一个数据库,数组保存着redisDB结构,每个redisDB结构都代表一个数据库对象。

struct redisServer {
    ...
    //数据库数量(默认为16个)
    int dbnum;
    //数据库对象数组,保存着服务器中所有的数据库
    redisDb *db;

    //其他属性对象......
}

dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,所以Redis服务器默认会创建16个数据库,如下图所示:


服务器数据库示例

切换数据库

默认情况下,Redis客户端的目标数据库为0号数据库,但是客户端可以通过执行SELECT命令来切换目标数据库。

typedef struct redisClient {

    //记录客户端正在使用的数据库
    redisDB *db;

} redisClient;

在redis客户端执行> SELECT 1 时,将切换成1号数据库,如下图所示:

客户端的目标数据库由0号切换成1号

需要注意的是,当你的程序在处理多数据库的Redis时,多次切换数据库后可能忘记当前处理哪个数据库,为了避免误操作,在执行redis命令如 flushdb时,最好先执行select命令显示切换到目标数据库。

数据库键空间

Redis服务器中的每个数据库都由一个redis.h/redisDb结构表示,redisDb结构的dict字典保存了数据库中的所有键值对,称为键空间(key space):

typedef struct redisDb {
    // ...
    // 数据库键空间,保存着数据库中所有键值对
    dict *dict;
    // ...
} redisDb;

键空间就是我们平时操作Redis存取数据的地方

假设我们执行以下几个命令:

redis> SET message "hello world"
OK
redis> RPUSH alphabet  "a" "b" "c"
(integer) 3
redis> HSET book name "Redis in Action"
(integer) 1
redis> HSET book author "Josiah L. Carlson"
(integer) 1

执行命令后,数据库的键空间如图:


数据库键空间示例

我们对Redis的增删改查等操作,都是通过对键空间字典进行操作来实现的,包括 exists, rename, keys等。

读写键空间时的维护操作
当使用Redis命令对数据库进行读写操作时,服务器不仅会对键空间执行相应的命令操作,还会执行一些额外的维护操作,包括:

键操作

设置过期时间
通过expire命令或者pexpire命令,客户端可以通过秒或者毫秒的精度为数据库中的某个键设置国过期时间(Time To Live, TTL)。

redis> set name "jasorchen"
OK

redis> expire name 5
(integer) 1

redis> get name // 5秒内
"jasorchen"

redis> get name // 5秒后
(nil)

与expire命令类似的,还有expireat命令,设置指定过期时间为指定的时间戳,本质上是一样的,expire命令就是在当前时间戳的基础上加上过期时间而已。Redis提供了四个命令设置键的过期时间:

虽然有多种命令设置键的过期时间,但是最终都是通过转换成PEXPIREAT命令来实现的。

命令转换

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

typedef struct redisDb {
    // ...
    //过期字典,保存键的过期时间
    dict *expires;
    // ...
} redisDb;
过期字典示例

移除过期时间
PERSISIT命令可以移除一个键的过期时间,其大致原理就是在过期字典中找到给定的键,并从过期字典中移除。

过期键的判定
通过过期字典,程序可以判断以下步骤检查一个键是否过期:

过期键的删除策略
如果一个键过期了,那么在何时会被删除呢?有如下三种策略:

过期键删除策略——立即删除

过期键删除策略——惰性删除

过期键删除策略——定期删除
定期策略是前面两种策略的一种整合折中:
每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的市场和频率来减少删除操作对CPU时间的影响,通过定期删除,有效的减少了因为过期键而带来的内存浪费。

该策略的难点是确定删除操作执行的时长和频率:
1.如果删除操作执行得太频繁,或者执行的时间太长,CPU将消耗过多的时间在删除键上面。
2.如果删除频率太低,或者执行的时间太短,一样会造成内存浪费的情况。

Redis的过期键删除策略

前面讨论了立即删除、惰性删除和定期删除三种过期键删除策略,那么Redis服务器实际采用了哪种方式删除过期键呢?
答案是,惰性删除和定期删除结合使用,使得服务器可以在合理使用CPU时间和避免浪费内存空间之间取得平衡。

惰性删除策略的实现
键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行前,都必须调用该函数作检查:

定期删除策略实现
键的定期删除策略由activeExpireCycle函数实现,每当Redis的服务器周期性操作serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。

在下一次activeExpireCycle函数调用时,会接着上一次的进度进行处理。

伪代码如下

//默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16;
//默认每次检查的键数量
DEFAULT_KEY_NUMBERS = 20;

//全局变量,记录数据库检查进度
current_db = 0;

def activeExpireCycle() {
    //确保数据库数量不会越界
    if server.dbnum < DEFAULT_DB_NUMBERS;
        db_numbers = server.dbnum;
    else
        db_numbers = DEFAULT_DB_NUMBERS;
    //获取当前要处理的数据库
    redisDB = server.db[current_db];
    //数据库索引加一
    current_db++;
    
    //获取过期字典里面的键,
    for (int i = 0; i < DEFAULT_KEY_NUMBERS; i++) {
        //没有过期键的直接跳过
        if (redisDB.expires.size() == 0) {
            break;
        }
        //随机获取一个带有过期时间的键
        key_with_ttl = redisDB.expires.getRandomKey();
        //检查是否过期
        if (isExcepired(key_with_ttl)) {
            deleteKey(key_with_ttl)
        }
        //如果已经达到时间上限,停止处理
        if (reach_time_limit())
           return;
    }
}

AOF、RDB和复制功能对过期键的处理

在Redis的持久化(AOF和RDB),以及复制功能时,是如何处理数据库的过期键的呢?

生成RDB文件
用save命令或者bgsave命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已经过期的键不会被保存在新创建的RDB文件中。因此不会对生成RDB文件有任何的影响。

载入RDB文件
在启动Redis服务器时,如果开启了RDB功能,那么服务器将对RDB文件进行载入:

AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中某个键已经过期,但它还没有被各种删除策略发现,那么AOF文件不会因为这个过期键而产生任何影响。当键被惰性或者定期策略删除删除后,程序会像AOF文件追加一条DEL命令, 显示的删除这个键。

因为AOF保存的操作命令,所以键的读写操作命令都会被保存在AOF文件中,当键过期时,通过惰性删除或者定时删除之后,AOF会保存相应的删除命令,显式记录该键已经被删除。

如:客户端使用GET message访问过期的message键,那么服务器将执行以下三个动作:

  1. 从数据库中删除message键
  2. 追加一条DEL message命令道AOF文件
  3. 向执行GET命令的客户端返回空回复

AOF重写

执行AOF文件重写,程序会对键进行检查,已经过期的不写入到新的AOF文件中。

为什么会有AOF重写?

  1. AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大;影响包括但不限于:对于Redis服务器,计算机的存储压力;AOF还原出数据库状态的时间增加;
  2. 为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。

主从复制对过期键的处理

当服务器运行在复制模式下,从服务器的过期键删除策略又主服务器控制:

  1. 主服务器删除一个过期键之后,会显式的向所有从服务器发送一条DEL命令。
  2. 从服务器接收到客户端的读取key请求,即使该键已经过期,也不删除,而是当做未过期键来处理。
  3. 从服务器只有在接收到主服务器发送的DEL命令后,才会删除过期键。

通过主服务器来控制从服务器统一地删除过期键,可以最大程度的保证主从数据的一致性,由主服务器来确定何时删除过期键。

上一篇下一篇

猜你喜欢

热点阅读