数据库
Redis数据库结构体在源码中定义为redis.h/redisDb结构体,结构体并不复杂,其中仅仅含有8个字段,理解*dict、*expires、id三个字段即可。
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
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;
从本篇文章开始记录Redis数据库相关的内容,数据库相关的知识点分为五个部分,分别是Redis服务器中的数据库、键的生存时间和过期时间、过期键删除策略和AOF,RDB,复制功能对过期键的处理。
1. Redis服务器中的数据库
Redis服务器将所有数据库保存在一个redisDb类型的数组中,数组中每一个redisDb类型的元素代表一个数组。在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,该属性由服务器配置选项database
进行设置。,默认值为16。
![]() |
---|
Redis服务器中的数据库 |
Redis服务器结构体在源码中定义为redis.h/redisServe
r结构体,redisServer结构体中数据库相关的定义仅有2个字段*db
和dbnum
。
struct redisServer {
...
// 数据库,指向一个长度为dbnum的数组首地址
redisDb *db;
// 服务器数量,由服务器配置的database选项确定,默认值为16
int dbnum;
...
}
1.1. 切换数据库
每个Redis客户端都有自己的目标数据库,当客户端执行命令时,目标数据库就会成为这些命令的操作对象。默认情况下,Redis客户端目标数据库为0号数据库,客户端通过执行SELECT
命令来切换目标数据库。
切换数据库时,只需要将redisClient结构体中的redisDb类型的指针指向redisServer.db相应的数组元素即可。通过修改redisClient.db指针,让其指向服务器中的不同数据库,从而实现切换目标数据库的功能,这便是SELECT
命令的实现原理。
![]() |
---|
选择数据库 |
1.2. 数据库键空间
Redis是一个键值对数据库服务器,在redisDb结构体中有一个dict类型的字典,保存了数据库中的所有键值对,这个字典称为键空间。键空间中的键都是一个STRING对象;值对应Redis支持的五种数据类型中任意一种,分别是STRING、HASH、LIST、SET和ZSET。
![]() |
---|
数据库键空间 |
数据库的键空间本质上就是一个字典,对Redis数据库的操作,实际上就是对redisDb.dict进行的操作。下面以数据库常见的四种操作增、删、改、查为例分别进行说明。
增加一个键值对到数据库,实际是将该键值对添加到键空间字典里,键为字符串对象,值为Redis支持的任意类型对象。同样删除,修改,查询数据库中的某个键,实际上是删除,修改,查询redisDb.dict中对应的键值对对象。
除此之外,还有其他针对Redis键空间的一些操作,如清空数据库FLUSHDB
,随机返回某个键RANDOMKEY
等。
2. 键的生存时间和过期时间
有些情况下,需要键空间中的某些键仅在某一个时间段(秒和毫秒级别)存在,过了设定时间就自动删除。Redis提供了两种策略,分别是设置键的生存时间和过期时间。设置键的生存时间,使得键空间中的某些键仅在指定时间内存在,也就是EXPIRE
和PEXPIRE
命令。设置键的过期时间,是以UNIX时间戳的方式设置键在某个时间段的有效性,也就是EXPIREAT
和PEXPIREAT
命令。
数据库键的过期时间都保存在过期字典中,对应redisDb.expires属性。Redis中和过期时间相关的知识点包括,设置过期时间,保存过期时间,移除过期时间,计算并返回剩余生存时间,过期键的判定等
3. 过期键删除策略
既然Redis支持设置键的生存时间和过期时间,那么在键的失效后,如何执行删除操作呢?Redis针对过期键提供了三种不同的过期删除策略,分别是定时删除、惰性删除和定期删除。定时删除和定期删除为主动删除策略,惰性删除为被动删除策略。
3.1. 定时删除
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
定时删除策略对内存最友好,可以保证过期键尽可能快的被删除。从另一方面讲,这种策略对CPU并不友好,可能会占据相当一部分CPU时间。
如果有大量命令等待服务器处理,且服务器中有大量空闲内存,如果此时有大量过期键,CPU会去删除这些过期键,而不去处理客户端请求。
另外,服务器维护大量定时器的创建,也是一个很大的开销,事实上,现阶段使用定时删除策略并不现实。
3.2. 惰性删除
放任过期键不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期就删除,否则就返回该键。
惰性删除策略对CPU是最友好的,程序只会在取键时才会对键进行过期检查。从另一方面讲,这种策略对内存并不友好,一些过期键会保留在数据库中。
如果数据库中有大量过期键,而这些键又恰好没有被访问到,会永远不被删除,这种情况甚至可以被看作内存泄露。
对于一些和时间有关的数据,如log文件,在某个时间点过后,访问会大大减少,甚至不再访问,这种数据大量积压在数据库,会造成严重的后果。
3.3. 定期删除
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。
定期删除可以看作定时删除和惰性删除两种策略的折中,但执行起来有一定难度,主要问题在确定删除操作的执行时长和删除频率。因此,如果采用定期删除策略,一定要根据服务器具体情况,合理设置删除操作的执行时长和删除频率。
3.4. Redis的过期键删除策略
Redis服务器使用的过期键删除策略是惰性删除和定期删除配合使用。惰性删除由db.c/expireIfNeeded
函数实现,所有读写数据库的Redis命令,在执行之前都会调用expireIfNeeded
函数对输入键进行检查;定期删除策略由redis.c/activeExpireCycle
函数实现,每当redis的服务器周期性操作redis.c/serverCron
函数执行时,activeExpireCycle
函数就会被调用。
4. AOF、RDB和复制功能对过期键处理
- RDB:Redis的RDB文件涉及到的对过期键操作,主要体现在生成RDB文件和载入RDB文件两个过程中。
生成RDB文件和载入RDB文件:
- 生成RDB文件 :执行
SAVE
或者BGSAVE
命令时,程序会对数据库进行检查,已过期的键不会被保存到新创建的RDB文件中- 载入RDB文件:如果服务器以主服务器模式运行,过期键会被忽略,不会对RDB文件主服务器造成影响;如果服务器以从服务器模式运行,载入RDB文件时,文件中保存的所有键都会载入到数据库中。
- AOF:Redis的AOF文件涉及到的对过期键的操作,主要体现在AOF文件写入和AOF文件重写两个过程中。
AOF文件写入和AOF重写:
- AOF文件写入:当执行GET命令,发现过期键被惰性删除或定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。
- AOF重写:在执行AOF重写过程中,程序会对数据库中的键进行检查,已过期的键不会保存到重写后的AOF文件中。
- 复制:当服务器运行在复制模式下,从服务器的过期删除动作由主服务器控制,从服务器即使发现过期键也不会自作主张删除它。
5. 参考文献
[1] 黄健宏.Redis设计与实现[M].北京:机械工业出版社