Redis数据库底层实现和过期键详解
- 1. 服务器中的数据库底层表示
- 2. select切换数据库原理
- 3. 数据库键空间
- 4. 过期时间设置和过期键删除策略
- 4.1 过期时间设置
- 4.2 保存过期时间
- 4.3 过期键的判定
- 4.4 过期键的删除策略
1. 服务器中的数据库底层表示
在src/server.h中
图1.1
图1.1中,每一个redisDb表示一个数据库,这结构里面包含了所有数据库
图1.2
图1.2中,dbnum为服务器数据库的数量,默认为16个数据库
图1.3
图1.3中表示了一个Redis的总体结构
2. select切换数据库原理
在每个连接的客户端中,都有一个默认连接的数据库
在服务器内部,redsiClient结构记录了客户端当前的状态
图2.1
图2.1中,redisDb元素指向的是redisServer.db数组的其中一个元素,示意图 如图2.2
图2.2 客户端数据库为1号数据库
select执行后,客户端的db指针就指向了redisServer的另一个db结构
3. 数据库键空间
图3.1
Redis由redisServer结构中redisDb *db元素保存所有的数据库,而每个数据库是由redisDb结构构成的.图3.1表示了每个数据库的结构,其中dict保存了当前数据库的所有键值对(Redis是一个键值对数据库)。
图3.2
图3.2 表示了一个数据库的大概结构。
其中book的hash结构有误,底层应该使用的是ziplist
4. 过期时间设置和过期键删除策略
4.1 过期时间设置
expire < key>< ttl> 将过期时间设置为ttl秒。还有几种过期时间设置这里就不介绍了
persist < key> 在过期字典的键空间中移除该key,用来移除过期时间
4.2 保存过期时间
图4.1
如上图4.1所示,redisDb结构中有一个过期时间expires键,保存了当前数据库中所有的过期键。该字典键是一个指针,指向键空间中的某一个键;该字典的值保存的是过期时间。
图4.2
图4.2便是一个具体的例子,其中过期字典的键其实是一个指针,并没有保存实际的东西,以节约空间
4.3 过期键的判定
Redis检查给定键是否存在于过期字典:如果存在,则将它的过期时间取出来,检查当前时间是否大于过期时间。大于则键已经过期。
4.4 过期键的删除策略
我们知道了过期键保存在过期字典当中,那么什么时候去删除这些键呢??毕竟这些东西是多余的,占用我们的内存。我们可以选择过期马上删,还是等会删,还是其他什么策略呢???下面我们就来介绍三种删除策略:
- 定时删除:在对键设置过期时间的同时,建立一个定时器,当过期时间到后,就立即删除该键
定时删除:
优点是:可以最快释放内存。但是当我们有多个过期键需要删除时,会占用过多的CPU,当业务紧急时,这显然是不好的。而且创建定时器还需要用到我们的时间事件(后面会讲)—而时间事件是一个无序链表,每次查找需要O(N)的时间复杂度全 - 惰性删除:不管键有没有过期。只有当使用时,每次都去过期字典里找,如果过期就删除该键。没有过期就返回该键
惰性删除:
对于CPU是友好的,但是对内存要求很大。这种策略只有在取出键时才会删除该键,并且每次只有当前处理的键删除,对CPU比较友好。然而如果对一些键我们很久没有使用,那么这些键永久也不可能删除,可能会造成内存泄露 - 定期删除:每隔一段固定的时间,就到过期字典里删除过期的键。要删除多少键检查多少个数据则由具体算法决定
定期删除:
每隔一段时间执行一次过期键的删除操作,并限制删除操作的时长和频率减少对CPU的影响。并且通过定期删除键可以减少内存的消耗。算是前面两种策略的折中
Redis采用的过期删除策略
Redis采用惰性删除和定期删除两种方式配合,在合理使用CPU时间和内存之间取得平衡
惰性删除策略实现:
所有读写命令在执行之前都会调用函数对输入键进行检查,过期则删除,实际流程如图4.3所示:
图4.3 命令调用expireIfNeeded来删除过期键
定期删除策略的实现:
Redis服务器周期性的执行删除函数,它的规定的时间内多次遍历各个数据库,从过期字典中随机检查一部分的过期时间,并删除过期键。
全局变量current_db会记录当前检查的进度,下一次检查则从这里开始,全部数据库检查一遍后又从0开始检查
注意:这里的定期删除都是遍历各个数据库,并从数据库中随机抽取一定数量的键来检查,且有时间限制,超过时间就不检查了
上面也许说的不够清楚,下面的伪代码可以很好的解释这个过程:
#默认每次检查的数据库数量 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
至于这里为什么要随机呢??
我想的话是因为如果不是随机的话,而是全部遍历的话,那么就有可能很长时间内都只在前面的数据库里,遍历全部数据库的时间则会变得很长,就有可能增长很快的库会很快占满内存。当然随机的话也有一定的坏处
:比如说有一些键可能运气比较好,永远也不能删除,导致内存泄露,这就要我们后面的知识了!!!