Redis设计与实现第9章 -- 数据库 总结(键空间 过期策略 过期键的影响)

9.1 服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组里,db数组的每一项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库

cpp 复制代码
struct redisServer
//一个数组,保存着服务器中的所有数据库
    redisDb *db;
};

初始化服务器的时候,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,该属性的值由服务器配置的database选项决定,默认情况下为16

9.2 切换数据库

每个Redis客户端都有自己的目标数据库,每当客户端执行数据库读写命令的时候,目标数据库就会成为操作对象。

默认情况下,目标数据库是0号数据库,客户端使用SELECT num命令来切换到num数据库

在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这是一个指向redisDb结构的指针

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

redisClient.db指针指向redisServer.db数组的其中一个元素,这个元素就是客户端的目标数据库

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

注意:

Redis没有可以返回客户端目标数据库的命令,为了避免误操作,最好先执行 SELECT命令显式地切换到指定的数据库

9.3 数据库键空间

Redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中,dict字典保存了数据库里的所有键值对

cpp 复制代码
typedef struct redisDb(
    //数据库键空间,保存着数据库中的所有键值对
    dict *dict;
} redisDb;

这个字典就叫做键空间

  • 键空间的键就是数据库的键,每个键都是一个字符串对象

  • 键空间的值就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象

对数据库的操作,都是通过对键空间字典进行操作来实现的

9.3.1 添加新键

实际上是将一个新键值对添加到键空间字典里面,键是字符串对象,值是任意一种类型的Redis对象

举个例子,在执行以下命令之后:

redis>sET date"2013.12.1"

OK

键空间将添加一个新的键值对,这个新键值对的键是一个包含字符串"date"的字符串对象,而键值对的值则是一个包含字符串"2013.12.1"的字符串对象,如下图所示。

9.3.2 删除键

删除数据库的一个键,实际上是在键空间里删除键对应的键值对对象

9.3.3 更新键

对键空间里键所对应的值对象进行更新,根据值对象的类型不同,更新的具体方法也不同

9.3.4 对键取值

在键空间里取出键所对应的值对象,根据值的类型不同,具体的取值方法也不同。

9.3.5 其他

其他的命令也是通过对键空间进行处理来完成的,比如清空整个数据库的FLUSHDB命令,就是通过删除键空间的所有键值对实现的

9.3.6 读写键空间时的维护操作

当对数据库进行读写时,还需要执行一些额外的维护操作,比如

  • 读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中hit次数或键空间不命中miss次数,可以通过INFO stats命令的keyspace_hits属性和keyspace_misses属性查看

  • 读取一个键以后,服务器会更新键的LRU最后一次使用时间 ,这个值可以用于计算键的闲置时间,使用OBJECT idletime <key>命令可以查看键key的闲置时间

  • 读取一个键时发现该键已经过期,服务器会先删除这个过期键

  • 如果有客户端使用WATCH命令监视了这个键,服务器在修改了被监视的键之后,**会把键标记为脏dity**,让事务程序注意到这个键已经被修改过

  • 服务器每次修改一个键之后,都会对脏dity键计数器的值增加1,这个计数器会触发服务器的持久化以及复制操作

  • 数据库通知功能

9.4 设置键的生存时间或过期时间

使用EXPIRE/PEXPIRE命令,可以以秒/毫秒精度为数据库中的某个键设置生存时间TTL,在经过指定的时间后,服务器就会自动删除生存时间为0的键

注意:SETEX命令可以设置一个字符串键的同时设置过期时间,只用于字符串键

客户端还可以通过EXPIREAT/PEXPIREAT命令,以秒/毫秒精度为某个键设置过期时间

过期时间是一个UNIX时间戳

TTL/PTTL命令接受一个带有生存时间或过期时间的键,返回这个键的剩余生存时间

9.4.1 设置过期时间

实际上有4个命令可以设置

  • EXPIRE <key> <ttl> 将键key的生存时间设置为ttl

  • PEXPIRE <key> <ttl> 将键key的生存时间设置为ttl毫秒

  • EXPIREAT <key> <timestamp> 将键key的生存时间过期时间为timestamp所指定的秒级时间戳

  • PEXPIREAT <key> <timestamp> 将键key的生存时间过期时间为timestamp所指定的毫秒级时间戳

实际上都是用PEXPIREAT命令实现的

9.4.2 保存过期时间

redisDb结构的expires字典保存了数据库里所有键的过期时间,称这个字典为过期字典:

  • 过期字典的键是一个指针,指向键空间的某个键对象

  • 过期字典的值是一个long long类型的整数,保存了过期时间,毫秒精度的UNIX时间戳

PEXPIREAT命令的伪代码如下:

python 复制代码
def PEXPIREAT(key,expire_time_in_ms):
    #如果给定的键不存在于键空间,那么不能设置过期时间
    if key not in redisDb.dict:
        return 0
    #在过期字典中关联键和过期时间
    redisDb.expires[key]=expire_time_in_ms
    #过期时间设置成功
    return 1

9.4.3 移除过期时间

PERSIST命令可以移除一个键的过期时间,相当于PEXPIREAT的反操作:在过期字典里查找给定的键,解除键和值的关联

python 复制代码
def PERSIST(key):
    #如果给定的键不存在于键空间,那么不能设置过期时间
    if key not in redisDb.dict:
        return 0
    #移除
    redisDb.expires.remove(key) 
    return 1

9.4.4 计算并返回剩余生存时间

TTL/PTTL以秒/毫秒为单位返回键的剩余生存时间,是通过计算键的过期时间和当前时间之间的差来实现的

返回值里,-2 表示不存在,-1表示永久有效

python 复制代码
def PTTL(key):
    #键不存在于数据库
    if key not in redisDb.dict:
        return -2
    # 尝试取得键的过期时间#
    #如果键没有设置过期时间,那么expire_time_in_ms将为None
    expire_time_in_ms = redisDb.expires.get(key)
    #键没有设置过期时间
    if expire_time_in_ms  is None:
        return -1
    #获得当前时间
    now_ms =get_current_unix_timestamp_in_ms()
    #过期时间减去当前时间,得出的差就是键的剩余生存时间
    return(expire_time_in_ms - now_ms )
def TTL(key):
    #获取以毫秒为单位的剩余生存时间
    ttl_in_ms = PTTL(key)
    if ttl_in_ms  <0:
        #处理返回值为-2和-1的情况
        return ttl_in_ms
    else:
        #将毫秒转换为秒
        return ms_to_sec(ttl_in_ms)

9.4.5 过期键的判定

检查过期字典,程序通过下面步骤来检查一个给定的键是否过期

  1. 检查给定键是否存在于过期字典:如果存在,取得键的过期时间

  2. 检查当前UNIX时间戳是否大于键的过期时间:是的话,键过期

python 复制代码
def is expired(key):
    #取得键的过期时间
    expire_time_in_ms=redisDb.expires.get(key)
    #键没有设置过期时间
    if expire_time_in_ms is None:
        return False
    #取得当前时间的UNIX时间戳
    now_ms=get_current_unix_timestamp_in_ms()
    #检查当前时间是否大于键的过期时间
    if now_ms>expire_time_in_ms :
        #是,键已经过期
        return True
    else:
        #否,键未过期
        return False

9.5 过期删除策略

如果一个键过期了,什么时候会被删除?有三种不同的删除策略:

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

  • 惰性删除:放任键过期不管,但是每次从键空间获取键的时候,都检查取得的键是否过期;过期的话删除,不过期返回

  • 定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及检查多少个数据库由算法决定。

1和3为主动删除策略,2是被动删除策略

9.5.1 定时删除

对内存友好,过期键会尽可能快的被删除并释放内存

但是对CPU时间最不友好,在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间,对服务器的响应时间和吞吐量造成影响。

另外,创建一个定时器需要用到Redis服务器的时间事件,实现方式为无序链表,查找一个时间复杂度为O(N)

9.5.2 惰性删除

对CPU时间最友好,保证了删除过期键的操作只会在非做不可的情况下进行,而且不会再删除其他无关的过期键上花费任何CPU时间。

但是对内存最不友好,过期键占用的内存一直不会释放,甚至可以看作内存泄漏 -- 无用的垃圾数据占用了大量内存

9.5.3 定期删除

是前两种策略的整合和折中

  • 每个一段时间执行一次删除过期键操作,通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响

  • 有效的减少了因为过期键带来的内存浪费

难点是确定删除操作执行的时长和频率

  • 执行太频繁或执行时间太长,就会退化为定时删除策略

  • 执行太少或执行时间太短,和惰性策略一样出现了浪费内存的情况

9.6 Redis的过期键删除策略

Redis服务器实际上使用的是惰性删除和定期删除两种策略,在合理使用CPU时间和避免浪费内存空间之间取得平衡

9.6.1 惰性删除策略的实现

db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行前都会调用该函数对输入键进行检查

  • 如果输入键过期,该函数将输入键从数据库里删除

  • 输入键未过期,不做操作

每个命令的实现函数要同时处理键存在和键不存在两种情况

9.6.2 定期删除策略的实现

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

伪代码如下:

python 复制代码
#默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16
#默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20
#全局变量,记录检查进度
current_db = 0
def activeExpireCycle():
    #初始化要检查的数据库数量
    #如果服务器的数据库数量比DEFAULTDBNUMBERS要小
    #那么以服务器的数据库数量为准
    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[currentdb]
    #将数据库索引增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 记录了检查进度,下一次调用时也是接着上一次的进度进行处理

  • 如果所有的数据库都被检查完了,current_db被设置为0,重新开始

9.7 AOF、RDB和复制功能对过期键的处理

9.7.1 生成RDB文件

在执行SAVE/BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中

数据库中包含过期键不会对生成新的RDB文件造成影响

9.7.2 载入RDB文件

启动Redis服务器时,如果开启了RDB功能,会对RDB文件进行载入

  • 服务器以主服务器模式运行,程序会对文件中保存的键进行检查,只有未过期的键才会被载入到数据库

  • 服务器以从服务器模式运行,文件中保存的所有键,不论是否过期都会被载入到数据库里。但是因为主从服务器在进行数据同步的时候,从服务器的数据库就会被情况,所以一般也不会造成影响

9.7.3 AOF文件写入

当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但是它还没有被惰性删除或定期删除,AOF文件不会因为这个过期键而产生任何影响

当过期键被惰性删除/定期删除后,程序会向AOF文件追加一条DEL命令,显式记录该键已经被删除

9.7.4 AOF重写

在执行AOF重写的过程里,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件里。

因此数据库中包含过期键不会对AOF重写造成影响

9.7.5 复制

当服务器运行在复制模式下,从服务器的过期键删除动作由主服务器控制:

  • 主服务器在删除一个过期键后,显式地向所有从服务器发送一个DEL命令,告知从服务器删除过期键

  • 从服务器在执行客户端发送的读命令,即使碰到过期键也不会将过期键删除,而且按照正常流程返回值。只有收到主服务器发来地DEL命令之后,才会删除过期键

由主服务器来控制从服务器统一删除过期键,可以保证主从服务器数据地一致性,但是当一个过期键仍然存在主服务器地数据库里,过期键地复制品也依旧存在从服务器。

9.8 数据通知

Redis2.8版本新增加的功能,可以让客户端通过订阅给定的频道或模式来获知数据库中键的变化以及数据库中命令的执行情况

  • 键空间通知:某个键执行了什么命令

  • 键事件通知:某个命令被什么键执行了

服务器配置的 notify-keyspace-events选项决定了服务器所发送通知的类型

  • 想让服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE。

  • 想让服务器发送所有类型的键空间通知,可以将选项的值设置为AK。

  • 想让服务器发送所有类型的键事件通知,可以将选项的值设置为AE。

  • 想让服务器只发送和字符串键有关的键空间通知,可以将选项的值设置为KS。

  • 想让服务器只发送和列表键有关的键事件通知,可以将选项的值设置为E1。

9.8.1 发送通知

notify.c/notifyKeyspaceEvent函数实现,定义如下:

cpp 复制代码
void notifyKeyspaceEvent(int type, char *event,robj *key,
    int dbid);

其中type参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是服务器配置notify-keyspace-events选项所选定的通知类型,从而决定是否发送通知。

event表示事件的名称,key表示产生事件的键,dbid表示产生事件的数据库号码

当一个Redis命令需要发送数据库通知的时候,该命令的实现函数就会调用notifyKeyspaceEvent函数,并且传入相关信息

比如SADD命令的实现函数里的其中一部分代码

cpp 复制代码
Voids saddCommand(red1sclient *c){
    //如果至少有一个元素被成功添加,那么执行以下程序
    if(added){
        //发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",
            c->argv[1],c->db->id);
    } 

}

SADD命令至少成功地向集合里添加了一个集合元素之后,命令就会发送通知,类型为REDIS_NOTIFY_SET,表示这是一个集合键通知,名称为sadd,表示这是执行SADD命令产生地通知

9.8.2 发送通知的实现

notifyKeyspaceEvent的伪代码如下:

python 复制代码
def notifyKeyspaceEvent(type,event,key,dbid):
    #如果给定的通知不是服务器允许发送的通知,那么直接返回
    if not(server.notify_keyspace_events & type):
        return
    #发送键空间通知
    if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
        #将通知发送给频道keyspace@<dbid>_:<key>
        #内容为键所发生的事件<event>
        #构建频道名字
        chan="_keyspace@{dbid)_:{key}".format (dbid=dbid, key=key)
        #发送通知
        pubsubPublishMessage(chan,event)
    #发送键事件通知
    if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT :
    #将通知发送给频道keyevent@<dbid>:<event>#内容为发生事件的键<key>
    #构建频道名字
    chan ="_keyevent@(dbid}_:{event}".format(dbid=dbid,event=event)
    #发送通知
    pubsubPublishMessage(chan,key)

执行以下操作

  • server.notify_keyspace_events 属性是服务器配置notify-keyspace-events选项设置的值,如果给的通知类型type不是服务器允许发送的,直接返回

  • 检测是否允许发送键空间通知,允许的话,构建并发送

  • 检测是否允许发送键事件通知,允许的话,构建并发送

han,event)

#发送键事件通知

if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT :

#将通知发送给频道keyevent@:#内容为发生事件的键

#构建频道名字

chan ="keyevent@(dbid} :{event}".format(dbid=dbid,event=event)

#发送通知

pubsubPublishMessage(chan,key)

执行以下操作

- `server.notify_keyspace_events` 属性是服务器配置`notify-keyspace-events`选项设置的值,如果给的通知类型`type`不是服务器允许发送的,直接返回

- 检测是否允许发送键空间通知,允许的话,构建并发送

- 检测是否允许发送键事件通知,允许的话,构建并发送
相关推荐
SelectDB技术团队2 分钟前
Apache Doris 创始人:何为“现代化”的数据仓库?
大数据·数据库·数据仓库·数据分析·doris
搬码后生仔24 分钟前
将 ASP.NET Core 应用程序的日志保存到 D 盘的文件中 (如 Serilog)
后端·asp.net
Suwg20926 分钟前
《手写Mybatis渐进式源码实践》实践笔记(第七章 SQL执行器的创建和使用)
java·数据库·笔记·后端·sql·mybatis·模板方法模式
雪球不会消失了26 分钟前
MVC架构模式
架构·mvc
丁总学Java30 分钟前
优化 invite_codes 表的 SQL 创建语句
java·数据库·sql
hmbbpdx43 分钟前
MySql B树 B+树
数据库·b树·mysql
编程阿布1 小时前
Python基础——多线程编程
java·数据库·python
冰镇毛衣1 小时前
4.5 数据表的外连接
数据库·sql·mysql
又蓝1 小时前
使用 Python 操作 MySQL 数据库的实用工具类:MySQLHandler
数据库·python·mysql
庄小焱1 小时前
Java开发经验——数据库开发经验
数据库·系统设计·代码重构