本章节主要的内容在于对 Redis 数据库的介绍,数据库保存键值对的方式,数据库增删改查数据的方式,保存键的过期时间的方式以及自动删除过期键的策略。

数据库结构

Redis 所有的数据库都保存在服务器 redisServer 结构的 db 数组中,每一项都是 redisDb 结构,每一个redisDb 结构都代表着一个数据库。

struct redisServer {
    ......
    // 数据库
    redisDb *db;
    
    // 服务器数据库数量
    int dbnum;
    ......
}
typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */
    // 正处于阻塞状态的键
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    // 可以解除阻塞的键
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    // 数据库号码
    int id;                     /* Database ID */
    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

一般 dbnum 值默认的是 16 ,因为 Redis 服务器一般默认创建 16 个数据库。
而且从上面的 redisDb 结构可以看到,每个数据库都是通过 dict 这个属性来保存所有的键值对,我们可以称它为键空间,它是一个字典结构:

  1. 键空间的键也就是数据库的键,每个键都是字符串对象。
  2. 键空间的值也就是数据库的值,每个值都是 Redis 对象也就是包含字符串对象,列表对象,哈希表对象等等之前说过的数据结构对象。

数据库一些基本操作

切换数据库

每个 Redis 客户端都有一个自己的目标数据库,每次执行对应的数据库命令的是就会对对应的目标数据库进行操作。默认情况下 Redis 客户端目标数据库是 0 号数据库,当然我们也可以通过 select 命令来切换目标数据库。

原理是在 Redis 服务器内部是通过 redisClient 结构来记录客户端状态,这个结构中有一个指向 redisDb 的指针,这个指针指向我们刚说的 redisServer 结构的 db 数组的其中一个元素,这个被指向的元素就是客户端的目标数据库。通过修改 redisClient.db 指针就可以让它指向服务器的不同的数据库。

redis库创建文件夹 redis建库建表_Redis

增删改查等操作

其实所有的增删改查操作都是对这个键空间也就是字典进行操作的。以书上的为例说明:

  1. 先通过以下命令初始化一个数据库键空间
SET message "hello world"
RPUSH alphabet "a" "b" "c"
HSET book name "Redis in Action"
HSET book author "Josiah L. Carlson"
HSET book publisher "Manning"

redis库创建文件夹 redis建库建表_服务器_02

  1. 添加新建也就是将一个键值对添加到这个字典当中。
SET date "2013.12.1"

redis库创建文件夹 redis建库建表_Redis_03

  1. 删除键实际就是在这个字典中删除对应的键值对。
DEL book

redis库创建文件夹 redis建库建表_数据库_04

  1. 更新键实际就是在字典中找到对应的键值对,根据键对应的值对象进行更新。
SET message "blah blah"

redis库创建文件夹 redis建库建表_Redis_05

  1. 对键取值实际就是在字典中找到对应的键值对,取出键对应的值对象。
GET message

redis库创建文件夹 redis建库建表_redis库创建文件夹_06

设置过期时间及过期键删除策略

设置过期时间

Redis 提供了四种不同的命令用于设置键的生存时间,在经过指定的时间之后服务器就会自动删除生存时间为 0 的键。

  1. EXPIRE [key] [ttl] 命令用于将键 key 的生存时间设置 ttl 秒。
  2. PEXPIRE [key] [ttl] 命令用于将键 key 的生存时间设置 ttl 毫秒。
  3. EXPIREAT [key] [timestamp] 命令用于将键 key 的生存时间设置 timestamp 所指定的秒数时间戳。
  4. PEXPIREAT [key] [timestamp] 命令用于将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳。

虽然有多种不同单位和不同形式的设置命令,但实际上 EXPIRE、PEXPIRE、EXPIREAT 三个命令都是使用PEXPIREAT 命令来实现的。无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行都是通过执行 PEXPIREAT 命令来实现。转化图如下:

redis库创建文件夹 redis建库建表_服务器_07

保存过期时间

在数据库结构中,我们也会看到我们除了用字典来保存键空间之后,我们还用了一个 expires 这个字典保存着数据库所有键的过期时间。

  1. 过期字典的键是一个指针,指向键空间的某一个键对象。
  2. 过期字典的值是一个 long long 类型的证书,保存着键所指向的数据库的过期时间(一个毫秒精度的 UNIX 的时间戳)。
    这里键空间的键和过期字典的键都指向同一个对象的目的就是不想出现重复的对象,浪费空间。

过期键的判断

在 Redis 中通过以下的步骤检查一个给定键是否过期:

  1. 检查这个键是否在过期字典中。
  2. 检查当前的 UNIX 时间戳是否大于过期时间。

过期键的删除策略

一般来说有三种不同的删除策略

  1. 定时删除:在设置键的过期时间的同时,创建一个定时器 timer,让定时器在键的过期时间来临时,立即执行对键的删除操作。
  2. 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
  3. 定期删除:每隔一段时间,程序就对数据库进行一-次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

在 Redis 中是通过惰性删除和定时删除两种策略搭配着来:

  1. 在所有的读写数据库的 Redis 命令执行之前,调用 expiredIfNeed 函数对输入键做检查,如果是过期键就删除,不然那就不做操作。
  2. Redis 周期性的执行 activeExpireCycle 函数,在规定的时间内,多次遍历服务器的数据库,从数据库的 expired 字典中随机检查一部分键的过期时间,并删除其中的过期键。

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

RDB处理方式

在生成一个 RDB 文件时,就已经会对数据库中的键进行检查,已过期的键是不会被保存到新创建的 RDB 文件中的。
在载入一个 RDB 文件时,如果是主服务器,在载入的时候就会对保存的键进行检查,过期的键就不会放到数据库中;如果是从服务器的话不管是否过期都会全部载入,但是因为主服务器会在数据同步的时候清理从服务的数据所以也不会产生影响。

AOF处理方式

在生成一个 AOF 文件时,如果某个键过期没有被删除也是会被写进到 AOF 文件中,但是当这个键被删除的时候,程序会向 AOF 文件中追加一条 DEL 命令来表示这个键已经被删除了。
在载入一个 AOF 文件时,不管是主服务器还是从服务器都会对数据库中的键进行检查,过期的键就不会放到数据库中。

复制功能的处理

从服务器的过期键删除动作由主服务器控制:

  1. 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
  2. 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
  3. 从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。