一、数据库键空间
Redis是一个键值对数据库服务器,服务器的每个数据库都由一个redis.h/redisDb结构表示,其中redisDb结构的dict字典保存了数据库的所有键值对,我们将这个字典称为键空间。
键空间和用户所见的数据库时直接对应的:
- 键空间的键就是数据库的键,每个键都是一个字符串对象
- 键空间的值也就是数据库的值,每个值是任意一种Redis对象(五种数据类型)
redisDb结构的 expires 字典保存了数据库中所有键的过期时间,我们称为过期字典:
- 过期字典的键是一个指针,这个指针指向键空间的某个键对象
- 过期字典的值是一个long类型的整数,这个整数保存了键所指向的数据库键过期时间(一个毫秒精度的UNIX时间戳)
注意:在实际中,键空间的键和过期字典的键都指向同一个键对象,所以不会出现任何重复的对象,也不会浪费任何空间
1.1 过期键判定
计算并返回剩余生存时间:TTL和PTTL两个命令都是通过计算键的过期时间和当前时间之间的差来实现
- 检查给定键是否存在于过期字典,如果存在,那么取得键的过期时间
- 检查当前UNIX时间戳是否大于键的过期时间,如果是的话,那么键已过期
二、过期键删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作
- 惰性删除:放任键过期不管,但每次从键空间中获取键时,都检查取得的键是否过期, 如果过期的话,就删除该键;如果没有过期,就返回该键
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键
服务器实际使用的是惰性删除和定期删除
2.1 惰性删除实现
过期键的惰性删除策略有 expireIfNeeded 函数实现,所有读写数据库的Redis命令在执行之前都会调用 expireIfNeeded 函数对输入键进行检查
- 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除
- 如果输入键未过期,那么expireIfNeeded函数不做动作
2.2 定期删除策略实现
Redis服务器周期性操作 serverCron 函数执行时,activeExpireCycle函数就会被调用。activeExpireCycle 函数分多次遍历服务器中的各个数据库(一次可能遍历不完所有数据库),从数据库的过期字典中随机检查一部分键的过期时间,并删除其中的过期键。
# 默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16
# 默认每个数据检查的键数量
DEFAULT_KEY_NUMBERS = 20
# 全局变量,记录检查进度
current_db = 0
def activeExpireCycle():
# 初始化要检查的数据库数量
# 如果服务器的数据库数量比DEFAULT_DB_NUMBERS小
# 那么以服务器的数据库数量为准
if server.dbnum < DEFAULT_DB_NUMBERS:
db_numbers = server.dbnum
else:
db_numbers = DEFAULT_DB_NUMBERS
# 遍历各个数据库
for i in range(db_numbers):
# 如果current_db的值等于服务器的数据库数量
# 表示检查程序已经遍历了服务器的所有数据库一次
# 将current_db重置为0,开始新一轮遍历
if current_db == server.dbnum:
current_db = 0
# 获取当前要处理的数据库
redisDb = server.db[current_db]
# 将数据库索引增1,指向下一个要处理的数据库
current_db += 1
# 检查数据库键
for j in range(DEFAULT_KEY_NUMBERS):
# 如果数据库中没有过期键,那么跳过这个数据库
if redisDb.expires.size() == 0: break
# 随机获取一个带有过期时间的键
key_with_ttl = redisDb.expires.get_random_key()
# 检查键是否过期,如果过期就删除
if is_expired(key_with_ttl):
delete_key(key_with_ttl)
# 已经达到本次删除操作时间上限,停止处理
if reach_time_limit(): return
- current_db用于记录activeExpireCycle函数检查的进度,下一次调用activeExpireCycle时接着上一次的处理进度进行处理
- 随着activeExpireCycle函数不断执行,服务器中所有的数据库都会被检查一遍,这时将current_db变量置为0,然后开始新的一轮检查工作
三、AOF、RDB、复制 对过期键的处理
3.1 RDB文件
生成RDB文件
执行 save 和 bgsave 命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中
载入RDB文件
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入
- 如果服务器以主服务器模式运行,那么载入RDB文件时,程序会对文件中保存的键进行检查(生成RDB文件时有的键可能没过期,但Redis重新启动的时候,这些键到时间过期了),未过期的键会被载入到数据库中,而过期键会被忽略
- 如果服务器以从服务器模式运行,那么载入RDB文件时,文件中保存的所有键不论是否过期,都会被载入到数据库。(主服务器进行数据同步时,从服务器的数据库就会被清空)
3.2 AOF文件
AOF文件写入
- 如果数据库的某个键已经过期,但他还没被惰性删除或者定期删除,那么AOF不会因为过期键而更改
- 当过期键被惰性删除或者定期删除之后,向AOF文件追加一条DEL命令,来显式记录该键已被删除
AOF重写
程序会对数据库的键进行检查,已经过期的键不会被保存到重写后的AOF文件中
3.3 复制
从服务器的过期键删除动作由主服务器控制
- 主服务器在删除一个过期键之后,会显式向所有从服务器发送DEL命令,通知从服务器删除这个过期键
- 从服务器在直接客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是像处理未过期的键一样处理(例如客户端发送给从服务器 get 过期键的命令,从服务器还是会返回过期键的值给客户端)
- 从服务器只有在收到主服务器发来的DEL命令,才会删除过期键
四、逐出算法
Redis使用内存存储数据,在执行每一个命令前,会调用freeMemoryIfNeeded()检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间。清理数据的策略称为逐出算法。
注意:逐出数据的过程不是100%能够清理出足够的可使用的内存空间,如果不成功则反复执行。当对所 有数据尝试完毕后,如果不能达到内存清理的要求,将出现错误信息。
# 最大空用内存,默认时全用
maxmemory 256mb
# 每次选取待删除数据的个数
maxmemory-samples 100
# 删除策略(逐出算法)
# 可选操作
# 检查易失数据(有过期时间的数据)
# volatile-lru: 挑选最近最少使用的数据淘汰
# volatile-lfu: 挑选最近使用次数最少的数据淘汰
# volatile-ttl: 按将要过期的数据淘汰
# volatile-random: 随机淘汰有过期时间的数据
# 检测全库数据(所有的key,不管有没有过期时间)
# allkeys-lru
# allkeys-lfu
# allkeys-random
# 放弃数据驱逐
# no-enviction
maxmemory-policy volatile-lru