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命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。

 

 

 

 

 

 

 

posted @ 2018-06-08 10:55  冬日降临  阅读(6591)  评论(1编辑  收藏  举报