Redis-数据库&过期

一、结构概括

Redis中的键值对(数据)保存的地方叫做Redis的数据库,数据库的结构信息使用redisServer来描述。Redis服务器启动时,默认是初始化16个数据库,一般我们使用的都是0号数据库(默认数据库)。

当然,可以通过使用select命令切换至你要操作的数据库。一般时候,我们通过redis-cli客户端连接Redis的服务器的时候,会在会话处显示默认连接的数据库是[0],可以使用select x来切换数据库,切换之后在会话结尾处可以直接看到数据库标识变化。

虽然redis初始化了16个数据库,但是,一般不建议切换数据库来进行数据分开存储,因为这需要使用者自己维护和选择,且在一些客户端连接没有显示正在使用的数据库的情况下,容易造成数据的丢失,不易操作。

下面描述了redisServer的结构信息:

struct redisServer {
    //redis数据库数组
    redisDb *db;
    //redis服务器数据库数量
    int dbnum;
    //...
} redisServer;

上图展示了redisServer中存储了数据库的数组以及数据库的数量dbnum,dbnum可以通过配置参数设置,默认是16。下面我们看下redisDB数据库的基本信息组成:

struct redisDb {
    //使用字典结构存储所有键值对
    dict *dict;
    //...
} redisDb;

可以看到redis数据库中的键值对都是存储在这个dict字典结构中的,下图展示了一个数据库存储部分键值对的数据库结构,假如我们向redis中存储了一个列表对象,一个哈希对象和一个字符串对象,在库中的逻辑显示结构如下:

对于多个redis数据库,redis客户端都会有自己的目标数据库,在客户端连接目标数据库期间,该客户端的命令都会在这个数据库中执行。在服务器内部,维护了客户端状态的信息,是使用redisClient结构来记录此状态,状态中记录了客户端的目标数据库,redisClient的结构如下:

上图展示了客户端状态和服务器状态的一个关联关系,客户端状态redisClient中含有指向目标数据库的指针,并且可以通过命令进行切换至其他数据库。 

二、命令空间

1.命令操作

上面对redis数据库的结构做了介绍,我们可以知道redis数据库是使用字典结构来作为整个库的存储结构,字典的key值就是我们的操作redis对象的key值,是一个String对象,字典的value就是我们存储的key所对应的值,他可以是StringObject、hashObject等,关于redis的对象以及对象底层结构可以参考文章《Redis-库之数据结构》了解。

当我们操作数据库键值对的时候,实际上就是操作目标数据库的这个ht结构中的键值对,包括最基本的增删改查操作,都会映射到整个字典结构的变更和获取,以下是简单的操作示例:

添加新键:实际上是将一个新的键值对添加到键空间的字典中,其中键为字符串对象,值为任意一种redis对象;

删除键:实际上是将一个键值对从键空间种删除的过程;

更新键:则是对键空间的里边键所对应的值对象进行更新,不同的redis对象,其支持的更新的方式也是不同的;

键取值:实际上是从键空间中取出查询键所对应的值对象,不同的redis对象,支持的查询方式也是不同的;

还有包括一些其他的键空间的操作,例如清空键空间,FLUSHDB,通过删除整个键空间的键值对。DBSIZE,返回键空间中的键的数量;RSNDOMKEY,键空间随机返回一个键;还有其他命令,包括EXISTS、RENAME、KEYS等命令。

2.空间维护

当我们对键空间进行一系列的读写操作时,出来会对键空间有一定的影响作用之外,还会维护其他的一些信息,大致如下:

操作类型维护操作其他
读取键每当读取一个键时(读写都需要读取key),会根据这个key是否存在来维护键空间的命中次数(hit)和键空间的不命中数(miss)。这两个值可以通过INFO status命令的keySpace_hits和keySpace_misses查询。
读取键每当读取一个键时(读写都需要读取key),服务器会更新这个键的LRU(最后一次使用)时间。

这个值可以用于计算键的空闲时间,通过使用OBJECT idletime <key>来查看键key的闲置时间。

读取键判断键值是否过期,如果过期则删除这个过期键。redis对于过期键的处理策略后续会讲解。
watch键如果客户端对这个key使用了WATCH关键字,则当对此key进行修改时,会把该key标记为脏key。让事务程序检测到这个key值在事物期间发生变化,进行响应。
修改键每当服务器对key进行修改操作,都会进行计脏操作。这个数据关系到redis服务的持久化和复制操作。
数据库通知如果数据库开启了数据库通知功能,则会发送相应的数据库通知。消息通知功能。

可以看出数据库除了作相应的命令操作之外,还会作一些相应的统计、标记、通知等操作,从而支持更加多维的操作和查询分析。

三、过期操作

1.过期操作

redis数据库是支持键的有效时间设置的,有几个命令可以对键设置过期时间,如下:

命令含义
EXPIRE <key> <ttl>将key的生存时间设置为ttl秒
PEXPIRE <key> <ttl>将key的生存时间设置为ttl毫秒
EXPIREAT <key> <timestamp>将key的过期时间设置为指定timestamp秒数时间戳
PEXPIREAT <key> <timestamp>将key的过期时间设置为指定timestamp毫秒数时间戳

虽然上述四种命令的略有不同,但是实际上底层调用的都是PEXPIREAT命令,也就是其他三种命令在redis服务器中都要转化为PEXPIREAT命令,然后对key设置过期时间,如下转换:

设置过期时间后,redis服务器提供了命令查询key的剩余生存时间,分别是TTL和PTTL,两者都是返回当前key距离服务器删除他还有多少的时间剩余。

对于过期时间的存储,redis服务器使用redisDb中的expires字典保存了数据库中所有键的过期时间,被称作过期字典,定义数据结构如下:

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

下图展示了带有过期字典的redisDb的一个简单结构,本例子中键空间保存了键值对,过期字典则保存了键的long类型的过期时间戳:

注意:保存键过期的expires字典中,图中显示key存储在过期字典中,实际上它会是一个指向db中key的指针,图中是为了便于理解。

从结构上来看,当我们对指定键进行过期时间操作的时候,实际上是对应在redisDb中的expires字典进行相应的操作,比如设置一个从未设置过期时间key来指定过期时间,则是在expires中添加一个k-v结构,移除键的过期时间(PERSIST)则意味着删除expires中对应的键的过期时间戳。TTL命令可以以秒位单位返回键的剩余生存时间,PTTL以毫秒返回键的过期时间,结合expires字典结构,则是计算当前时间和过期时间戳的一个差值返回到客户端。

2.过期键删除策略

上面我们了解到redis中过期键的存储结构,知道过期时间是存储在redisDb中的一个expires字典中的,那么下面就看下redis对于过期键的删除时机是怎样的。

首先有三种过期键的删除策略:

1.定时删除:在给定key设置过期时间的同时,创建一个定时器(Timer),让定时器在键过期之际,删除键值对。

优点:能够使得尽快删除过期键,及时释放内存,对内存友好;

缺点:因为定时器是比较占用CPU的,因为定时器过多的情况下,会对系统CPU有一定影响,从而整个服务性能,且目前redis中定时器需要使用时间事件,时间事件是无序链表实现的,查找一个事件的复杂度O(N),因此效率上有是有瓶颈的。

2.惰性删除:放任该键不管,但是每次获取该键的时候,检查该键是否过期,相应的执行删除操作。

优点:当获取键的时候才会去检查键的过期,这样对CPU是非常友好的,且只操作当前键。

缺点:对内存不够友好,因为键是在获取时进行检查,如果过期键后期不被使用的话,将没有机会删除,继续占用内存,是一种内存泄漏的现象,此种策略需要结合具体场景使用。

3.定期删除:每隔一段时间,就对数据库中的键进行检查,删除过期键。计划时间、删除数量则由算法决定。

上面的两种方式定时和惰性删除策略,各有优缺点,分别是对CPU和内存方面的保护使用,定期删除则是对两种方案的折中,通过定期执行,通过限制执行的时间和频率从而减少删除操作对CPU的影响;另一方面,通过定期执行删除,可以尽可能处理过期键,减少内存占用,避免内存泄漏现象的持续发生。

但是定期策略的难点在于控制时常和频率,因为太过于频繁则退化为定时删除,太过于延长则退化为惰性删除,因此要结合自己的使用场景进行合理的参数配置。

redis的过期策略?redis服务器中对于过期键处理采用的是惰性删除和定期删除两种策略结合使用,以便一方面可以合理使用CPU时间,另一方面避免内存泄漏带来的内存浪费。

redis对于惰性删除的策略实现基于db.c/expireIfNeeded函数实现,所有的读写数据库命令在执行之前都会对调用该函数,执行过程如下:

上右图以get命令为例,展示惰性删除中对于过期操作的处理大致流程。

redis对于定期删除的策略实现是基于redis.c/activeExpireCycle函数实现的,也就是说redis会根据配置来定期执行activeExpireCycle函数,在定期时间内,分多次遍历各个数据库,从数据库中expires字典中随机找到一部分过期键并删除,基本过程如下:

1.函数每次运行时,都是从一定量的数据库中抽取一定量的过期键进行检查,并删除其中过期的过期键;

2.redis中会通过current_db记录定期删除策略redis.c/activeExpireCycle函数执行的进度,在下一次redis.c/activeExpireCycle函数调用时,接着上次执行的地方继续执行。比如说,本次activeExpireCycle函数执行完数据库10返回了,那么下一次则依据本次记录为基准,本次从数据库11开始执行检查和删除过期键;

3.随着定期删除策略redis.c/activeExpireCycle函数的多次执行,遍历完数据库后,会把current_db的记录重置为0,开始下一轮新的检查。

3.持久化过程(Redis-持久化)过期键处理

RDB:快照方式的持久化

1.生成RDB文件:当SAVE或者BGSAVE命令创建新的RDB文件时,程序会对数据库中过期键检查,过期的键不会放在RDB文件中。

2.载入RDB文件:在redis服务器启动时,如果开启了RDB功能,会根据RDB文件进行数据的恢复,如果服务器以主服务器的方式运行,会对载入的键做过期检查,如果已过期则跳过,如果未过期则载入;如果服务器是以从服务器的方式运行,则在载入RDB文件的时候会保留所有键,不会有check操作,他会以主服务器发送的DEL过期键命令为准进行过期键的删除操作,以此保证主从的一致性。

AOF:追加命令方式的持久化

1.AOF文件写入:当服务器在运行中的时候,开启了AOF功能,当过期键通过定期删除或者惰性删除策略被删除时候,会在AOF文件追加DEL命令,以此处理过期键,以GET一个过期键key的执行过程举例,过程如下:

  • 从数据库删除key键
  • 追加一条DEL key命令到AOF文件
  • 向执行GET命令的客户端返回空回复

2.AOF重写:与RDB的载入相同原理,在AOF重写的过程中,会对过期键进行检查,排除掉过期键,不写入到AOF文件中。

复制模式下

当redis运行在复制模式下,从服务器不会主动的对过期键进行检查处理,而是以主服务器的命令为基准,这样可以最大限度的保证主从一致。那么也就是说有一个过期键存在与从服务器上,且主服务器还没有发送过期删除命令前,如果有客户端从从服务器读取该过期键,仍然会读取到该过期键的值。该问题在redis3.2以后已经解决,会返回nul处理。

四、资源

官网:https://redis.io/

文档:《Redis 设计与实现》 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值