> 文档中心 > Redis数据库底层实现和过期键详解

Redis数据库底层实现和过期键详解

Redis数据库底层实现和过期键详解

  • 1. 服务器中的数据库底层表示
  • 2. select切换数据库原理
  • 3. 数据库键空间
  • 4. 过期时间设置和过期键删除策略
    • 4.1 过期时间设置
    • 4.2 保存过期时间
    • 4.3 过期键的判定
    • 4.4 过期键的删除策略

1. 服务器中的数据库底层表示

在src/server.h中
Redis数据库底层实现和过期键详解

图1.1

图1.1中,每一个redisDb表示一个数据库,这结构里面包含了所有数据库
Redis数据库底层实现和过期键详解

图1.2 图1.2中,dbnum为服务器数据库的数量,默认为16个数据库

Redis数据库底层实现和过期键详解

图1.3 图1.3中表示了一个Redis的总体结构

2. select切换数据库原理

在每个连接的客户端中,都有一个默认连接的数据库
在服务器内部,redsiClient结构记录了客户端当前的状态
Redis数据库底层实现和过期键详解

图2.1

图2.1中,redisDb元素指向的是redisServer.db数组的其中一个元素,示意图 如图2.2
Redis数据库底层实现和过期键详解

图2.2 客户端数据库为1号数据库

select执行后,客户端的db指针就指向了redisServer的另一个db结构

3. 数据库键空间

Redis数据库底层实现和过期键详解

图3.1 Redis由redisServer结构中redisDb *db元素保存所有的数据库,而每个数据库是由redisDb结构构成的.图3.1表示了每个数据库的结构,其中dict保存了当前数据库的所有键值对(Redis是一个键值对数据库)。

Redis数据库底层实现和过期键详解

图3.2 图3.2 表示了一个数据库的大概结构。

其中book的hash结构有误,底层应该使用的是ziplist

4. 过期时间设置和过期键删除策略

4.1 过期时间设置

expire 将过期时间设置为ttl秒。还有几种过期时间设置这里就不介绍了

persist 在过期字典的键空间中移除该key,用来移除过期时间

4.2 保存过期时间

Redis数据库底层实现和过期键详解

图4.1

如上图4.1所示,redisDb结构中有一个过期时间expires键,保存了当前数据库中所有的过期键。该字典键是一个指针,指向键空间中的某一个键;该字典的值保存的是过期时间。

Redis数据库底层实现和过期键详解

图4.2 图4.2便是一个具体的例子,其中过期字典的键其实是一个指针,并没有保存实际的东西,以节约空间

4.3 过期键的判定

Redis检查给定键是否存在于过期字典:如果存在,则将它的过期时间取出来,检查当前时间是否大于过期时间。大于则键已经过期。

4.4 过期键的删除策略

我们知道了过期键保存在过期字典当中,那么什么时候去删除这些键呢??毕竟这些东西是多余的,占用我们的内存。我们可以选择过期马上删,还是等会删,还是其他什么策略呢???下面我们就来介绍三种删除策略:

  • 定时删除:在对键设置过期时间的同时,建立一个定时器,当过期时间到后,就立即删除该键
    定时删除:优点是:可以最快释放内存。但是当我们有多个过期键需要删除时,会占用过多的CPU,当业务紧急时,这显然是不好的。而且创建定时器还需要用到我们的时间事件(后面会讲)—而时间事件是一个无序链表,每次查找需要O(N)的时间复杂度全

  • 惰性删除:不管键有没有过期。只有当使用时,每次都去过期字典里找,如果过期就删除该键。没有过期就返回该键
    惰性删除:对于CPU是友好的,但是对内存要求很大。这种策略只有在取出键时才会删除该键,并且每次只有当前处理的键删除,对CPU比较友好。然而如果对一些键我们很久没有使用,那么这些键永久也不可能删除,可能会造成内存泄露

  • 定期删除:每隔一段固定的时间,就到过期字典里删除过期的键。要删除多少键检查多少个数据则由具体算法决定
    定期删除:每隔一段时间执行一次过期键的删除操作,并限制删除操作的时长和频率减少对CPU的影响。并且通过定期删除键可以减少内存的消耗。算是前面两种策略的折中

Redis采用的过期删除策略
Redis采用惰性删除和定期删除两种方式配合,在合理使用CPU时间和内存之间取得平衡

惰性删除策略实现:所有读写命令在执行之前都会调用函数对输入键进行检查,过期则删除,实际流程如图4.3所示:
Redis数据库底层实现和过期键详解

图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

至于这里为什么要随机呢??我想的话是因为如果不是随机的话,而是全部遍历的话,那么就有可能很长时间内都只在前面的数据库里,遍历全部数据库的时间则会变得很长,就有可能增长很快的库会很快占满内存。
当然随机的话也有一定的坏处:比如说有一些键可能运气比较好,永远也不能删除,导致内存泄露,这就要我们后面的知识了!!!