美文网首页
Redis数据库及过期实现

Redis数据库及过期实现

作者: 都是浮云啊 | 来源:发表于2019-05-08 22:21 被阅读0次

    [TOC]

    前言

    本文主要分析 Redis 的数据库的一些基本知识,主要是服务器保存数据库的方法和客户端切换数据集的方法,数据库键的键值对的增加、更新、删除、查找指令,以及Redis对过期是如何处理的。

    1 服务器的数据库

    Redis服务器将所有数据库都保存在 redisServer 结构中。在redis服务器初始化的时候,会根据 dbnum 属性来决定要创建多少个数据库,这个值在配置文件中,默认是16,也就意味着redis最初会创建16个数据库。

    struct redisServer{
    // ···
    //一个指向数组的指针,数组中保存着服务器中的所有数据库,每个数据库中的字典保存着所有的键值对
    redisDb *db;
    // 服务器中的数据库数量
    int dbnum;
    };
    

    而对于每个redis客户端,redis服务器都会创建一个 redisClient 实例,这个实例中的db属性记录了客户端当前的目标数据库,也就是客户端要操作的数据库,这个属性是一个指向了redisDB的指针。

    typedef struct redisClient{
        // ···
        // 指向redisServer中db数组的某个,默认是0号db,可通过select nun选择
        redisDb *db;
    }redisClient;
    

    而每个 redisDb 都使用了字典 dict 的结构来存储该数据库中的所有的键值对。实际上我们在客户端对数据库所进行的操作大部分都是在对redisDb中的字典进行的操作,比如对数据库的键值对进行增删查改操作,实际上都是操作的字典。

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

    假设执行了如下命令:

    localhost:6379> set name yupao
    OK
    localhost:6379> sadd class 7 8 9
    (integer) 3
    localhost:6379>
    

    此时在redis服务的数据库中的结构如下所示,键值对的键就是字典中的键都是 stringObject 格式的,值则是对应存储的类型。之前说过5种类型的结构。对数据库的新增就是在字典上新增一个结构,在删除也是从字典里删除,同理更新和查找也是。(字典的底层主要有 2 个哈希表,然后 k-v 存储在第一个哈希表ht[0] 的节点中,详情可参考之前的文章)

    image.png

    键值对的操作都是基于字典完成的,还有一些针对数据库本身的命令,也是通过键空间来完成的,比如SELECT FLUSHDB(极度危险的命令,慎用之...)。并且当redis命令对数据库进行读写时服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作。举几个简单的例子:

    1. 更新 redisObject 中的lru属性,记录最后一次使用时间,这个值被用来计算键值对的空转时长,前面对象里学习过
    2. 如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期的键然后才会执行余下的指令。

    2. redis 键值对过期

    2.1 设置过期时间

    通过EXPIRE/PEXPIRE命令,客户端可以以秒/毫秒的精度为数据库中的某个键设置生存时间(TTL Time To Live),在指定的秒/毫秒数之后,服务器就会自动删除生存时间为9的键。值得注意的是,这2个命令只能用于字符串(k、v都是string)。与EXPIRE/PEXPIRE命令类似,客户端还可以通过EXPIREAT/PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间,这个时间是一个UNIX时间戳,当键的过期时间来临时,服务器就会自动从数据库中删除这个键,这就是Redis的2种类型4个命令的过期处理。而对应的TTL/PTTL可以返回指定键离自动删除还有多长时间。
    虽然有多种不同单位和不同形式的设置命令,但是实际上EXPIRE/PEXPIRE/EXPIREAT这3个命令都是使用PEXPIREAT命令实现的,经过转换之后最终的执行和PEXPIREAT命令是一样的.

    1. EXPIRE可以转换成PEXPIRE,也就是把秒转成毫秒
    2. PEXPIRE可以转成PEXPIREAT,也就是获取剩余毫秒+当前UNIX时间戳
    3. EXPIREAT可以转成PEXPIREAT,把秒转成毫秒
    2.2 保存过期时间

    redisDb结构中的expires字典保存了数据库中所有键的过期时间,称之为过期字典。

    1. 过期字典的键是一个指针,指向数据库键空间中某个要过期的键对象
    2. 过期字典的值时一个long long类型的整数,这个整数保存了键所指向的数据库的键的过期时间也就是一个 UNIX时间戳
    typedef struct redisDb{
        // ···
        //数据库的键空间
        dict *dict;
        //过期字典,保存键的过期时间
        dict *expire;
    }
    

    假设我们操作了一个有过期属性的键的命令如下:

    localhost:6379> set name yupao
    OK
    localhost:6379> expire name 10
    (integer) 1
    

    此时在redis的底层它的结构抽象起来如图所示

    image.png

    事实上,redis不会去再创建一个 stringObject 对象 name的,两个字典的指针指向同一个键对象,所以不会出现任何重复对象,也不会浪费任何空间。值是一个long long类型的UNIX时间戳,前面讲过,无论执行的是哪个过期时间的设定,实际上存储的是过期的时间戳。

    2.3 删除过期时间

    PERSIST命令可以移除一个键的过期时间,-1代表没过期时间,-2代表过期。这个命令会在过期字典中查找到给定的键,并接触键值在过期字典中的关联。TTL/PTTL是返回剩余的过期时间,它是计算得来的,拿过期字典中的对应的键的long long类型的UNIX时间戳减去当前时间戳,然后看是需要毫秒还是秒再转换然后返回的。通过过期字典,程序可以检查一个给定的键是否过期。底层也有一个方法is_expired(string k),返回true就代表过期了。redis检查键过期就是用的这个方法而不是TTL/PTTL,毕竟直接操作字典比指令要快一点点(接收指令到执行还要寻找指令的执行方法等等,可参照一个 redis 的命令是如何执行的文章)

    localhost:6379> set name yupao
    OK
    localhost:6379> EXPIRE name 1000
    (integer) 1
    localhost:6379> TTL name
    (integer) 995
    localhost:6379> PERSIST name
    (integer) 1
    localhost:6379> TTL name
    (integer) -1
    localhost:6379>
    
    2.4 redis提供的过期删除的一些策略

    通过上面我们知道了所有带有过期时间属性的键值对都将在过期字典中有一份记录,这个记录的键是对应有过期属性的键,值是long long类型的UNIX时间戳,也知道了如何判断一个键是否过期,底层使用is_expired方法,客户端使用TTL/PTTL指令实现的。现在是不是还有一个问题,如果一个键过期了,那么它将会在什么时候被删除呢?redis提供了3种针对过期键值的删除策略

    1. 定时删除【主动】:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时删除之。
    2. 惰性删除【被动】:过期了放那,但是每次从键空间获取该键的时候判断有没有过期,如果过期了就删除,没过期就返回。
    3. 定期删除【主动】:每隔一段时间程序对数据库进行一次检查,删除过期键。
    2.4.1 定时删除
    • 定时删除对内存是很友好的,通过使用定时器,会保证键过期的时候立马被删除,节约内存。但是对CPU是不友好的,过期键很多的情况下,删除过期键的行为可能要占用很长的时间,对吞吐量有影响。
    • 除了上面说的之外,创建一个定时器需要用到Redis的时间事件,而时间事件的实现是无序链表,要查找一个事件的时间复杂度为O(N),导致不能高效的处理大量的时间事件。
    2.4.2 惰性删除
    • 毫无疑问,惰性删除对内存是不友好的,可能会存在许多过期的键没有被访问的情况。对CPU时间是最友好的,程序只有在取出键的时候才对键进行过期检查,如果有些键被用一次就没用被用了,那么可能永远都不会被回收除非执行FLUSHDB命令强制刷新。
    • 它使用expireIfNeeded方法,就像一个过滤器一样,每个访问的键都可能因为国企被它删除
    2.4.3 定期删除

    上面的2种都带有明显的优点和缺点,而定期删除时一种整合和折中的处理。

    • 定期删除每隔一段时就按执行一次删除过期键操作,并限制删除执行的次数和频率减少对CPU的影响
    • 那些过期的但是没被回收的会在下个周期到来的时候回收,不会太夸张的浪费内存
    • 定期删除的难点是难以确定删除操作执行的时长和频率,这个最好在实际生产环境中看到。
    • 定期删除activeExpireCycle方法,每次运行时从一定数量的数据库中取出一定数量的随机键进行检查并删除其中过期键。全局变量current_db记录当前检查的进度,假设当前检查10号库,current_db=10,下一个执行周期就会从current_db=11开始找。检查完了会再次被置为0。一直开始循环执行。
    2.5 AOF、RDB和复制功能对过期键的处理

    这一部分,看看Redis的过期键对Redis服务器其它模块的影响,RDB持久化和AOF持久化以及复制功能如何处理数据库中的过期键。

    2.5.1 RDB处理
    • 在执行SAVE/BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。比如有三个键k1,k2k3,并且k2已经过期了,程序就不会吧k2保存到RDB文件中。因此数据库中包含过期的键不会对RDB文件造成影响。

    • 当启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入:

    1. 如果服务以主服务器模式运行,载入RDB文件时,程序会对文件中保存的键进行检查,未过期的会被载入到数据库中,过期的忽略。
    2. 如果服务器以从服务器模式运行,载入RDB文件时,文件中保存的所有键不论是否过期都会被载入到数据库中。不过,当主从同步的时候,从服务器的数据库就会被清空,所以载入过期键无影响。
    2.5.2 AOF处理
    • 和生成RDB文件类似,过期的键不会被保存到重写后的AOF文件中
    • 主服务器在删除一个过期键之后,会显式向所有从服务器发出一个DEL命令,告知从服务器删除这个过期键
    • 从服务器在执行客户端发送的命令时,即使碰到过期键也不会将过期键删除,而是会按照正常键处理
    • 从服务器只有在接到主服务器发送来的DEL命令之后才会删除过期键。

    通过主服务器来控制从服务器统一删除过期键,可以保证主从数据的一致性,也正是因此,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器的复制品也会继续存在。

    总结

    Redis的数据库相关的知识,主要是服务器保存数据库的方法和客户端切换数据集的方法,数据库键的键值对增加、更新、删除、查找指令。在 redis 的服务器/客户端模型中使用的 redisDb 结构维护了一个 expires 字典,字典的 key 一个指向带过期属性的键的指针,value 是 long long 类型的,表示过期的时间戳。然后 redis 提供了三种类型的过期删除,分别是 定时、惰性、定期,其中定期就是每次运行时从数据库中随机取一定的随机键值并检查,然后循环一个参数实现每次不会从一个地方去取。同时在 redis 的持久化、主从复制也对过期进行了相关的处理。这个特性其实还是蛮有意思的。

    相关文章

      网友评论

          本文标题:Redis数据库及过期实现

          本文链接:https://www.haomeiwen.com/subject/xneboqtx.html