8 数据库
8.1 服务器中的数据库
struct redisServer{
//...
//一个数组,保存所有数据库
redisDb *db;
//服务器数据库数量
int dbnum;
//...
}
- Redis服务器将所有数据库都保存在服务器状态redisServer结构的db数组中,db数组中每个项都是一个redisDb结构,每个redisDb结构代表一个数据库
- dbnum属性的值由服务器配置的database选项决定,默认情况为16.所有redis服务器默认创建16个数据库
8.2 切换数据库
默认情况下使用的是0号数据库,可以使用select命令进行数据库切换
服务器内部,redisClient结构的db属性指向正在使用的数据库
typedef struct redisClient{
//...
//指向正在使用的数据库(redisServer结构中db数组的其中一个)
redisDb *db;
//...
}redisClient;
select命令的原理就是改变db指向的数据库。
注:在其他语言客服端使用redis时,不会有redis-cli一直显示目标数据库的方法,所以在对数据库进行操作,最好都先select到目标数据库,尤其是使用flushdb这样的命令的时候。
8.3 数据库键空间
Redis是一个键值对数据库服务器,服务器中每个数据库都是一个redisDb结构,而在redisDb结构中有一个dict字典保存了数据库中所有键值对,我们将其称为字典
typedef struct redisDb{
//...
//数据库键空间,保存了数据库中所有的键值对
dict *dict;
//...
}redisDb;
键空间与数据库是直接对应的
- dict中每个键是要数据库要保存的键值对中的键,是一个字符串对象
- dict中每个值,为键所对应的值,可以是字符串对象,列表对象,哈希表对象,集合对象和有序集合对象中任一种。
8.3.1 增删改查键值对
- 新增键值对就是往对应数据库的dict中添加一组新的键值对保存起来
- 删除就是直接在对应数据库的dict中清除对应的键值对
- 修改就是直接在对应数据库的dict中修改键对应的值
- 查找就是直接在对应数据库的dict中返回指定键对应的值
8.3.2 其他键空间操作
除了增删改查外,其他针对数据库的操作,例如
- FLUSHDB是删除数据库中所有的键值对
- RANDOMKEY是随机返回一个键
其他DBSIZE,EXISTS,RENAME,KEYS都是对键空间进行操作
8.3.3 读写键空间时的维护操作
- 读取一个键时(删改查都会读键),服务器会根据键是否存在来更新服务器的键空间命中或键空间不命中次数,这两个值可以在INFO stats命令的keyspace_hits和keyspace_misses属性中查看。
- 读取键时,会更新键的LRU(最后一次使用)时间,用于计算键的闲置时间
- 若服务器发现读取的键已经过期,会先删除这个键,再进行下一步操作
- 若使用WATCH监视某个键,且某个键被修改过,那么这个键会被标记为脏,从而让事务程序知道这个键被修改过。
- 服务器每修改一个键后,都会对脏键计数器加1,这个计数器会触发服务器的持久化和复制操作。
- 如果服务器开启数据库通知功能,那在对键进行修改后,服务器会按配置发送相应的数据库通知
8.4 设置键的生存时间
通过EXPIPE或者PEXPIRE命令,客服端可以以秒或者毫秒精度为数据库中某个键设置生存时间,在经过指定描述或者毫秒以后,服务器会自动删除生存时间为0的键
注:SETEX命令可以在新增键的同时为键设置过期时间,其只能用于字符串键(类型限定),设置过期时间的原理和EXPIRE一样
TTL与PTTL可以以秒或毫秒为单位查看键的生存时间,-1表示永不过期,-2表示该键不存在
8.4.1 设置过期时间
- EXPIRE key ttl :键还有ttl秒的生存时间
- PEXPIRE key ttl :键还有ttl毫秒的生存时间
- EXPIREAT key timestamp :键在timestamp指定的秒时间戳过期
- PEXPIREAT key timestamp :键在timestamp指定的毫秒时间戳过期
上诉四个命令其实都是通过PEXPIREAT来完成的,设置一个毫秒时间戳为过期时间。指定键在这个时间过期。
8.4.2 保存过期时间
redisDb结构中除了有保存数据库键值对的字典dict,还有另一个字典expire,这个字典的键为键值对的键,值为键过期的毫秒时间戳。这个时间戳是一个long long类型的整数。
typedef struct redisDb{
//...
//过期字典,保存键的过期时间
dict *expire;
//...
}redisDb;
向数据库执行PEXPOIREAT命令(所有设置过期时间的命令都会转换为这个)会为expire字典增加个键值对,保存了对应键的过期时间。
8.4.3 移除过期时间
PERSIST命令会为键解除过期时间的绑定,让其变成永不过期的状态
8.4.4 剩余时间的返回
使用TTL或者PTTL返回键的剩余时间,其实就是利用expire中过期时间戳减去现在的时间戳并返回
8.4.5 过期键的判定
若在expire中存在键的过期时间戳并且这个时间戳小于当前的时间戳,则为过期,否则未过期。
8.5 过期键的删除策略
删除策略有三种
- 定时删除:设置键的过期时间戳的时候,在设置一个定时器,在键的过期时间来临时,立即执行删除操作。
- 惰性删除:放任键的过期不管,每次在访问键时,查看键是否过期,若未过期,正常操作,过期就先删除,再执行后续操作。
- 定期删除,每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键,而检查多少数据库,删除多少过期键,由算法来决定
8.5.1 定时删除
- 定时删除策略对内存是最友好的,使用定时器可以保证过期键会在第一时间被删除,并释放过期键占用的内存。
- 但它对CPU时间最不友好,在过期键较多的时候,删除过期键的行为会占用相当一部分CPU时间,在内存不紧张,CPU时间紧张的时候,将CPU时间用于删除无用的键,会很大的影响服务器的相应时间和吞吐量。
- 创建定时器需要用到Redis服务器的时间事件,时间事件的实现方式是无序列表,查找一个事件的时间复杂度为O(N)——并不能高效处理大量时间事件
8.5.2 惰性删除
- 惰性删除对CPU时间是最友好的,程序只有在读该键时才会进行过期检查,不会再删除其他无关过期键花费任何CPU时间。
- 但惰性删除对内存最不友好,大量无关键在不读取的情况下,一直不删除,占用着内存,如果某些键再也不会被读到,那么久永远不会被删除,这可以被视为一种内存泄漏的现象。
8.5.3 定期删除
从上可知,定时删除占用太多CPU时间,惰性删除浪费太多内存,定期删除是前两种策略的整合和折中
- 定期删除每隔一段时间执行一次删除过期键的操作,并且现在删除操作执行的时长与频率来减少对CPU时间的影响
- 通过定期删除,可以有效地减少因为过期键带来的内存浪费
- 若删除操作时长过长或频率过高定期删除策略会退化为定时删除,大量浪费CPU时间
- 若删除操作时长过短或频率过低定期删除策略会退化为惰性删除,造成内存的浪费。
8.6 Redis的过期键删除策略
Redis服务器实际使用的是惰性删除和定期删除两种策略,两种策略使得服务器能很好的在CPU时间与内存直接取得平衡。
8.6.1 惰性删除策略的实现
过期键的惰性删除策略有db.c/expireIfNeeded函数实现,所有读写数据库的命令执行之前都会调用expireIfNeeded函数对输入键进行检查
- 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库删除
- 如果输入键未过期,expireIfNeeded函数什么都不做
8.6.2 定期删除策略的实现
过期键的定时删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数被执行时,activeExpireCycle函数就会被调用,它会在规定时间内,分多次遍历服务器的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,若有键过期,就删除它。
- 函数每次运行时,都从当前数据库随机抽取一定量的键进行检查,并删除其中过期的键
- 全局变量current_db会记录当前activeExpireCycle函数检查的进度,若规定的检查时间到了,activeExpireCycle函数会停止,在下次activeExpireCycle函数执行时,会根据current_db来决定从哪个数据库开始检查。例如上次activeExpireCycle函数在检查10号数据库时停止,那么下次会从11号数据库开始检查。
- 随着activeExpireCycle函数不断进行,在所有数据库都被检查一遍后,current_db的值会被清零,又从头开始检查,如此循环。
8.7 AOF、RDB和复制功能对过期键的处理
之前的博客有对RDB和AOF的浅显描述链接
8.7.1 生成RDB文件
在生成RDB文件时,已经过期的键不会被保存到RDB文件中。
8.7.2 载入RDB文件
在启动Redis服务器时,若开启了RDB功能,则将进行RDB文件进行载入
- 若服务器以主服务器模式运行,则在载入RDB文件时,程序会对文件保存的键进行检查,未过期的键会被载入,过期的键会被忽略,所以过期键不会对RDB主服务器造成影响。
- 若服务器以从服务器模式运行,在载入RDB文件时,无论是否过期都会被载入数据库。不过由于主服务器在进行数据同步时,从服务器数据会被清空,所以对于从服务器也不会造成什么影响。
8.7.3 AOF文件写入
在启用AOF持久化模式时,若某个键已经被删除,那么在删除时,同时也会对AOF文件中写入del,那么在AOF文件就不会因为过期键而产生影响。
在程序访问到过期的键时,会执行以下操作
- 从数据库中删除该键
- 追加一条del key到AOF文件中
- 返回一个空回复
8.7.4 AOF的重写
在进行AOF文件重写时,不会对已过期键进行重写,所以数据库中的过期键不会对AOF重写造成影响。
8.7.5 复制
当服务器在复制模式下的时候,从服务器的过期键删除由主服务器控制
- 主服务器删除一个键时,会给从服务器发送del命令,告诉从服务器删除这个键。
- 从服务器执行读命令时,即使碰到过期键也不会删除它,而是像访问未过期键一样访问。
- 从服务器只有收到主服务器的del命令,才会删除键。
8.8 数据库通知
使用subscribe命令订阅针对键或者命令的操作信息,来获知键的变化。如:
subscribe __keyspace@0__:<key|command>
此外,服务器配置notify_keyspace_events选项决定了服务器发送通知的类型:
- AKE———-服务器发送所有键空间通知和键事件通知
- AK————服务器发送所有键空间通知
- AE————服务器发送所有键事件通知
- K$————服务器发送所有和字符串相关的键空间通知
- El————-服务器发送所有和列表键相关的键事件通知
发送通知。当命令被执行的时候,如果执行成功后会调用notify.c/notifyKeyspaceEvent函数发送通知。伪代码如下:
def notifyKeyspaceEvent(type,event,key dbid):
if 通知类型type不是server允许发送通知类型:
return
#发送键空间通知
if server.notify_keyspace_events & NOTIFY_KEYSPACE
#将通知发送给频道__keyspace@<dbid>__:<key>,内容为发生的事件通知<event>
chan = __keyspace@<dbid>__:<key>
pubsubPublishMessage(chanobj, eventobj);
#发送键事件通知
if server.notify_keyspace_events & NOTIFY_KEYEVENT
#将通知发送给频道__keyspace@<dbid>__:<event>,内容为发生的事件通知<key>
chan = __keyspace@<dbid>__:<event>
pubsubPublishMessage(chanobj, keyobj);