1.数据库的结构
Redis中的每个数据库,都由一个redisDb 表示,结构定义如下:
typedef struct redisDb { // 保存着数据库以整数表示的号码 int id; // 保存着数据库中的所有键值对数据 // 这个属性也被称为键空间( key space) dict *dict; // 保存着键的过期信息 dict *expires; // 实现列表阻塞原语,如 BLPOP // 在列表类型一章有详细的讨论 dict *blocking_keys; dict *ready_keys; // 用于实现 WATCH 命令 // 在事务章节有详细的讨论 dict *watched_keys; } redisDb;
其中,
dict表示Redis的存储空间,保存着数据库中的所有键值对数据。
expires保存着Redis中键的过期信息。
id 域保存着数据库的号码 ,方便一些内部程序 ,比如AOF程序,复制程序,RDB程序判定当前使用的是哪一个数据库。
1.1数据库字典
Redis是一个键值对数据库,因此本身也是一个字典:
- 字典的键是字符串对象
- 字典的值可以是字符串,列表,哈希,集合,有序集合等数据类型。
dict空间保存着redis所有键值对信息,如下图所示:

1.2键空间操作
redis数据库本身就是一个字典,所以对数据中数据进行操作的时候,相当于在字典中进行增删改查操作。
添加键:则是相当于在字典中添加一个字符串类型的键,然后对应任意redis数据类型的值。
删除键:则是相当于在字典中删除字符串类型的键,同时删除键所对应的值。
修改键:则是相当于字典中删除键对象和值对象。
2.键的过期处理
2.1 键的过期时间
redis实现了缓存最基本的一个特性:键的过期时间。
从上面数据库redisDb定义:
typedef struct redisDb { // 保存着数据库以整数表示的号码 int id; // 保存着数据库中的所有键值对数据 // 这个属性也被称为键空间( key space) dict *dict; // 保存着键的过期信息 dict *expires; // 实现列表阻塞原语,如 BLPOP // 在列表类型一章有详细的讨论 dict *blocking_keys; dict *ready_keys; // 用于实现 WATCH 命令 // 在事务章节有详细的讨论 dict *watched_keys; } redisDb;
expires保存着所有键的过期信息。expires 字典的键 是指向dict里面对应的键的指针,字典里的值对应的是键的过期时间,这个值以 long long 类型表示 。
如图所示:

我们可以看到只有键为number的键才有过期时间:123467777.也就是说这个键能够在redisDb保存时间为123467777ms。一旦过了这个时间,键值都失效了。
2.2 键过期的判断
通过 expires 字典,可以用以下步骤检查某个键是否过期 :
- 判断expires是否有这个key,如果没有,就是没有设置过期时间,不会过期。如果存在,则取出过期时间。
- 过期时间和当前时间判断,如果过期时间大于当前时间,则没有过期。否则,已过期。
流程图如下所示:

2.3过期键的处理
2.3.1 清理键方案
现在我们知道过期键存储在redisDb的位置,以及如何去判断键是否过期。那么进一步需要讨论如何清理调过期键。可能有三种解决方案:
- 定时清理:就是说在设置键的过期时间时候,创建一个定时事件,一旦到了键的过期时间,则定时事件立即启动清理过期键。
- 被动清理:当每次从dict中取key时,判断是否过期,然后进行清理,兵返回空。否则,返回键对应的值。
- 定期清理:每隔一段时间,对expires空间进行清理,清理掉已经过期的key.
2.3.2 清理方案的衡量(内存,CPU时间两个层面分析)
定时清理:可以及时清理过期键值,并释放过期键所占用的空间。但是,删除过期键值会占用大量的CPU时间,在CPU时间比较紧张的情况下,比如:进行排序。将 CPU 时间花在删除那些和当前任务无关的过期键值上,是一种低效的做法。
除此之外,redis创建定时事件的方式是:无序链表,查找的时间复杂度为O(N),对于过期键值量很大的情况下是不使用的。
被动清理:对CPU时间是非常友好的,因为只有在取键的时候才去判断过期,过期则清理,清理的是当前调用的键,不会占用CPU的时间清理其他无关的键值。但是对于内存不友好,也许有其他已经过期的键值因为不能及时清理占用内存空间。如果redis中有大量过期键值,但是键没有被调用,过期键就未得到及时清理,占用了大量内存,这对以内存为基础的Redis数据库来说,并不是个理智选择。
定期清理:弥补以上两种清理方案,每隔一段时间清理,既及时释放内存空间,又不占用大量CPU时间。
2.3.3 redis清理方法的选择
鉴于以上的分析,redis采用的清理过期键方案为:被动清理+定期清理。
- redis实现的被动清理方案:核心是expiresIfNeeded函数。所有命令在读取或者写入的时候,必须调用该函数判断过期键并删除过期键值。expiresIfNeeded作用是:判断过期键是否过期,如果过期,则删除redisDb中expires中的键相关的过期时间设置,以及删除dict中对应的键值。最后将删除命令写入到AOF文件和附属结点,写入AOF文件为了保证数据可靠性,写入附属结点,就是告诉从节点,键值失效了。
伪代码如下:
def expireIfNeeded(key): # 对过期键执行以下操作 。。。 if key.is_expired(): # 从键空间中删除键值对 db.dict.remove(key) # 删除键的过期时间 db.expires.remove(key) # 将删除命令传播到 AOF 文件和附属节点 propagateDelKeyToAofAndReplication(key)
- redis实现的定期清理方法:核心是redis.c/activeExpireCycle 函数。每当 Redis 的例行处理程序serverCron 执行时,activeExpireCycle 都会被调用 ,这个函数在规定时间内,尽可能遍历整个redisDb的expires空间,删除过期键。这里需要注意的是,定期清理有一个时间设置,在这个时间范围内,不一定遍历所有的redisDb的expires空间,也就是说不一定能够清理完所有的过期键值。这样就不会长期占用CPU时间了。
伪代码如下:
def activeExpireCycle(): # 遍历数据库(不一定能全部都遍历完,看时间是否足够) for db in server.db: # MAX_KEY_PER_DB 是一个 DB 最大能处理的 key 个数 # 它保证时间不会全部用在个别的 DB 上(避免饥饿) i = 0 while (i < MAX_KEY_PER_DB): # 数据库为空,跳出 while ,处理下个 DB if db.is_empty(): break # 随机取出一个带 TTL 的键 key_with_ttl = db.expires.get_random_key() # 检查键是否过期,如果是的话,将它删除 if is_expired(key_with_ttl): db.deleteExpiredKey(key_with_ttl) # 当执行时间到达上限,函数就返回,不再继续 # 这确保删除操作不会占用太多的 CPU 时间 if reach_time_limit(): return i += 1
综上所述:redis在设计过期键清理的时候主要从内存和CPU时间两个方面来衡量的。被动清理方案,减少了大量占用CPU时间。而定期清理兼顾CPU时间和内存释放。
posted on
浙公网安备 33010602011771号