前言
深入了解一下Redis内存机制如何存储数据,以及对于过期数据采取何种策略来清理。
@
目录一、Redis服务器中的数据库
Redis服务器将所有的数据库保存在 redis.h/redisServer
结构的 db 数组中,db 数组的每个项都是一个 redis.h/redisDb
结构,每个 redisDb
结构代表一个数据库。另外Redis中数据库的数量由 dbnum
指定,默认为16。
struct redisServer {
//一个数组,保存着服务器中所有的数据库
redisDb *db;
//服务器的数据库数量,默认为16
int dbnum;
}
客户端状态的 redis.h/redisClient
结构的 db 属性记录了客户端当前的目标数据库,这个属性是一个指向 redisDb 结构的指针。redisClient.db 指针指向 redisServer.db 数组中的一个元素,而被指向的元素就是客户端的目标数据库。
typedef struct redisClient {
//记录客户端当前正在使用的数据库
redisDb *db;
}
常见的 SELECT xxx
切换数据库指令的实现原理就是通过修改 redisClient.db
指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能。
二、数据库键空间
2.1 数据存储——键空间 dict
redisDb 数据库结构中的 dict 字典保存了数据库中的所有键值对,我们称这个字典为键空间。
键空间的键就是数据库的键,均为字符串对象;键空间的值对应着Redis 的几种数据结构类型(字符串对象、列表对象、哈希表对象、集合对象、有序集合对象)。
typedef struct redisDb {
// 数据库键空间,保存了数据库中所有的键值对
dict *dict;
} redisDb;
2.2 键的生存时间——过期字典expires
2.2.1 设置过期时间
EXPIRE <key> <ttl>
:将键的生存时间设置为 ttl 秒;PEXPIRE <key> <ttl>
:将键的生存时间设置为 ttl 毫秒;EXPIREAT <key> <timestamp>
:将键 key的过期时间设置为 timestamp 所指定的秒数时间戳;PEXPIREAT <key> <timestamp>
:将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳。
2.2.2 保存过期时间
redisDb 结构中的 expires
字典保存了数据库中所有键的过期时间,这个字典被称为过期字典。
过期字典的键是一个指针,指针指向键空间中的某个键对象(也就是某个数据库键);
过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间,是一个毫秒精度的UNIX时间戳。
typedef struct redisDb {
//过期字典
dict *expires;
}
2.2.3 移除过期时间
PERSIST xxx
命令可以移除一个键的过期时间,该命令在过期字典中查找给定的键,并解除键和值在过期字典中的关联。
三、过期键的删除策略
Redis 中包含三种过期键的删除策略:定时删除、惰性删除、定期删除
3.1 定时删除
在设置键的过期时间的同时,创建一个定时器 (timer),让定时器的过期时间来临时,能够立即执行对键的删除操作。
- 优点:
定时删除对内存是最友好的,使得过期的键能够尽快被删除,并且释放掉过期键所占用的内存; - 缺点:
(1) 对于CPU时间最不友好,在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分的CPU时间。在内存不紧张但是CPU时间非常紧张的情况下,会对服务器的响应时间和吞吐量造成影响。
(2) 创建一个定时器需要用到Redis服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效处理大量时间事件。
3.2 惰性删除
放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键,否则返回该键。
- 优点:
对于CPU时间是最友好的,只有在取出键的时候才会检查键是否过期; - 缺点:
对内存不友好,会造成大量过期无用的键占用内存,如果一直得不到回收就会造成内存泄漏。
3.3 定期删除
是定时删除和惰性删除的整合和折中。
定期删除每隔一段时间执行一次删除过期键的操作,并且通过限制删除操作的执行时长和频率来减少删除操作对于CPU 时间的影响,同时减少了因为过期键带来的内存占用。
四、Redis的过期键删除策略
Redis采用惰性删除和定期删除策略来实现过期键的删除
4.1 惰性删除策略的实现
惰性删除策略由 db.c/ expireIfNeeded
函数来实现,所有读写数据库的Redis命令在执行前都会调用 expireIfNeeded
函数对输入的键进行检查。如果检查发现输入的键已经过期,那么会将输入的键从数据库中删除;否则什么也不做,继续执行命令。总的来说 expireIfNeeded
函数相当于一个过滤器,在真正的命令执行前过滤掉过期的输入键,从而避免命令接触到过期键。
4.2 定期删除策略的实现
Redis 定期删除策略是通过函数 redis.c/ activeExpireCycle
来实现的,每当 Redis 服务器的周期性操作 redis.c/ serverCron
函数执行时,activeExpireCycle
函数就会被调用,在规定时间内分多次遍历服务器中的各个数据库,从数据库的过期字典中随机检查一部分键的过期时间,并删除其中过期的键。
activeExpireCycle 函数伪代码:
函数实现时定义了默认每次检查数据库的数量为16,默认每个数据库检查键的数量为20,同时定义了一个 全局变量current_db
来记录当前函数的检查进度,并在下一次 activeExpireCycle
函数调用时接着上一次的进度进行处理。并且随着函数的不断调用执行,当所有数据库检查完毕后将 current_db
值重置为0,再开始进行新一轮检查。
五、AOF、RDB持久化和复制功能对过期键的处理
5.1 生成RDB文件
在执行 SAVE 或者 BGSAVE 命令创建一个新的 RDB文件时,程序会对数据库中的键进行检查,已过期的键不会保存到新创建的 RDB 文件中。因此数据库中包含过期键不会对生成新RDB文件造成影响。
5.2 载入RDB文件
在启动Redis服务器时会恢复数据,载入 RDB文件,但是对于主从服务器处理过期键的策略不同:
- 如果是主服务器模式运行,载入时程序会检查文件中保存的键,未过期的键会被载入到数据库中,而过期键则会被忽略,所以对于主服务器没有影响;
- 如果时从服务器模式运行,载入时不管过期与否都会保存所有的键。但是因为主从服务器在进行数据同步时从服务器的数据库会被清空,所以也不会受到影响。
5.3 AOF文件写入
如果数据库中的某个键已经过期但是还没有被删除,那么AOF文件会照常记录。但是当过期键被惰性删除或者定期删除时,程序会向AOF文件追加一条DEL命令,来显式地记录该键已经被删除。
比如说客户端使用 GET message 命令试图访问过期的键 message,那么服务器会执行以下三个动作:
- 从数据库中删除 message键;
- 追加一条 DEL message 命令到 AOF文件中;
- 返回空结果给客户端。
5.4 AOF重写
和RDB类似,在执行AOF重写过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到结果AOF文件中。
5.5 复制
主从模式下,从服务器的过期键删除动作由主服务器控制,这样才能保证主从服务器的数据的一致性:
- 主服务器在删除一个过期键后会向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键;
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会删除,而是继续像处理未过期键一样来处理过期键。
- 从服务器只有在接收主服务器发来的DEL命令后,才会删除过期键。
六、数据库通知
6.1 通知分类
数据库通知能够实现客户端订阅给定频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
通知分为两种,一种是关注 "某个键执行了什么命令",叫做键空间通知;另外一种是 "某个命令被什么键执行了",叫做键事件通知。
SUBSCRIBE __keyspace@0__:message
:针对键message的键空间通知;
SUBSCRIBE __keyevent@0__:del
:针对命令del的键事件通知;
6.2 通知发送
发送数据库通知的功能是由 notify.c/notifyKeyspaceEvent
函数实现的,其中服务器配置的 notify-keyspace-events
选项决定了服务器所发送通知的类型,可以配置的类型如下:
- 服务器发送所有类型的键空间通知和键事件通知,设置为 AKE;
- 服务器发送所有类型的键空间通知,设置为AK;
- 服务器发送所有类型的键事件通知,设置为AE;
函数 notifyKeyspaceEvent(int type, char *event, robj *key, int dbid)
,type指明发送的通知类型,event指明事件名称,key指明产生事件的键,dbid指明产生事件的数据库号码。