《Redis设计与实现》数据库

本文最后更新于 2025年8月29日 下午

服务器数据库

1
2
3
4
5
6
7
8
struct redisServer{
// ...
// 一个保存着服务器中所有数据库的数组
redisDb *db;
// 服务器的数据库数量(df长度)
int dbnum;
// ...
}

注意:

  • df:保存了服务器中所有数据库的数组,说是数据库,但由于Redis是一个键值对数据库,所以实际上内部也是一个类似于字典的结构
  • dbnum:属性值有服务器配置的database选项决定,不过默认是16

切换数据库

用户可以用SELECT来切换目标数据库。在客户端结构中,具有一个指向redisDb结构的指针:

1
2
3
4
5
6
typedef struct redisClient{
// ...
// 记录客户端正在使用的数据库
redisDb *db;
// ...
} redisClient;

注意:

  • db:这个与服务器结构中的db不一样,这个指向的是一个特定的数据库,而服务器中指代的是一个数据库数组。
  • Redis并没有提供一个方法用于让客户知道自己现在使用的是哪个数据库。通常来说,在redis-cli中可以看见正在使用的数据库号码,然而在其他语言环境下也许并没有这个号码。因此最好在执行数据库关键命令前显式地将当前数据库切换到想要操作的数据库上。
  • 多线程问题?Redis的命令执行是一个绝对的单线程。即便这里看起来是有多个数据库,Redis也依然是在同一个程序上单线程地执行所有数据中的内容。因此:
    • 单个数据库出现慢查询问题会影响到所有其他数据库

数据库键空间

1
2
3
4
5
6
typedef struct redisDb{
// ...
// 数据库键空间,保存着该数据库的所有键值对
dict *dict;
// ...
} redisDb;

这一段非常地直观,我这里就直接给一个结构图即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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> HSET book publisher "Manning"
(integer) 1

执行这些命令后,数据库键空间长这样:

数据库键空间示例

过期时间

作为一个基于内存的数据库,对数据库元素有一个严谨的管理十分重要。Redis中设计了一套命令使用户能够轻松地定义任何数据的过期时间,基于设定的时间,通过一系列算法来完成过期删除操作。

  • EXPIRE命令:用于将键key的生存时间设置为ttl秒。
  • PEXPIRE命令:用于将键key的生存时间设置为ttl毫秒。
  • EXPIREAT命令:用于将键key的过期时间设置为timestamp所指定的秒数时间戳。
  • PEXPIREAT命令:用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳。

虽然每一个命令都扮演着不同的角色,但实际上EXPIRE, PEXPIRE, EXPIREAT在底层代码中,最终都会被转化为PEXPIREAT。具体转化关系如下:

graph TD

EXPIRE --> PEXPIRE --> PEXPIREAT
EXPIREAT --> PEXPIREAT

能够定义键的过期时间,则必然需要一个地方用于存储这些时间

1
2
3
4
5
6
typedef struct redisDb{
// ...
// 过期时间字典,保存着键的过期时间
dict *expires;
// ...
} redisDb;

expires中,每一个键代表的是设置了过期时间的对象键,值为其过期时间。

  • 过期时间为一个Unix时间戳(Unix Timestamp)。对于那些不知道这是啥的小伙伴,这个时间戳表示了自1970年1月1日 00:00:00 UTC 以来所经过的描述。Redis这里的时间戳代表的是毫秒数。

对于那些已经设置了过期时间的键,也可以使用PERSIST来移除其过期时间。

过期键的判定基于当前时间,如果当前时间比键的过期时间大,则代表键过期了,此时调用这个键的is_expired将会返回True

过期键的删除

过期键的主流删除策略有如下三种:

  • 定时删除: 在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
    • CPU不友好,内存友好。当同时有大量键需要删除时,会造成明显的性能问题。
    • 维护定时器的方法为使用一个无序链表。因此查找定时器的时间复杂度为O(N)O(N),从这个角度看,也不合理。
  • 惰性删除: 放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
    • CPU时间友好,内存不友好。当一个键已经过期了,但长期没有再接到任何访问,则会一只存在于内存中积灰。当有大量键出现如此情况时,则是灾难级的问题。
  • 定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
    • 介于定时删除于惰性删除之间的方法。每个一段时间执行一次删除过期键的操作,并且限定该操作的执行时间,以减少对CPU时间的影响。同时,它也确实删除了一些过期键,也会减少内存浪费的现象。
    • 删除操作的执行时间需要找到一个合理的平衡或者算法,不然过长或过短,都会将该策略变成定时或惰性删除策略。

在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。

Redis的策略

Redis采用了定期删除+惰性删除的综合策略

  • 惰性删除:就是使用正常的惰性删除逻辑。每一次访问(读写)数据库的Redis命令在执行前都会调用expireIfNeeded函数对输入键进行检查,以判断做一下三个行为的其中一个:
    • 没找到输入键数据:返回空值
    • 找到了输入键数据,但数据过期了:删除数据,并返回空值
    • 找到了输入键数据且没有过期:返回数据值
  • 定期删除:redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用。它将在规定时间内,分多次遍历服务器中的各个数据库,从expire字典中随机检查一部分键,并删除过期键。
    • activeExpireCycle函数中具有一个current_db全局变量,用于记录当前activeExpireCycle的检查进度。下一次调用时,则会从current_db的下一个开始,接着执行。
    • 当所有数据库检查完毕后,current_db变量重置为0

对AOF、RDB和复制功能的影响

生成RDB文件: 执行SAVEBGSAVE命令时,会检查数据库中的键,并且过滤掉那些已经过期的键。

  • 注意: RDB文件生成阶段只会过滤掉过期键,不会帮助删除过期键!

载入RDB文件: RDB文件的载入机制取决于该服务器以主从哪个模式运行:

  • 主服务器模式:过滤已过期的键,只会把未过期的键载入到数据库中,以确保过期键不会对主服务器造成影响。
  • 从服务器模式:会把所有键载入到数据库中。不过在进行主从同步时,从服务器的数据会被清空,因此一般来说问题不大。

AOF文件写入: 服务器以AOF持久化模式运行时,已过期但未被删除的键不会对AOF文件产生影响(因为本来也没有对过期键做出任何操作)

  • 在删除一个过期键后,程序会在AOF文件中append一条DEL命令,显式地记录该键已被删除
  • 在程序访问一个过期键时,由于GET返回的内容为空且程序会将该键删除,所以在AOF文件中只会多出一条DEL命令表示该键已被删除。不会添加GET命令

AOF重写: 与RDB的生成类似,会过滤掉已经过期的键。

复制: 服务器处于复制模式时,从服务器的过期键删除动作由主服务器控制:

  • 主服务器删除一个过期键后,会显式对所有从服务器发送一个DEL命令,告知他们需要删除这个过期键
  • 从服务器只会在收到从主服务器发来的DEL命令之后才会删除指定过期键。所以不论客户端发来的读命令是否是一个过期键,从服务器也还是会像处理未过期的键一样处理过期键。

《Redis设计与实现》数据库
http://example.com/2025/08/27/Redis学习/《Redis设计与实现》数据库/
作者
Clain Chen
发布于
2025年8月27日
许可协议