• Redis学习笔记(三)redis 的键管理


    Redis 的键管理

    一、Redis 数据库管理

    Redis 是一个键值对(key-value pair)的数据库服务器,其数据保存在 src/server.h/redisDb 中(网上很多帖子说在 redis.h 文件中,但是 redis 6.x版本目录中都没有这个文件。redisDb 结构应该在 server.h文件中)

    typedef redisServer {
        ....
        
        // Redis数据库
        redisDb *db;
        
        ....
    }
    

    Redis 默认会创建 16 个数据库,每个数据库是独立互不影响。其默认的目标数据库是 0 号数据库,可以通过 select 命令来切换目标数据库。在 redisClient 结构中记录客户端当前的目标数据库:

    typedef struct redisClient {
    
        // 套接字描述符
        int fd;
    
        // 当前正在使用的数据库
        redisDb *db;
    
        // 当前正在使用的数据库的 id (号码)
        int dictid;
    
        // 客户端的名字
        robj *name;             /* As set by CLIENT SETNAME */
    
    } redisClient;
    

    下面是客户端和服务器状态之间的关系实例,客户端的目标数据库目前为 1 号数据库:

    通过修改 redisClient.db 的指针来指向不同数据库,这也就是 select 命令的实现原理。但是,到目前为止,Redis 仍然没有可以返回客户端目标数据库的命令。虽然在 redis-cli 客户端中输入时会显示:

    redis> SELECT 1
    Ok
    redis[1]> 
    

    但是在其他语言客户端没有显示目标数据库的号端,所以在频繁切换数据库后,会导致忘记目前使用的是哪一个数据库,也容易产生误操作。因此要谨慎处理多数据库程序,必须要执行时,可以先显示切换指定数据库,然后再执行别的命令。

    二、Redis 数据库键

    2.1 数据库键空间

    Redis 服务器中的每一个数据库是由一个 server.h/redisDb 结构来表示的,其具体结构如下:

    typedef struct redisDb {
        //数据库键空间
        dict *dict;                 /* The keyspace for this DB */
        //键的过期时间,字典的值为过期事件 UNIX 时间戳
        dict *expires;              /* Timeout of keys with a timeout set */
        //正处于阻塞状态的键
        dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
        //可以解除阻塞的键
        dict *ready_keys;           /* Blocked keys that received a PUSH */
        //正在被 WATCH 命令监视的键
        dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
        //数据库号端
        int id;                     /* Database ID */
        //数据库键的平均 TTL,统计信息
        long long avg_ttl;          /* Average TTL, just for stats */
        //
        unsigned long expires_cursor; /* Cursor of the active expire cycle. */
        list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    } redisDb;
    

    键空间和用户所见的数据库是直接对应:

    • 键空间的 key 就是数据库的 key, 每个 key 都是一个字符串对象
    • 键空间的 value 是数据库的 value, 每个 value 可以是字符串对象、列表对象和集合对象等等任意一种 Redis 对象

    举个实例,若在空白数据库中执行一下命令:插入字符串对象、列表对象和哈希对象

    # 插入一个字符串对象
    redis> SET message "hello world"
    OK
    # 插入包含三个元素的列表对象
    redis> RPUSH alphabet "a" "b" "c"
    (integer)3
    # 插入包含三个元素的哈希表对象
    redis> HSET book name "Redis in Action"
    (integer) 1
    redis> HSET book author "Josiah L. Carlson"
    (integer) 1
    redis> HSET book publisher "Manning"
    (integer) 1
    

    所以说 redis 对数据的增删改查是通过操作 dict 来操作 Redis 中的数据

    2.2 数据库键的过期

    我们可以通过两种方式设置键的生命周期:

    • 通过 EXPIRE 或者 PEXPIRE 命令来为数据库中的某个键设置生存时间(TTL,Time To Live)。在经过 TTL 个生存时间后,服务器会自动删除生存时间为0 的键。比如:

      redis> set key value
      OK
      # 设置键的 TTL 为 5
      redis> EXPIRE key 5
      (integer)1
      
    • 此外,客户端也可以通过 EXPIREAT 或者PEXPIREAT 命令,为数据库中的某个键设置过期时间(expire time)。过期时间是一个 UNIX 时间戳,当过期时间来临时,服务器就会自动从数据库中删除这个键。比如

      redis> SET key value
      OK
      redis> EXPIREAT key 1377257300
      (integer) 1
      # 当前系统时间
      redis> TIME
      1)"1377257296"
      # 过一段时间后,再查询key 
      redis> GET key // 1377257300
      (nil)
      

    2.2.1 过期时间

    redisDb 中的dict *dictdict *expires 字典 分别保存了数据库中的键和键的过期时间,分别叫做键空间过期字典

    • 过期字典的键是一个指向键空间中的某个键对象
    • 过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间

    2.3 过期键的删除策略

    对于已经过期的数据是如何删除这些过期键的呢?主要有两种方式:惰性删除和定期删除:

    1.惰性删除

    是指 Redis 服务器不主动删除过期的键值,而是通过访问键值时,检查当前的键值是否过期

    • 如果过期则执行删除并返回 null
    • 没有过期则正常访问值信息给客户端

    惰性删除的源码在 src/db.c/expireIfNeeded 方法中

    int expireIfNeeded(redisDb *db, robj *key) {
    
        // 判断键是否过期
    
        if (!keyIsExpired(db,key)) return 0;
    
        if (server.masterhost != NULL) return 1;
    
        /* 删除过期键 */
    
        // 增加过期键个数
    
        server.stat_expiredkeys++;
    
        // 传播键过期的消息
    
        propagateExpire(db,key,server.lazyfree_lazy_expire);
    
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
    
            "expired",key,db->id);
    
        // server.lazyfree_lazy_expire 为 1 表示异步删除,否则则为同步删除
    
        return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
    
                                             dbSyncDelete(db,key);
    
    }
    
    // 判断键是否过期
    
    int keyIsExpired(redisDb *db, robj *key) {
    
        mstime_t when = getExpire(db,key);
    
        if (when < 0) return 0; 
    
        if (server.loading) return 0;
    
        mstime_t now = server.lua_caller ? server.lua_time_start : mstime();
    
        return now > when;
    
    }
    
    // 获取键的过期时间
    
    long long getExpire(redisDb *db, robj *key) {
    
        dictEntry *de;
    
        if (dictSize(db->expires) == 0 ||
    
           (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    
        serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    
        return dictGetSignedIntegerVal(de);
    
    }
    

    2.定期删除

    与惰性删除不同,定期删除是指 Redis 服务器会每隔一段时间就会检查一下数据库,看看是否有过期键可以清除,默认情况下,Redis 定期检查的频率是每秒扫描 10 次,这个值在 redis.conf 中的 "hz" , 默认是 10 ,可以进行修改。

    定期删除的扫描并不是遍历所有的键值对,这样的话比较费时且太消耗系统资源。Redis 服务器采用的是随机抽取形式,每次从过期字典中,取出 20 个键进行过期检测,过期字典中存储的是所有设置了过期时间的键值对。如果这批随机检查的数据中有 25% 的比例过期,那么会再抽取 20 个随机键值进行检测和删除,并且会循环执行这个流程,直到抽取的这批数据中过期键值小于 25%,此次检测才算完成。

    定期删除的源码在 expire.c/activeExpireCycle 方法中:

    void activeExpireCycle(int type) {
    
        static unsigned int current_db = 0; /* 上次定期删除遍历到的数据库ID */
    
        static int timelimit_exit = 0;      
    
        static long long last_fast_cycle = 0; /* 上次执行定期删除的时间点 */
    
        int j, iteration = 0;
    
        int dbs_per_call = CRON_DBS_PER_CALL; // 需要遍历数据库的数量
    
        long long start = ustime(), timelimit, elapsed;
    
        if (clientsArePaused()) return;
    
        if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
    
            if (!timelimit_exit) return;
    
            // ACTIVE_EXPIRE_CYCLE_FAST_DURATION 快速定期删除的执行时长
    
            if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
    
            last_fast_cycle = start;
    
        }
    
        if (dbs_per_call > server.dbnum || timelimit_exit)
    
            dbs_per_call = server.dbnum;
    
        // 慢速定期删除的执行时长
    
        timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    
        timelimit_exit = 0;
    
        if (timelimit <= 0) timelimit = 1;
    
        if (type == ACTIVE_EXPIRE_CYCLE_FAST)
    
            timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* 删除操作花费的时间 */
    
        long total_sampled = 0;
    
        long total_expired = 0;
    
        for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
    
            int expired;
    
            redisDb *db = server.db+(current_db % server.dbnum);
    
            current_db++;
    
            do {
    
                // .......
    
                expired = 0;
    
                ttl_sum = 0;
    
                ttl_samples = 0;
    
                // 每个数据库中检查的键的数量
    
                if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
    
                    num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
    
                // 从数据库中随机选取 num 个键进行检查
    
                while (num--) {
    
                    dictEntry *de;
    
                    long long ttl;
    
                    if ((de = dictGetRandomKey(db->expires)) == NULL) break;
    
                    ttl = dictGetSignedInteger
    
                    // 过期检查,并对过期键进行删除
    
                    if (activeExpireCycleTryExpire(db,de,now)) expired++;
    
                    if (ttl > 0) {
    
                        ttl_sum += ttl;
    
                        ttl_samples++;
    
                    }
    
                    total_sampled++;
    
                }
    
                total_expired += expired;
    
                if (ttl_samples) {
    
                    long long avg_ttl = ttl_sum/ttl_samples;
    
                    if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
    
                    db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
    
                }
    
                if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
    
                    elapsed = ustime()-start;
    
                    if (elapsed > timelimit) {
    
                        timelimit_exit = 1;
    
                        server.stat_expired_time_cap_reached_count++;
    
                        break;
    
                    }
    
                }
    
                /* 判断过期键删除数量是否超过 25% */
    
            } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    
        }
    
        // .......
    
    }
    

    以上就是Redis 的删除策略。下面来看一个面试题:

    面试题:你知道 Redis 内存淘汰策略和键的删除策略的区别吗?

    Redis 内存淘汰策略

    我们可以通过 config get maxmemory-policy 命令来查看当前 Redis 的内存淘汰策略:

    127.0.0.1:6379> config get maxmemory-policy
    
    1) "maxmemory-policy"
    
    2) "noeviction"
    

    当前服务器设置的是 noeviction 类型的,对于 redis 6.x版本,主要有以下几种内存淘汰策略

    • noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,它是 Redis 默认内存淘汰策略。
    • allkeys-lru:淘汰整个键值中最久未使用的键值。
    • allkeys-random:随机淘汰任意键值。
    • volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值。
    • volatile-random:随机淘汰设置了过期时间的任意键值。
    • volatile-ttl:优先淘汰更早过期的键值。
    • volatile-lfu: 淘汰所有设置了过期时间的键值中最少使用的键值。
    • alkeys-lfu: 淘汰整个键值中最少使用的键值

    也就是 alkeys 开头的表示从所有键值中淘汰相关数据,而 volatile 表示从设置了过期键的键值中淘汰数据。

    Redis 内存淘汰算法

    内存淘汰算法主要分为 LRU 和 LFU 淘汰算法

    LRU(Least Recently Used) 淘汰算法

    是一种常用的页面置换算法,LRU 是基于链表结构实现,链表中的元素按照操作顺序从前往后排列。最新操作的键会被移动到表头,当需要进行内存淘汰时,只需要删除链表尾部的元素。

    Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间。Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,随机取5个值,然后淘汰最久没有使用的数据。

    LFU(Least Frequently Used)淘汰算法

    根据总访问次数来淘汰数据,核心思想是如果数据过去被访问多次,那么将来被访问的频率也更高

    参考资料

    《Redis 设计与实现》
    https://kaiwu.lagou.com/course/courseInfo.htm?courseId=59#/detail/pc?id=1779

  • 相关阅读:
    HDU 1465 不容易系列之一(错排,递归)
    HDU 1397 Goldbach's Conjecture(二分,查找素数)
    HDU 1393 Weird Clock (英语,纪念题)
    HDU 1163 Eddy's digital Roots(模)
    HDU 1098 Ignatius's puzzle(数学归纳)
    HDU 1028 Ignatius and the Princess III (递归,dp)
    字符串方法
    __name__和__main的含义
    list 和 str
    python_元组
  • 原文地址:https://www.cnblogs.com/EthanWong/p/15866054.html
Copyright © 2020-2023  润新知