前言

深入了解一下Redis内存机制如何存储数据,以及对于过期数据采取何种策略来清理。


@

目录

  • 前言
  • 一、Redis服务器中的数据库
  • 二、数据库键空间
  • 2.1 数据存储——键空间 dict
  • 2.2 键的生存时间——过期字典expires
  • 2.2.1 设置过期时间
  • 2.2.2 保存过期时间
  • 2.2.3 移除过期时间
  • 三、过期键的删除策略
  • 3.1 定时删除
  • 3.2 惰性删除
  • 3.3 定期删除
  • 四、Redis的过期键删除策略
  • 4.1 惰性删除策略的实现
  • 4.2 定期删除策略的实现
  • 五、AOF、RDB持久化和复制功能对过期键的处理
  • 5.1 生成RDB文件
  • 5.2 载入RDB文件
  • 5.3 AOF文件写入
  • 5.4 AOF重写
  • 5.5 复制
  • 六、数据库通知
  • 6.1 通知分类
  • 6.2 通知发送

一、Redis服务器中的数据库

Redis服务器将所有的数据库保存在 redis.h/redisServer 结构的 db 数组中,db 数组的每个项都是一个 redis.h/redisDb 结构,每个 redisDb 结构代表一个数据库。另外Redis中数据库的数量由 dbnum指定,默认为16。

struct redisServer {
    //一个数组,保存着服务器中所有的数据库
    redisDb *db;
    //服务器的数据库数量,默认为16
    int dbnum;
}

客户端状态的 redis.h/redisClient 结构的 db 属性记录了客户端当前的目标数据库,这个属性是一个指向 redisDb 结构的指针。redisClient.db 指针指向 redisServer.db 数组中的一个元素,而被指向的元素就是客户端的目标数据库。

typedef struct redisClient {
    //记录客户端当前正在使用的数据库
    redisDb *db;
}

redis 如何设计多对多 redis多db设计_redis 如何设计多对多


常见的 SELECT xxx 切换数据库指令的实现原理就是通过修改 redisClient.db 指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能。

二、数据库键空间

2.1 数据存储——键空间 dict

redisDb 数据库结构中的 dict 字典保存了数据库中的所有键值对,我们称这个字典为键空间。
键空间的键就是数据库的键,均为字符串对象;键空间的值对应着Redis 的几种数据结构类型(字符串对象、列表对象、哈希表对象、集合对象、有序集合对象)。

typedef struct redisDb {
    // 数据库键空间,保存了数据库中所有的键值对
    dict *dict;
} redisDb;


2.2 键的生存时间——过期字典expires

2.2.1 设置过期时间

  • EXPIRE <key> <ttl> :将键的生存时间设置为 ttl 秒;
  • PEXPIRE <key> <ttl>:将键的生存时间设置为 ttl 毫秒;
  • EXPIREAT <key> <timestamp>:将键 key的过期时间设置为 timestamp 所指定的秒数时间戳;
  • PEXPIREAT <key> <timestamp>:将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳。

2.2.2 保存过期时间

redisDb 结构中的 expires 字典保存了数据库中所有键的过期时间,这个字典被称为过期字典。
过期字典的键是一个指针,指针指向键空间中的某个键对象(也就是某个数据库键);
过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间,是一个毫秒精度的UNIX时间戳。

typedef struct redisDb {
    //过期字典
    dict *expires;
}

2.2.3 移除过期时间

PERSIST xxx 命令可以移除一个键的过期时间,该命令在过期字典中查找给定的键,并解除键和值在过期字典中的关联。

redis 如何设计多对多 redis多db设计_redis 如何设计多对多_02

三、过期键的删除策略

Redis 中包含三种过期键的删除策略:定时删除、惰性删除、定期删除

3.1 定时删除

在设置键的过期时间的同时,创建一个定时器 (timer),让定时器的过期时间来临时,能够立即执行对键的删除操作。

  • 优点:
    定时删除对内存是最友好的,使得过期的键能够尽快被删除,并且释放掉过期键所占用的内存;
  • 缺点:
    (1) 对于CPU时间最不友好,在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分的CPU时间。在内存不紧张但是CPU时间非常紧张的情况下,会对服务器的响应时间和吞吐量造成影响。
    (2) 创建一个定时器需要用到Redis服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效处理大量时间事件。

3.2 惰性删除

放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键,否则返回该键。

  • 优点:
    对于CPU时间是最友好的,只有在取出键的时候才会检查键是否过期;
  • 缺点:
    对内存不友好,会造成大量过期无用的键占用内存,如果一直得不到回收就会造成内存泄漏。

3.3 定期删除

是定时删除和惰性删除的整合和折中。

定期删除每隔一段时间执行一次删除过期键的操作,并且通过限制删除操作的执行时长和频率来减少删除操作对于CPU 时间的影响,同时减少了因为过期键带来的内存占用。

四、Redis的过期键删除策略

Redis采用惰性删除和定期删除策略来实现过期键的删除

4.1 惰性删除策略的实现

惰性删除策略由 db.c/ expireIfNeeded 函数来实现,所有读写数据库的Redis命令在执行前都会调用 expireIfNeeded 函数对输入的键进行检查。如果检查发现输入的键已经过期,那么会将输入的键从数据库中删除;否则什么也不做,继续执行命令。总的来说 expireIfNeeded 函数相当于一个过滤器,在真正的命令执行前过滤掉过期的输入键,从而避免命令接触到过期键。

redis 如何设计多对多 redis多db设计_服务器_03

4.2 定期删除策略的实现

Redis 定期删除策略是通过函数 redis.c/ activeExpireCycle 来实现的,每当 Redis 服务器的周期性操作 redis.c/ serverCron函数执行时,activeExpireCycle 函数就会被调用,在规定时间内分多次遍历服务器中的各个数据库,从数据库的过期字典中随机检查一部分键的过期时间,并删除其中过期的键。

activeExpireCycle 函数伪代码:

redis 如何设计多对多 redis多db设计_redis_04


函数实现时定义了默认每次检查数据库的数量为16,默认每个数据库检查键的数量为20,同时定义了一个 全局变量current_db来记录当前函数的检查进度,并在下一次 activeExpireCycle函数调用时接着上一次的进度进行处理。并且随着函数的不断调用执行,当所有数据库检查完毕后将 current_db值重置为0,再开始进行新一轮检查。

五、AOF、RDB持久化和复制功能对过期键的处理

5.1 生成RDB文件

在执行 SAVE 或者 BGSAVE 命令创建一个新的 RDB文件时,程序会对数据库中的键进行检查,已过期的键不会保存到新创建的 RDB 文件中。因此数据库中包含过期键不会对生成新RDB文件造成影响。

5.2 载入RDB文件

在启动Redis服务器时会恢复数据,载入 RDB文件,但是对于主从服务器处理过期键的策略不同:

  • 如果是主服务器模式运行,载入时程序会检查文件中保存的键,未过期的键会被载入到数据库中,而过期键则会被忽略,所以对于主服务器没有影响;
  • 如果时从服务器模式运行,载入时不管过期与否都会保存所有的键。但是因为主从服务器在进行数据同步时从服务器的数据库会被清空,所以也不会受到影响。

5.3 AOF文件写入

如果数据库中的某个键已经过期但是还没有被删除,那么AOF文件会照常记录。但是当过期键被惰性删除或者定期删除时,程序会向AOF文件追加一条DEL命令,来显式地记录该键已经被删除。
比如说客户端使用 GET message 命令试图访问过期的键 message,那么服务器会执行以下三个动作:

  1. 从数据库中删除 message键;
  2. 追加一条 DEL message 命令到 AOF文件中;
  3. 返回空结果给客户端。

5.4 AOF重写

和RDB类似,在执行AOF重写过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到结果AOF文件中。

5.5 复制

主从模式下,从服务器的过期键删除动作由主服务器控制,这样才能保证主从服务器的数据的一致性:

  • 主服务器在删除一个过期键后会向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键;
  • 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会删除,而是继续像处理未过期键一样来处理过期键。
  • 从服务器只有在接收主服务器发来的DEL命令后,才会删除过期键。

redis 如何设计多对多 redis多db设计_redis_05

六、数据库通知

6.1 通知分类

数据库通知能够实现客户端订阅给定频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。

通知分为两种,一种是关注 "某个键执行了什么命令",叫做键空间通知;另外一种是 "某个命令被什么键执行了",叫做键事件通知。

SUBSCRIBE __keyspace@0__:message:针对键message的键空间通知;

SUBSCRIBE __keyevent@0__:del:针对命令del的键事件通知;

redis 如何设计多对多 redis多db设计_redis 如何设计多对多_06


redis 如何设计多对多 redis多db设计_服务器_07

6.2 通知发送

发送数据库通知的功能是由 notify.c/notifyKeyspaceEvent 函数实现的,其中服务器配置的 notify-keyspace-events选项决定了服务器所发送通知的类型,可以配置的类型如下:

  • 服务器发送所有类型的键空间通知和键事件通知,设置为 AKE;
  • 服务器发送所有类型的键空间通知,设置为AK;
  • 服务器发送所有类型的键事件通知,设置为AE;

函数 notifyKeyspaceEvent(int type, char *event, robj *key, int dbid),type指明发送的通知类型,event指明事件名称,key指明产生事件的键,dbid指明产生事件的数据库号码。

redis 如何设计多对多 redis多db设计_redis_08