zoukankan      html  css  js  c++  java
  • Redis数据库

    Redis数据库

     

    1.Redis服务器

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

    struct redisServer {
        // ...
        //
        一个数组,保存着服务器中的所有数据库
        redisDb *db;
        // ...
       //服务器的数据库数量
        int dbnum;
    };

    dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,所以Redis服务器默认会创建16个数据库

    默认情况下,Redis客户端的目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库

     

    2.Redis客户端

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

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

    3.Redis数据库

    typedef struct redisDb {
        dict *dict;                   /* 当前数据库的键空间 */
        dict *expires;                /* 键的过期时间 */
        dict *blocking_keys;    /* 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)*/
        dict *ready_keys;        /* 准备好数据可以解除阻塞状态的键和相应的client */
        dict *watched_keys;     /* 被watch命令监控的key和相应client */
        int id;                          /* 数据库ID标识 */
        long long avg_ttl;         /* 数据库内所有键的平均TTL(生存时间) */
        list *defrag_later;         /*逐一尝试整理碎片的关键名称列表 */
    } redisDb;

    redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space) 键空间和用户所见的数据库是直接对应的:

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

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

    4.Redis键过期时间和过期时间查询TTL

    通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。

    expires字段也是一个字典dict结构,字典的键为key,值为该key对应的过期时间,过期时间为long long类型整数,是以毫秒为单位的过期 UNIX 时间戳。setExpire函数的作用是为指定key设置过期时间。

    /* 为指定key设置过期时间 */
    void setExpire(redisDb *db, robj *key, long long when) {
        dictEntry *kde, *de;
    
        /* Reuse the sds from the main dict in the expire dict */
        // db->dict和db->expires是共用key字符串对象的
        // 取出key
        kde = dictFind(db->dict,key->ptr);
        redisAssertWithInfo(NULL,key,kde != NULL);
        // 取出过期时间
        de = dictReplaceRaw(db->expires,dictGetKey(kde));
        // 重置key的过期时间
        dictSetSignedIntegerVal(de,when);
    }

    有了过期时间戳我们就很容易判断某个key是否过期:只要将当前时间戳跟过期时间戳比较一下即可,如果当前时间戳大于过期时间戳显然该key已经过期了。

    在Redis中,如果没有为一个key设置过期时间,那么该key就不会出现在db->expires字典中。也就是说db->expires字段只保存了设置有过期时间的key。

    • 设置过期时间

    Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):(expire.c中)

      ❑EXPIRE<key><ttl>命令用于将键key的生存时间设置为ttl秒

      ❑PEXPIRE<key><ttl>命令用于将键key的生存时间设置为ttl毫秒

      ❑EXPIREAT<key><timestamp>命令用于将键key的过期时间设置为timestamp所指定的秒数时间戳

      ❑PEXPIREAT<key><timestamp>命令用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳

    EXPIREAT命令与EXPIRE命令的差别在于前者使用Unix时间作为第二个参数表示键的生存时间的截止时间。PEXPIREAT命令与EXPIREAT命令的区别是前者的时间单位是毫秒。

    虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样。

    /* EXPIRE key seconds */
    void expireCommand(client *c) {
        expireGenericCommand(c,mstime(),UNIT_SECONDS);
    }
    
    /* EXPIREAT key time */
    void expireatCommand(client *c) {
        expireGenericCommand(c,0,UNIT_SECONDS);
    }
    
    /* PEXPIRE key milliseconds */
    void pexpireCommand(client *c) {
        expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
    }
    
    /* PEXPIREAT key ms_time */
    void pexpireatCommand(client *c) {
        expireGenericCommand(c,0,UNIT_MILLISECONDS);
    }
    
    
    void expireGenericCommand(redisClient *c, long long basetime, int unit) {
        robj *key = c->argv[1], *param = c->argv[2];
        // 以毫秒为单位的unix时间戳
        long long when;    // 获取过期时间
        if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
            return;
    
        // 如果传入的过期时间是以秒为单位,则转换为毫秒为单位
        if (unit == UNIT_SECONDS) when *= 1000;
        // 加上basetime得到过期时间戳
        when += basetime;
    
        /* No key, return zero. */
        // 取出key,如果该key不存在直接返回
        if (lookupKeyRead(c->db,key) == NULL) {
            addReply(c,shared.czero);
            return;
        }
        if (when <= mstime() && !server.loading && !server.masterhost) {
            // 如果when指定的时间已经过期,而且当前为服务器的主节点,并且目前没有载入数据
            robj *aux;
            redisAssertWithInfo(c,key,dbDelete(c->db,key));
            server.dirty++;
            // 传播一个显式的DEL命令
            aux = createStringObject("DEL",3);
            rewriteClientCommandVector(c,2,aux,key);
            decrRefCount(aux);
            signalModifiedKey(c->db,key);
            notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
            addReply(c, shared.cone);
            return;
        } else {
            // 设置key的过期时间(when提供的时间可能已经过期)
            setExpire(c->db,key,when);
            addReply(c,shared.cone);
            signalModifiedKey(c->db,key);                  notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
            server.dirty++;
            return;
        }
    }
    • TTL命令

    TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间

    /* TTL key */
    void ttlCommand(client *c) {
        ttlGenericCommand(c, 0);
    }
    
    /* PTTL key */
    void pttlCommand(client *c) {
        ttlGenericCommand(c, 1);
    }
    
    void ttlGenericCommand(client *c, int output_ms) {
        long long expire, ttl = -1;
    
        /* 如果这个键不存在 return -2 */
        if (lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH) == NULL) {
            addReplyLongLong(c,-2);
            return;
        }
        /* 键存在. Return -1 if 已经过期, or the 实际的TTL值otherwise. */
        expire = getExpire(c->db,c->argv[1]);
        if (expire != -1) {
            ttl = expire-mstime();
            if (ttl < 0) ttl = 0;
        }
        if (ttl == -1) {
            addReplyLongLong(c,-1);
        } else {
            addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
        }
    }
    • 过期键删除策略

    如果一个键过期了,那么它什么时候会被删除呢?这个问题有三种可能的答案,它们分别代表了三种不同的删除策略:

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

      ❑惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。

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

    在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。

    1)过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:

      ❑如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除。

      ❑如果输入键未过期,那么expireIfNeeded函数不做动作。

    对于过期的key,Redis负责将该key删除,为了提高运行效率,Redis采取这么一种处理方式:只有当真正要访问该key时才检查该key是否过期。如果过期就删除,如果没过期就正常访问。通常我们把这种只有在访问时才检查过期的策略叫做“惰性删除”。

    int expireIfNeeded(redisDb *db, robj *key) {
        // 获取key的过期时间
        mstime_t when = getExpire(db,key);
        mstime_t now;
    
        // 如果该key没有过期时间,返回0
      if (when < 0) return 0; 
    
        // 如果服务器正在加载操作中,则不进行过期检查,返回0
      if (server.loading) return 0;
    
        //如果我们处在Lua脚本的上下文中,我们假设直到Lua脚本启动时间不变的。 
        //通过这种方式,key只能在第一次访问而不是在脚本执行过程中过期,
        //从而使slave/AOF传播一致。
      now = server.lua_caller ? server.lua_time_start : mstime();
    
        // 如果当前程序运行在slave节点,该key的过期操作是由master节点控制的(master节点会发出DEL操作)
        // 在这种情况下该函数先返回一个正确值,即如果key未过期返回0,否则返回1。
        // 真正的删除操作等待master节点发来的DEL命令后再执行
        if (server.masterhost != NULL) return now > when;
    
        // 如果未过期,返回0
        if (now <= when) return 0;
    
        // 如果已过期,删除该key
        server.stat_expiredkeys++;
        propagateExpire(db,key,server.lazyfree_lazy_expire);
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",key,db->id);
        return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key);
    }

    对于过期key,Redis主(master)节点和附属(slave)节点有不同的处理策略,具体如下:

    如果当前Redis服务器是主节点,即if (server.masterhost != NULL)语句判断为false,那么当它发现一个过期key后,会调用propagateExpire函数向所有附属节点发送一个 DEL 命令,然后再删除该key。这种做法使得对key的过期操作可以集中在一个地方处理。

    如果当前Redis服务器是附属节点,即if (server.masterhost != NULL)语句判断为true,那么它立即向程序返回该key是否已经过期的信息。即便该key已经过期也不会真正的删除该key。直到该节点接到从主节点发来的DEL 命令之后,才会真正执行删除操作。

    当Redis从数据库db中取出指定key的对象时,总是先调用调用expireIfNeeded函数来检查对应key是否过期,然后再从数据库中查找对象。

    robj *lookupKeyRead(redisDb *db, robj *key) {
        return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
    }
    
    robj *lookupKeyReadWithFlags (redisDb *db, robj *key, int flags) {
        robj *val;
        // 如果key已过期,删除该key
        if (expireIfNeeded(db,key) == 1) {
            /*密钥过期, 如果当前为master,expireIfNeeded(),仅当密钥不存在时才返回0,所以它很安全,尽快返回NULL*/
            if (server.masterhost == NULL) return NULL;
    
            /* 如果当前处于slave节点,expireIfNeeded只返回信息*/
            if (server.current_client &&
                server.current_client != server.master &&
                server.current_client->cmd &&
                server.current_client->cmd->flags & CMD_READONLY)
            {
                return NULL;
            }
    }    
    // 从数据库db中找到指定key的对象
        val = lookupKey(db,key, flags);
        if (val == NULL)
            // 更新“未命中”次数
            server.stat_keyspace_misses++;
        else
            // 更新“命中”次数
            server.stat_keyspace_hits++;
        return val;
    }
    /*  该函数是为写操作而从数据库db中取出指定key的对象。
        如果敢函数执行成功则返回目标对象,否则返回NULL。*/
    robj *lookupKeyWrite(redisDb *db, robj *key) {
        // 如果key已过期,删除该key
    expireIfNeeded(db,key);
    // 从数据库db中找到指定key的对象
        return lookupKey(db,key,LOOKUP_NONE);
    }

    2)过期键的定期删除策略由expire.c/activeExpireCycle函数实现,周期性过期是通过周期心跳函数(serverCron)来触发的,每当Redis的服务器周期性操作serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。

    //周期性操作中进行慢速过期键删除,执行频率同databasesCron的执行频率  
    //执行时长为1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100  
    void databasesCron(void) {  
        if (server.active_expire_enabled && server.masterhost == NULL) {  
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);  
        } else if (server.masterhost != NULL) {  
            expireSlaveKeys();  
        }  
    }//进行快速过期键删除,执行间隔和执行时长都为ACTIVE_EXPIRE_CYCLE_FAST_DURATION  
    void beforeSleep(struct aeEventLoop *eventLoop) {  
        if (server.active_expire_enabled && server.masterhost == NULL)  
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);  
    }  
    void activeExpireCycle(int type) {
      static unsigned int current_db = 0; /* 上次定期删除遍历到的数据库id*/
      static int timelimit_exit = 0;      /* Time limit hit in previous call? */
      static long long last_fast_cycle = 0; /* 上一次执行快速定期删除的时间点 */
    
      int j, iteration = 0;
      int dbs_per_call = CRON_DBS_PER_CALL;//每次定期删除,遍历的数据库的数量
      long long start = ustime(), timelimit;   /*当客户端暂停时,数据集应该是静态的,不仅仅客户端的命令无法写入的,而且key到期的指令无法执行*/   if (clientsArePaused()) return;   if (type == ACTIVE_EXPIRE_CYCLE_FAST) { if (!timelimit_exit) return; //快速定期删除的时间间隔是ACTIVE_EXPIRE_CYCLE_FAST_DURATION //ACTIVE_EXPIRE_CYCLE_FAST_DURATION是快速定期删除的执行时长 if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; last_fast_cycle = start;   }   //我们通常应该每次迭代测试CRON_DBS_PER_CALL   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; j++) {     int expired;     redisDb *db = server.db+(current_db % server.dbnum);     current_db++;     do {       unsigned long num, slots;       long long now, ttl_sum;       int ttl_samples;       iteration++;       //如果没有要删除的键就转向下一个数据库       if ((num = dictSize(db->expires)) == 0) {         db->avg_ttl = 0;         break;       }       slots = dictSlots(db->expires);       now = mstime();       //当槽的填充小于1%,key显得很重要,因此会等待更好的时机进行键清除。       if (num && slots > DICT_HT_INITIAL_SIZE && (num*100/slots < 1)) break;     }   }   expired = 0;   ttl_sum = 0;   ttl_samples = 0;   //在每个数据库中检查的键的数量   if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)     num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;   //从db->expires中随机选取num个键进行检查   while (num--) {     if ((de = dictGetRandomKey(db->expires)) == NULL) break;       ttl = dictGetSignedIntegerVal(de)-now;       //过期检查,并对过期键进行删除       if (activeExpireCycleTryExpire(db,de,now)) expired++;         if (ttl > 0) {         /* We want the average TTL of keys yet not expired. */ ttl_sum += ttl; ttl_samples++; } total_sampled++; }       //更新平均过期时间 if (ttl_samples) { long long avg_ttl = ttl_sum/ttl_samples; //用几个样本做一个简单的运行平均值。我们只使用当前的估计值,权重为2%和以前的估计98%的权重。 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;         }       } } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); //每次检查只删除ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4个过期键 } }

     activeExpireCycle函数的工作模式可以总结如下:

    ❑函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。

    ❑全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle函数在遍历10号数据库时返回了,那么下次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键。

    ❑随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作。

    总结:

    ❑Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则由redisServer.dbnum属性保存。

    ❑客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同的数据库。

    ❑数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。

    ❑因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。

    ❑数据库的键总是一个字符串对象,而值则可以是任意一种Redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键、哈希表键、集合键、列表键和有序集合键。

    ❑expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间戳。

    ❑Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。

    ❑执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已经过期的键。

    ❑执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。

    ❑当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。

    ❑当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。

    ❑从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。

    ❑当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。

     

  • 相关阅读:
    Kubernetes之(四)kubeadm部署集群
    Kubernetes之(三)核心组件ETCD介绍
    Kubernetes之(二)核⼼技术概念和API对象
    Kubetnetes之(一)基础介绍
    docker入门基础(六)
    docker入门基础(八)
    docker入门基础(七)
    docker入门基础(五)
    docker入门基础(四)
    docker入门基础(三)
  • 原文地址:https://www.cnblogs.com/winterfells/p/9154181.html
Copyright © 2011-2022 走看看