zoukankan      html  css  js  c++  java
  • redis--数据库

    数据库

     
    除了说明数据库是如何储存数据对象之外,本章还会讨论键的过期信息是如何保存,而 Redis又是如何删除过期键的
     
    数据库的结构
    Redis 中的每个数据库,都由一个 redis.h/redisDb 结构表示:
     
    typedef struct redisDb {
         // 保存着数据库以整数表示的号码
         int id;
     
         // 保存着数据库中的所有键值对数据
         // 这个属性也被称为键空间( key space)
         dict *dict;
     
         // 保存着键的过期信息
         dict *expires;
     
         // 实现列表阻塞原语,如 BLPOP
         // 在列表类型一章有详细的讨论
         dict *blocking_keys;
         dict *ready_keys;
     
         // 用于实现 WATCH 命令
         // 在事务章节有详细的讨论
         dict *watched_keys;
    } redisDb;
     
    数据库的切换
    redisDb 结构的 id 域保存着数据库的号码,
    并不是给切换数据库的select 命令使用的,而是给redis 内部程序使用的。
     
    比如说,给AOF程序,复制程序,RDB程序,需要知道当前数据库的号码。
     
    数据库键空间
    因为 Redis 是一个键值对数据库( key-value pairs database),所以它的数据库本身也是一个字
    典(俗称 key space)
    • 字典的键是一个字符串对象。
    • 字典的值则可以是包括字符串、 列表、 哈希表、 集合或有序集在内的任意一种Redis类型对象。
    在 redisDb 结构的 dict 属性中,保存着数据库的所有键值对数据。
     
    键值空间例子:
     
    键空间的操作
    因为数据库本身是一个字典,所以对数据库的操作基本上都是对字典的操作,
    加上以下一些维护操作:
    • 更新键的命中率和不命中率,这个值可以用 INFO 命令查看
    • 更新键的 LRU 时间,这个值可以用 OBJECT 命令来查看;
    • 删除过期键(稍后会详细说明);
    • 如果键被修改了的话,那么将键设为脏(用于事务监视),并将服务器设为脏(等待 RDB保存)
    • 将对键的修改发送到 AOF 文件和附属节点,保持数据库状态的一致;
     
    增删改查,实际上就是对字典空间的操作。
    注意修改,是先释放原来的空间,新建空间的过程。
     
    其他操作
    • FLUSHDB 命令:删除键空间中的所有键值对。
    • RANDOMKEY 命令:从键空间中随机返回一个键。
    • DBSIZE 命令:返回键空间中键值对的数量。
    • EXISTS 命令:检查给定键是否存在于键空间中。
    • RENAME 命令:在键空间中,对给定键进行改名。
     
    键的过期时间
    EXPIRE 、PEXPIRE 、EXPIREAT 和 PEXPIREAT 四个命令,客户端可以给某个存
    在的键设置过期时间,当键的过期时间到达时,键就不再可用:
     
    redis> SETEX key 5 value
    OK
     
    redis> GET key
    "value"
     
    redis> GET key // 5 秒过后
    (nil)
     
    命令 TTL 和 PTTL 则用于返回给定键距离过期还有多长时间:
     
    redis> SETEX key 10086 value
    OK
     
    redis> TTL key
    (integer) 10082
     
    redis> PTTL key
    (integer) 10068998
     
    过期时间的保存
    在数据库中,所有键的过期时间都被保存在 redisDb 结构的 expires 字典里:
    typedef struct redisDb {
    // ...
    dict *expires;
    // ...
    } redisDb;
     
    expires 字典的键是一个指向 dict 字典(键空间)里某个键的指针,
    而字典的值则是键所指向的数据库键的到期时间,这个值以long long类型表示。
     
    设置生存时间
     
    Redis 有四个命令可以设置键的生存时间(可以存活多久)和过期时间(什么时候到期):
    • EXPIRE 以秒为单位设置键的生存时间;
    • PEXPIRE 以毫秒为单位设置键的生存时间;
    • EXPIREAT 以秒为单位,设置键的过期 UNIX 时间戳;
    • PEXPIREAT 以毫秒为单位,设置键的过期 UNIX 时间戳。
     
    虽然有不同形式的设置方式,但expires字典的值值保存以毫秒为单位的过期UNIX时间戳,
    这就是说,通过转换后,所有命令的效果最后都和pexpireat命令的效果一样。
     
    例子:
    从expire命令到pexpireat 命令的转换可以用一下伪代码表示:
     
    def EXPIRE(key, sec):
         # 将 TTL 从秒转换为毫秒
         ms = sec_to_ms(sec)
     
         # 获取以毫秒计算的当前 UNIX 时间戳
         ts_in_ms = get_current_unix_timestamp_in_ms()
     
         # 毫秒 TTL 加上毫秒时间戳,就是 key 到期的时间戳
         PEXPIREAT(ms + ts_in_ms, key)
     
    过期键的判定
    通过 expires 字典,可以用以下步骤检查某个键是否过期:
    1. 检查键是否存在于 expires 字典:如果存在,那么取出键的过期时间;
    2. 检查当前 UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则,键未过期
     
    过期键的删除
    • 定时删除
      • 在设置键的过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。
      • 优点
        • 定时删除策略对内存是最友好的:
        • 因为它保证过期键会在第一时间被删除,过期键所消耗的内存会立即被释放。
      • 缺点
        • 它对 CPU 时间是最不友好的:
        • 因为删除操作可能会占用大量的CPU时间,
        • 每设置一个过期时间,就会产生一个定时事件,CPU就需要处理这个定时事件
        • 此外,Redis事件处理器对时间事件的实现方式---无序表,查找一个时间复杂度为O(N)
          • 并不适合用来处理大量时间事件
    • 惰性删除
      • 放任键过期不管,但是在每次从dict字典中取出键值时,要检查键是否过期,如果过期的话,就删除它,并返回空;如果没过期,就返回键值。
      • 优点
        • 对CPU时间来说是最友好的:
        • 它只会在取出键时进行检查,这可以保证删除操作只会在非做不可的情况下进行
        • 并且删除的目标仅限于当前处理的键,这个策略不会在删除其他无关的过期键上花费任何 CPU 时间
      • 缺点
        • 对内存是最不友好的:
        • 如果一个键已经过期,而这个键又仍然保留在数据库中,内存不会被释放
    • 定期删除
      • 每隔一段时间,对expires字典进行检查,删除里面的过期键。
      • 是前两种方案的折中选择:
        • 它每隔一段时间执行一次删除操作,
          • 并通过限制删除操作执行的时长和频率,籍此来减少删除操作对CPU时间的影响。
        • 另一方面,通过定期删除过期键,它有效地减少了因惰性删除而带来的内存浪费
     
    redis使用,惰性删除加上定期删除,这两个策略相互配合,可以在合理利用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
     
    

      

    过期键对AOF,RDB和复制的影响
    过期键会被保存在更新后的 RDB 文件、 AOF 文件或者重写后的 AOF 文件里面吗?
    附属节点会会如何处理过期键?处理的方式和主节点一样吗?
     
    更新后的RDB文件
    • 在创建新的 RDB 文件时,程序会对键进行检查,过期的键不会被写入到更新后的 RDB 文件中。
    • 因此,过期键对更新后的 RDB 文件没有影响。
     
    AOF 文件
    • 在键已经过期,但是还没有被惰性删除或者定期删除之前,这个键不会产生任何影响,AOF 文件也不会因为这个键而被修改。
    • 当过期键被惰性删除、或者定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地记录该键已被删除。
     
    例子:GET message 试图访问message 键值,但message已经过期
    服务器执行三个动作:
    • 从数据库中删除message
    • 追加一条DEL message 命令到AOF文件
    • 向客户端返回 NTL
     
    AOF 重写
    和 RDB 文件类似,当进行 AOF 重写时,程序会对键进行检查,过期的键不会被保存到重写后的 AOF 文件
    因此,过期键对重写后的 AOF 文件没有影响。
     
    复制
    当服务器带有附属节点时,过期键的删除由主节点统一控制:
    • 如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有附属节点发送一个 DEL 命令。
    • 如果服务器是附属节点,那么当它碰到一个过期键的时候,它会向程序返回键已过期的回复,但并不真正的删除过期键。
      • 因为程序只根据键是否已经过期、而不是键是否已经被删除来决定执行流程,
      • 所以这种处理并不影响命令的正确执行结果。
      • 当接到从主节点发来的DEL命令之后,附属节点才会真正的将过期键删除掉。
     
    数据库空间的收缩和扩展
     
    tryResizeHashTables 函数的完整定义如下:
     
    /*
    * 对服务器中的所有数据库键空间字典、以及过期时间字典进行检查,
    * 看是否需要对这些字典进行收缩。
    **
    * 如果字典的使用空间比率低于 REDIS_HT_MINFILL
    * 那么将字典的大小缩小,让 USED/BUCKETS 的比率 <= 1
    */
    void tryResizeHashTables(void) {
         int j;
        
         for (j = 0; j < server.dbnum; j++) {
              // 缩小键空间字典
              if (htNeedsResize(server.db[j].dict))
                   dictResize(server.db[j].dict);
     
                   // 缩小过期时间字典
                   if (htNeedsResize(server.db[j].expires))
                        dictResize(server.db[j].expires);
         }
    }
     
    小结
      • 数据库主要由 dict 和 expires 两个字典构成,其中 dict 保存键值对,而 expires 则保存键的过期时间。
      • 数据库的键总是一个字符串对象,而值可以是任意一种 Redis 数据类型,包括字符串、哈希、集合、列表和有序集。
      • expires 的某个键和 dict 的某个键共同指向同一个字符串对象,而 expires 键的值则是该键以毫秒计算的 UNIX 过期时间戳。
      • Redis 使用惰性删除和定期删除两种策略来删除过期的键。
      • 更新后的 RDB 文件和重写后的 AOF 文件都不会保留已经过期的键。
      • 当一个过期键被删除之后,程序会追加一条新的 DEL 命令到现有 AOF 文件末尾。
      • 当主节点删除一个过期键之后,它会显式地发送一条 DEL 命令到所有附属节点。
        • 附属节点即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,
        • 这样可以保证主节点和附属节点的数据总是一致的。
      • 数据库的 dict 字典和 expires 字典的扩展策略和普通字典一样。
        • 它们的收缩策略是:当节点的填充百分比不足 10% 时,将可用节点数量减少至大于等于当前已用节点数量。
  • 相关阅读:
    第八周学习进度总结
    全国(球)疫情信息可视化
    第六周学习进度总结
    手把手教你爬取B站弹幕!
    Xpath基础学习
    团队成员及选题介绍
    第五周学习进度
    课堂练习之疫情APP
    SpringMVC02
    06 | 链表(上):如何实现LRU缓存淘汰算法?
  • 原文地址:https://www.cnblogs.com/Aiapple/p/7266759.html
Copyright © 2011-2022 走看看