1、服务器中的数据库
redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构体,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构都代表一个数据库。
struct redisServer {
redisDb *db; // 一个数组,保存着服务器中所有的数据库
int dbnum; // 服务器的数据库数量
}
在初始化服务器的时候 ,创建多少个数据库由dbnum决定,而dbnum由服务器配置的database决定的。
2、切换数据库
客户端可以执行SELECT命令来切换数据库。
在服务端内部,客户端状态redisClient结构体中的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针
struct redisClient {
redisDb *db ; //记录客户端当前正在使用的数据库
}
通过修改db指针,从而实现切换数据库,这也是SELECT的原理。
3、数据库实现
struct redisDb {
dict *dict; // 数据库键空间,保存着数据库中的所有键值对
}
键空间和用户所见的数据库是直接对应的:
1、键空间的键就是数据库的键,每个键都是一个字符串对象
2、键空间的值也是数据库的值,每个值可以是string、list、set、zset、hash中任意对象的一种。
3.1、 读取键空间时的维护操作
当对redis数据库读写的时候,除了会执行指定的命令外,还会执行一些维护操作,包括:
1、读写一个键后,服务器会根据键是否存在,更新hit和miss次数,更新键的命中率。可以通过INFO status 命令的keyspaces_hits 和 keyspaces_misses查看。
2、读写一个键之后,会更新redisObject的lru时间。
3、如果发现一个键已经过期,那么服务器会删除这个过期键(惰性删除)。
4、如果有客户端WATCH命令监视某个键,那么服务器对这个在对被监视的键进行修改之后,会标记这个键为脏(dirty),从而让事务程序注意到这个键已经被修改了。
5、服务器每修改一个键之后,都会对脏(dirty)键计数器的值加1,这个计数器会触发对数据库的持久化以及复制操作。
4、设置键的过期时间
4.1、如何设置
通过 EXPIRE 、 PEXPIRE 、 EXPIREAT 和 PEXPIREAT 四个命令, 客户端可以给某个存在的键设置过期时间, 当键的过期时间到达时, 键就不再可用。(实际上,都是通过使用PEXPIZREAT命令来实现的)
命令 TTL 和 PTTL 则用于返回给定键距离过期还有多长时间:
redis> SETEX key 10086 value
OK
redis>TTL key
(integer) 10082
redis> PTTL key
(integer) 10068998
4.2、保存过期时间
redisDb结构中又一个字典,这个字典保存了数据库中所有键的过期时间, 也就是所谓的过期字典。
struct redisDb {
dict *expires; // 过期字典,保存着键的过期时间
}
过期字典的键是一个指针,这个指针指向键空间的某个键对象。
过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间。
注意:为了展示的方便, 图中重复出现了两次 number 键和 book 键。 在实际中, 键空间字典的键和过期时间字典的键都指向同一个字符串对象, 所以不会浪费任何空间
4.3、删除过期时间
使用PERSIST命令删除一个键的过期时间。
PERSIST命令和PEXPIREST是反操作的, PERSIST命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典的关联。
4.4、过期键的判定
两个步骤:
1、检查给定键是否存在于过期字典,如果存在,那么取得键的过期时间。
2、检查当前UNIX时间戳是否大于键的过期时间,如果是的话,那么键已经过期。否则的话,键未过期。
5、过期删除策略
过期字典中的键如果过期了,什么时候会被删除呢?
定时删除:设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
惰性删除:放任过期键不管,当真正使用的时候查看是否过期,如果过期删除,不过期返回该键。
定期删除:每隔一段时间,程序就会对数据库进行一次检查,删除里面的过期键。至于删除多少,检查多少个数据库,由算法决定。
5.1、定时删除
定时策略对内存是友好的。通过使用定时器,定时删除策略可以保证过期键尽快被删除。
但是,定时删除策略的缺点很明显,他对CPU时间是很不友好的。因为在过期键比较多的情况下,删除过期键这一行为会占用很大一部分CPU时间,在内存不紧张但CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器的相应时间和吞吐量造成影响。
另外,创建一个定时器需要用到redis服务器中的时间事件,而当前时间事件的实现方式——无序链表,查找一个事件的时间复杂度为O(N),并不能高效地处理大量时间事件。
因此,定时删除策略一般不实用。
5.2、惰性删除
惰性删除对CPU是最友好的,这个策略不会在删除其他无关的过期键上花费任何CPU事件。
惰性删除的缺点是对内存是最不友好的:如果一个键过期了,而这个键不再使用,将会永远被保存在内存中。
如果仅使用惰性删除策略,很有可能会造成内存泄漏,很多过期键永远没有被访问到。
5.3、定期删除
定时删除太占用CPU时间,惰性删除浪费太多内存。定期删除策略算是一种折中。
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
定期删除策略的难点始载于如何确定删除操作的时长和频率。
如果删除操作太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,CPU占用时间就会过多。
如果删除操作执行的太少,或者执行的太短,就会和惰性删除没啥区别,出现浪费内存的情况。
5.4、redis的过期键删除策略
redis采用了两种集合,惰性删除加上定期删除。
5.4.1、redis惰性删除
redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查,这个函数就相当于一个过滤器,过滤掉过期的输入键,从而避免命令接触过期键。
5.4.2、redis定期删除
redis的定期删除策略由redis.c/activeExpireCycle函数实现,每当redis的服务器周期性操作redis.c/serverCron(配置文件中hz制定具体执行频率)函数执行时,activeExpireCycle就会执行。
它在规定的时间内,分多次遍历服务器的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度继续执行。随着activeExpireCycle函数的不断执行,服务器的所有数据都会被检查一边,这时候activeExpireCycle会把current_db设置为0,重新开始。
6、AOF、RDB和复制功能对过期键的处理
6.1、生成RDB文件
执行SAVE或者BGSAVE命令创建一个新的RDB文件的时候,已过期的键不会被保留到新创建的RDB文件中。
6.2、载入RDB文件
如果服务器是主服务器,载入RDB的时候,会对键进行检查,过期键会被忽略。
如果是从服务器,载入RDB的时候,文件所有的键都会被载入,不论是否过期。不过,因为主从服务器在数据进行同步的时候,从服务器的数据会被清空。所以,一般来讲,过期键载入从服务器也不会造成影响。
6.3、AOF文件写入
当服务器AOF持久化的时候,如果一个键已经过期,但是还没有被定期删除或者惰性删除,那么AOF文件不会因为这个过期键而产生任何影响。
当过期键被删除之后,程序会向AOF文件追加append一条DEL命令,来显示地记录该键已经被删除。
6.4、AOF重写
和生成RDB文件类似,在执行AOF重写的过程中,已经过期的键不易被保存到重写的AOF文件中。
6.5、复制
当服务器带有从节点时, 过期键的删除由主节点统一控制:
1、如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有从节点发送一个 DEL 命令。
2、如果服务器是从节点,那么当它碰到一个过期键的时候,不会执行删除操作,而是像处理为过期的键一样来处理过期键。只有收到主服务器发送过来的DEL命令之后,才会删除过期键。
这样做的目的:可以保证主从数据库的一致性。也正是这样,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器的复制品也会继续存在。
7、数据库通知
数据库通知是redis2.8之后新增加的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
SUBSCRIBE _ _keyspace@0_ _:message
上述命令是针对message键执行的所有命令(包括后台的Expire、DEL命令),订阅键空间。
SUBSCRIBE _ _keyevent@0_ _:del
上述命令是针对0号数据库中所有的del命令,订阅键事件。
但是如果真的想让服务器可以订阅键空间和键事件,还需要配置服务器的notify-keyspace-events。这个选项决定了服务器所发送通知的类型。