redis数据库设计与实现教程

作者:袖梨 2022-06-29

Redis数据库定义:

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    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 */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

dict

dict数组保存所有的数据库,Redis初始化的时候,默认会创建16个数据库
#define REDIS_DEFAULT_DBNUM     16

默认情况下,Redis客户端的目标数据库是0 号数据库,可以通过select命令切换。 注意,由于Redis缺少获取当前操作的数据库命令,使用select切换需要特别注意

读写数据库中的键值对的时候,Redis除了对键空间执行指定操作外,还有一些额外的操作:

    读取键之后(读和写操作都会先读取),记录键空间命中或不命中次数
    读取键之后,更新键的LRU
    读取时发现已经过期,会先删除过期键
    如果有客户端使用watch命令监视了key,会在修改后标记为dirty
    修改之后,会对dirty键计数器加1,用于持久化和复制
    如果开启了数据库通知,修改之后会发送相应通知

robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {
    robj *o = lookupKeyRead(c->db, key);
    if (!o) addReply(c,reply);
    return o;
}
robj *lookupKeyRead(redisDb *db, robj *key) {
    robj *val;
 
    //查询是否已经过期
    expireIfNeeded(db,key);
    val = lookupKey(db,key);
    if (val == NULL)
        server.stat_keyspace_misses++;
    else
        server.stat_keyspace_hits++;
    return val;
}
robj *lookupKey(redisDb *db, robj *key) {
    dictEntry *de = dictFind(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
 
        /* Update the access time for the ageing algorithm.
         * Don’t do it if we have a saving child, as this will trigger
         * a copy on write madness. */
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
            //设置lru时间
            val->lru = server.lruclock;
        return val;
    } else {
        return NULL;
    }
}

expires

通过exprire或者pexpire命令,可以设置键的TTL,如果键的TTL为0,会被自动删除。

expires字典保存了数据库中所有键的过期时间。

    过期字典的键是指向某个数据中的键对象
    过期字段的值是long long类型的整数,保存这个键的过期时间
    void expireCommand(redisClient *c) {
      expireGenericCommand(c,mstime(),UNIT_SECONDS);
    }
    void expireGenericCommand(redisClient *c, long long basetime, int unit) {
      robj *key = c->argv[1], *param = c->argv[2];
      long long when; /* unix time in milliseconds when the key will expire. */
    
      if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
          return;
    
      if (unit == UNIT_SECONDS) when *= 1000;
      when += basetime;
    
      /* No key, return zero. */
      if (lookupKeyRead(c->db,key) == NULL) {
          addReply(c,shared.czero);
          return;
      }
    
      /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
       * should never be executed as a DEL when load the AOF or in the context
       * of a slave instance.
       *
       * Instead we take the other branch of the IF statement setting an expire
       * (possibly in the past) and wait for an explicit DEL from the master. */
      if (when <= mstime() && !server.loading && !server.masterhost) {
          robj *aux;
    
          redisAssertWithInfo(c,key,dbDelete(c->db,key));
          server.dirty++;
    
          /* Replicate/AOF this as an explicit 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 {
          //放到expires字典中
          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;
      }
    }

过期键删除策略

    惰性删除:每次执行命令前,都会调用expireIfNeeded函数检查是否过期,如果已经过期,改函数会删除过期键
    定时删除:定时执行activeExpireCycleTryExpire函数
    expireIfNeeded
    int expireIfNeeded(redisDb *db, robj *key) {
      mstime_t when = getExpire(db,key);
      mstime_t now;
    
      if (when < 0) return 0; /* No expire for this key */
    
      /* Don’t expire anything while loading. It will be done later. */
      if (server.loading) return 0;
    
      /* If we are in the context of a Lua script, we claim that time is
       * blocked to when the Lua script started. This way a key can expire
       * only the first time it is accessed and not in the middle of the
       * script execution, making propagation to slaves / AOF consistent.
       * See issue #1525 on Github for more information. */
      now = server.lua_caller ? server.lua_time_start : mstime();
    
      /* If we are running in the context of a slave, return ASAP:
       * the slave key expiration is controlled by the master that will
       * send us synthesized DEL operations for expired keys.
       *
       * Still we try to return the right information to the caller,
       * that is, 0 if we think the key should be still valid, 1 if
       * we think the key is expired at this time. */
      if (server.masterhost != NULL) return now > when;
    
      /* Return when this key has not expired */
      if (now <= when) return 0;
    
      /* Delete the key */
      server.stat_expiredkeys++;
      propagateExpire(db,key);
      notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
          “expired“,key,db->id);
      return dbDelete(db,key);
    }

    activeExpireCycleTryExpire

    while (num–) {
      dictEntry *de;
      long long ttl;
    
      if ((de = dictGetRandomKey(db->expires)) == NULL) break;
      ttl = dictGetSignedIntegerVal(de)-now;
      if (activeExpireCycleTryExpire(db,de,now)) expired++;
      if (ttl < 0) ttl = 0;
      ttl_sum += ttl;
      ttl_samples++;
    }

AOF、RDB和复制功能对过期键的处理

    生成RDB文件时,已过期的键不会被保存到新的RDB文件中
    载入RDB文件:
        主服务器载入时,会忽略过期键
        从服务器载入时,都会被载入(但是很快会因为同步被覆盖)
    AOF写入,已过期未删除的键没有影响,被删除后,会追加一条del命令
    AOF重写,会对键进行检查,过期键不会保存到重写后的AOF文件
    复制:
        主服务器删除一个过期键后,会显式向所有从服务器发送DEL命令
        从服务器执行读命令,及时过期也不会删除,只有接受到主服务器DEL命令才会删除

相关文章

精彩推荐