redis源码浅析--八-数据库的实现

环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
参考书籍:《redis的设计与实现》
文章推荐:
redis源码阅读-一--sds简单动态字符串
redis源码阅读--二-链表
redis源码阅读--三-redis散列表的实现
redis源码浅析--四-redis跳跃表的实现
redis源码浅析--五-整数集合的实现
redis源码浅析--六-压缩列表
redis源码浅析--七-redisObject对象(下)(内存回收、共享)
redis源码浅析--八-数据库的实现
redis源码浅析--九-RDB持久化
redis源码浅析--十-AOF(append only file)持久化
redis源码浅析--十一.事件(上)文件事件
redis源码浅析--十一.事件(下)时间事件
redis源码浅析--十二.单机数据库的实现-客户端
redis源码浅析--十三.单机数据库的实现-服务端 - 时间事件
redis源码浅析--十三.单机数据库的实现-服务端 - redis服务器的初始化
redis源码浅析--十四.多机数据库的实现(一)--新老版本复制功能的区别与实现原理
redis源码浅析--十四.多机数据库的实现(二)--复制的实现SLAVEOF、PSYNY
redis源码浅析--十五.哨兵sentinel的设计与实现
redis源码浅析--十六.cluster集群的设计与实现
redis源码浅析--十七.发布与订阅的实现
redis源码浅析--十八.事务的实现
redis源码浅析--十九.排序的实现
redis源码浅析--二十.BIT MAP的实现
redis源码浅析--二十一.慢查询日志的实现
redis源码浅析--二十二.监视器的实现

目录

 

一 服务器中的数据库

二 切换数据

三 数据库键空间

四 键的过期时间

五 过期键删除策略

六删除策略的实现


一 服务器中的数据库

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

struct redisServer {
    /* General */
   ....
    redisDb *db; //数据库数组
   ....
    int dbnum;                      /* Total number of configured DBs */
}

初始化数据的数量 由dbnum来决定。默认是16个;


二 切换数据

每个redis客户端都有自己的目标数据库,客户端状态server.h/client结构的db属性记录来客户端当前的目标数据库;

/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct client {
    uint64_t id;            /* Client incremental unique ID. */
    int fd;                 /* Client socket. */
    redisDb *db;            /* Pointer to currently SELECTed DB. */
    .....
}

如果客户端使用select 命令修改了目标数据库,那么客户端的db指针将指定二号数据库,从而实现了切换目标数据库的功能;


三 数据库键空间

服务器中每个数据库是由server.h/redisDb结构来表示,其中dict字典保存来数据库中的所有键值对;

/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
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;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
  • 字典中的键就是数据库的键,每个键都是一个字符串对象
  • 字典的值就是数据库的值,值可以是字符串对象,列表对象、哈表表、集合、有序集合中的任一个redis对象

因为数据库的键空间是一个字典,所以所有针对数据库的操作,都是基于字典的操作来实现的。

1.添加新键
添加键的入口位于db.c/setKey ,数据库中的所有key都是通过这个方法来创建;
 

/*
数据库中所有的key 都应该通过这个接口创建
*/
/* High level Set operation. This function can be used in order to set
 * a key, whatever it was existing or not, to a new object.
 *
 * 1) The ref count of the value object is incremented.
 * 2) clients WATCHing for the destination key notified.
 * 3) The expire time of the key is reset (the key is made persistent).
 *
 * All the new keys in the database should be created via this interface. */
void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) { 
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val);//引用计数+1
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

/*
将key添加到db中,由调用折决定是否需要增加引用计数的值
*/
/* Add the key to the DB. It's up to the caller to increment the reference
 * counter of the value if needed.
 *
 * The program is aborted if the key already exists. */
void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);

    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    if (val->type == OBJ_LIST ||
        val->type == OBJ_ZSET)
        signalKeyAsReady(db, key);
    if (server.cluster_enabled) slotToKeyAdd(key);
}

可以看到添加键到数据库,最终是调用字典的dictAdd方法来实现的;

2.删除键

删除键时间上调用的字典dict.c/dictGenericDelete接口,删除键(如下代码中dictFreeKey(d, he)方法和键所对应的值对象(  如下代码中dictFreeVal方法):


/* Search and remove an element. This is an helper function for
 * dictDelete() and dictUnlink(), please check the top comment
 * of those functions. */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    //hash表是空的
    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);

    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        prevHe = NULL;
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}

3更新和查找操作

更新和查找操作 ,会根据值对象的不同类型,做相应的操作。
以get命令作为例子:

/*
获取字符类型的值命令实现
*/
int getGenericCommand(client *c) {
    robj *o;

    //db.c/lookupKeyReadOrReply 查找key是否在数据库中
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return C_OK;

    //检查值的类型是否是string
    if (o->type != OBJ_STRING) {
        addReply(c,shared.wrongtypeerr);
        return C_ERR;
    } else {
        addReplyBulk(c,o);
        return C_OK;
    }
}

void getCommand(client *c) {
    getGenericCommand(c);
}

4其他操作

除了增删改查外,还有很多针对数据库本身操作,如:
fulshdb 就是删除键空间中的所有键值对;
randdomkey 在键空间中随机返回一个键;
dbsize、exist、rename、keys等都是通过对键空间进行操作来实现的;


四 键的过期时间

通过EXPIRE或者PEXPIRE命令,可以设置某个key的的生存时间,在经过指定的秒数或者毫秒数之后,服务器就会删除生存时间是0的键;

1.设置过期时间

redis有四个命令可以设置键的生存时间 EXPIRE、PEXPIR、EXPIREAT、PEXPIREAT;
如下,可以看到这个四个命令的都是由exipre.c/expireGenericCommand函数来实现的;

/* 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);
}

2.保存过期时间

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

  • 过期字典的key指向键空间的某个键对象
  • 过期字典的值一个long long类型的整数(一个毫秒精度的时间戳)

db.c/setExipre


/* Set an expire to the specified key. If the expire is set in the context
 * of an user calling a command 'c' is the client, otherwise 'c' is set
 * to NULL. The 'when' parameter is the absolute unix time in milliseconds
 * after which the key will no longer be considered valid. */
void setExpire(client *c, redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    kde = dictFind(db->dict,key->ptr); //在键空间中查找指定key
    serverAssertWithInfo(NULL,key,kde != NULL);
    
    //在db的expire中查找key或者新增过期key(如果key之前没设置过过期时间则新增)
    de = dictAddOrFind(db->expires,dictGetKey(kde));

    dictSetSignedIntegerVal(de,when); //设置过期的时间

    int writable_slave = server.masterhost && server.repl_slave_ro == 0;
    if (c && writable_slave && !(c->flags & CLIENT_MASTER))
        rememberSlaveKeyWithExpire(db,key);
}

3.移除过期时间

移除过期时间相对简单写,只需要调用字典接口dictDelete 将key从过期字典删除即可;
db.c/removeExpire

int removeExpire(redisDb *db, robj *key) {
    /* An expire may only be removed if there is a corresponding entry in the
     * main dict. Otherwise, the key will never be freed. */
    serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    return dictDelete(db->expires,key->ptr) == DICT_OK;
}

4.获取过期时间

获取过期时间也很好理解,需要在过期字典中查找相应key即可;直接贴上TTL、PTTL命令的实现源码;

//expire.c/ttlGenericCommand
/* Implements TTL and PTTL */
void ttlGenericCommand(client *c, int output_ms) {
    long long expire, ttl = -1;

    //判断key是否存在,不存在返回-2
    /* If the key does not exist at all, return -2 */
    if (lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    
    //key存在但是没有过期时间,返回-1
    /* The key exists. Return -1 if it has no expire, or the actual
     * TTL value 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));
    }
}


//db.c/getExpire

/* Return the expire time of the specified key, or -1 if no expire
 * is associated with this key (i.e. the key is non volatile) */
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;

    /* No expire? return ASAP */
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;

    /* The entry was found in the expire dict, this means it should also
     * be present in the main dict (safety check). */
    serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    return dictGetSignedIntegerVal(de);
}

五 过期键删除策略

key的过期时间保存在一个过期数组中,那么key什么时候被删除呢?

  • 定时删除
    在设置过期时间同时,创建一个定时器,让定时器在键过期时间来临时,立即执行删除操作
  • 惰性删除
    放任键过期不管,但每次在键空间中获取键时,判断是否过期,进行删除操作
  • 定期删除
    每隔一段时间,程序对数据进行一次检查,删除里面的过期键。(要检查多少个数据库,删除多少键,由算法决定)

1.定时删除

定时删除对内存友好,定时删除可以保证过期的键会尽快地被删除,释放键所占用的空间;

但是对cpu不友好,当过期键比较多的情况下,删除过期键这一行为会占用相当一部分cpu时间,会对服务器相应时间和吞吐量造成一定影响

2.惰性删除

惰性删除对cpu时间来说是最友好的:程序只会在取键时进行过期检查,并且检查只限于当前操作的键;

缺点是对内存不友好,如果数据库中很多键已经过期,而这些键又没有被访问到,那么这些键也去永远不会被删除;

3.定期删除

定期删除可以看作是前两种策略侧折中;

定期删除每隔一点时间进行一次删除操作,并限定执行时间和频率,以减少对cpu操作时间的影响;


六删除策略的实现

1.惰性删除

惰性删除策略实现位于db.c/expireIfNeeded;

db.c中数据库键值查找函数(lookupKeyXXXX函数) 的都会调用expireIfNeeded方法,来检查key是否过期
 

int expireIfNeeded(redisDb *db, robj *key) {
    if (!keyIsExpired(db,key)) return 0;

    /* If we are running in the context of a slave, instead of
     * evicting the expired key from the database, we 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 1;

    /* Delete the 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);
}

2.定期删除

定期删除策略的实现位于db.c/activeExpireCycle,每当redis的服务器周期性操作server.c/databasesCron函数执行时,activeExpireCycle就都会执行;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值