数据库及过期策略
1. 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:
struct redisServer{
// ...
// 一个数组,保存服务器中的所有数据库
redisDb *db;
// ...
}
在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库:
struct redisServer{
// ...
// 服务器的数据库数量
int dbnum;
// ...
};
dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,Redis默认会创建16个数据库。
Redis服务器数据库示例2. 切换数据库
Redis客户端的默认数据库为0号数据库,客户端可以通过执行SELECT命令切换目标数据库。
SELECT 2 // 切换到2号数据库
客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针。
typedef struct redisClient{
// ...
// 记录客户端当前正在使用的数据库
redisDb *db;
// ...
} redisClient;
redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元素就是客户端的目标数据库。
如下图,反应了客户端与服务器端数据库的关系。
客户端的目标数据库3. 数据库键空间
服务器中的每个数据库都由一个redis.h/redisDb表示。
typedef struct redisDb{
// ...
// 数据库键空间,保存数据库中所有的键值对
dict *dict;
// ...
} redisDb
3.1 添加新键
添加一个新键值对到数据库,实际上就是将一个新键值对添加到键空间字典里面,其中键为字符串对象,值为任意一种类型的Redis对象。
3.2 删除键
删除数据库中的一个键,实际上就是在键空间里面删除键所对应的键值对对象。
3.3 更新键
对一个数据库键进行更新,实际上就是对键空间里面键所对应的值对象进行更新。
3.4 对键取值
对一个数据库键进行取值,实际上就是在键空间中取出键所对应的值对象。
3.5 其他键空间操作
还有很多针对数据库本身的Redis命令,也是通过对键空间进行处理来完成的,如:
- FLUSHDB,通过删除键空间中所有键值对来实现
- RANDOMKEY,通过在键空间随机返回一个键来实现
- DBSIZE,通过返回键空间中包含的键值对的数量来实现
- EXISTS、RENAME、KEYS等命令也是通过对键空间操作实现的
3.6 读写键空间时的维护操作
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,其中包括:
- 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性查看。
- 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间。
- 如果服务器在读取一个键时发现这个键已经过期,那么服务器会先删除这个过期键,再执行余下的其他操作。
- 如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
- 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化及复制操作。
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。
4. 设置键的生存时间或过期时间
- EXPIRE、PEXPIRE:以秒、毫秒精度为键设置生存时间(Time To Live,TTL)。设置值为几秒、几毫秒。
- EXPIREAT、PEXPIREAT:以秒、毫秒精度为键设置生存时间(Time To Live,TTL)。设置值为Unix时间戳。
- SETEX:可以在设置一个字符串键的同时为键设置过期时间。
- TTL、PTTL:查看键的剩余生存时间。
4.1 保存过期时间
typedef struct redisDb{
// ...
// 过期字典,保存键的过期时间
dict *expires;
} redisDb;
redisDb中的expires字典保存了数据库中所有键的过期时间,称这个字典为过期字典:
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象。
- 过期字典的值是一个long long类型的整数,一个毫秒精度的Unix时间戳。
下面是一个带有过期字典的数据库示例。
带有过期字典的数据库示例上图中键空间、过期字典中重复出现两次alphabat、book键对象。在实际中,键空间的键和过期字典的键都指向同一个键对象,不会出现任何重复对象,不会浪费任何空间。
4.2 移除过期时间
PERSIST:移除一个键的过期时间。
PERSIST在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
4.3 计算并返回剩余生存时间
TTL、PTTL两个命令都是通过计算键的过期时间和当前时间之间的差来实现的。
4.4 过期键的判定
Redis检查键是否过期的方法:先取得键的过期时间,检查当前UNIX时间戳是否大于键的过期时间,如果是,那么键已经过期;否则,键未过期。
另一种检查键过期的方法是:使用TTL、PTTL命令,判断返回的值是否大于等于0。但是Redis并未使用此策略,因为直接访问字典比执行一个命令稍微快一些。
4.5 过期键删除策略
过期键有3种不同的删除策略:
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除策略。(Redis实际未使用该策略)
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期,就删除该键,如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
上面第1种、第3种为主动删除策略,第2种为被动删除策略。
4.5.1 定时删除(Redis未使用此策略)
- 定时删除策略对内存是友好的:可以保证过期键会尽快被删除,并释放过期键所占用的内存。
- 定时删除策略对CPU时间是不友好的:在过期键比较多的情况下,删除过期键可能会占用相当一部分CPU时间。
创建一个定时器,需要用到Redis服务器中的时间事件,而Redis当前时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效地处理大量时间事件。
因此,要让服务器创建大量的定时器,从而实现定时删除策略,在现阶段来说并不现实。
4.5.2 惰性删除
- 惰性删除策略对CPU时间是友好的:程序只会在取出键时才对键进行过期检查。
- 惰性删除策略对内存是不友好的:如果一个键已经过期,只要这个过期键不被删除,所占用的内存就不会释放。
4.5.3 定期删除
定期删除策略是前两种策略的一种整合和折中:
- 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
- 定期删除策略也能有效地减少因为过期键带来的内存浪费。
定期删除策略的难点是确定删除操作执行的时长和频率。
5. Redis的过期键删除策略
Redis服务器实际使用的是惰性删除、定期删除两种策略。
5.1 惰性删除策略的实现
过期键的惰性删除策略由db.c/expireIfNeeded函数实现。
所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded对输入键进行检查。
5.2 定期删除策略的实现
过期键的定期删除策略由redis.c/activeExpireCycle函数实现。
每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。在遍历过程中,全局变量current_db会记录activeExpireCycle函数检查的进度。
6. RDB、AOF和复制功能对过期键的处理
下面介绍RDB持久化功能、AOF持久化功能、复制功能是如何处理数据库中的过期键的。
6.1 生成RDB文件
在执行SAVE、BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
6.2 载入RDB文件
在启动Redis服务器时,如果服务器启动了RDB功能,那么服务器将对RDB文件进行载入:
- 如果服务器以"主服务器模式"运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入数据库中,过期键会被忽略。
- 如果服务器以"从服务器模式"运行,那么在载入RDB文件时,文件中保存的所有键,无论是否过期,都会被载入数据库。不过,因为主从服务器在进行数据同步时,"从服务器"的数据库会被清空。
6.3 AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或定期删除后,程序会向AOF文件追加一条DEL命令,来显式记录该键已被删除。
6.4 AOF重写
在执行AOF重写时,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
6.5 复制
当服务器运行在复制模式下,"从服务器"的过期键删除动作由"主服务器"控制:
- 主服务器在删除一个过期键之后,会显式向所有"从服务器"发送一个DEL命令,告知"从服务器"删除这个过期键。
- "从服务器"在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
- "从服务器"只有在接到"主服务器"发来DEL命令后,才会删除过期键。
这种由"主服务器"控制"从服务器"统一删除过期键,可以保证主从服务器数据的一致性。
7. 数据库通知
这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
关注"某个键执行了什么命令"的通知称为"键空间通知"。
SUBSCRIBE _ _keyspace@0_ _:message
获取0号数据库中针对message键执行的所有命令。
关注"某个命令被什么键执行了"的通知称为"键事件通知"。
SUBSCRIBE _ _keyevent@0_ _:del
获取0号数据库中所有执行DEL命令的键。
notify-keyspace-events决定了服务器发送通知的类型:
- 想让服务器发送所有类型的键空间通知和键事件通知,可将选项值设为AKE。
- 想让服务器发送所有类型的键空间通知,可将选项值设为AK。
- 想让服务器发送所有类型的键事件通知,可将选项值设为AE。
- 想让服务器只发送和字符串有关的键空间通知,可将选项值设为K$。
- 想让服务器只发送和列表键有关的键事件通知,可将选项值设为El。
8.1 发送通知
发送数据库通知的功能由notify.c/notifyKeyspaceEvent函数实现。
void notifyKeyspaceEvent(int type, char *event, robj *key,int dbid);
type参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是notifyKeyspaceEvent设定的通知类型,从而决定是否发送通知。
events、key、dbid分别是事件的名称、产生事件的键、产生事件的数据库号码。
网友评论