redis数据库(CRUD)

本文深入解析Redis数据库的CRUD操作,包括新增键值对、修改键值对和删除键的实现细节。重点介绍了如何在redisDb结构中存储键值对,以及在不同操作中涉及的字典操作,如dictAdd、dictReplace和dictDelete。同时,提到了在集群模式下如何处理键的位置,并简要讨论了异步和同步删除策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在server.h中定义了redisServer结构,下面截取其中和数据库有关的两个部分

struct redisServer {
    redisDb *db; /* 数据库数组,所有的数据库都存放在这里 */
    int dbnum; /* 数据库总数 */
    //...
};

可以看到,数据库是存放在redisDb结构的数组里的,下面是redisDb的结构

/* 数据库编号从0开始,默认情况下会使用0号数据库 */
typedef struct redisDb {
    dict *dict;                 /* 键空间 */
    dict *expires;              /* 键的过期时间 */
    dict *blocking_keys;        /* 客户端等待数据的键(BLPOP)*/
    dict *ready_keys;           /* 阻塞键 */
    dict *watched_keys;         /* 监听键 */
    int id;                     /* 数据库编号,从0开始 */
    long long avg_ttl;          /* 平均ttl */
} redisDb;

其中id表示当前数据库的编号,一般默认使用第0号数据库,可以通过命令切换数据库,例如切换到1号数据库:

来看看切换数据库的实现,非常简单,就是在redisDb数组中获取下标为id的那个DB:

int selectDb(client *c, int id) {
    /* 校验id,server.dbnum就是数据库的总数 */
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    c->db = &server.db[id];
    return C_OK;
}

在redisDb结构中还有一个字典数组来存放所有的键值对,这里引用《redis设计与实现》一书中的示例来展示键值对在DB中的存储:

图中的StringObject、HashObject等对应着redis的对象,它们的实现可以查看前几篇介绍redis数据结构的文章。

对数据库的键值对做修改,其实就是对字典数组做修改。


例如,我们输入 set msg "hello world" 命令,这时会存在两种情况

  1. msg 这个键不存在,那么新增msg这个键,并且赋值为“hello world”
  2. msg 已经存在,那么复写msg所对应的value为“hello world”,其实就是“修改”

以下就是db中对这两种情况的实现:

void setKey(redisDb *db, robj *key, robj *val) {
    /* 去dict中根据key获取value,有则返回value,没有则返回NULL */
    if (lookupKeyWrite(db,key) == NULL) {/* 如果key不存在 */
        /* 新建 */
        dbAdd(db,key,val);
    } else {/* 如果key存在 */
        /* 修改 */
        dbOverwrite(db,key,val);
    }
    /* val的引用计数+1 */
    incrRefCount(val);
    /* 如果key过期了,那么移除key */
    removeExpire(db,key);
    /* 通知“这个key已经被修改了” */
    signalModifiedKey(db,key);
}

以下来看看新增和修改的具体实现


新增键值对

来看看它的实现

/*
 * 参数:当前db,
 * key,value是一个redisObject指针,它们的ptr指针指向底层数据结构的实现
 */
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) signalListAsReady(db, key);
    if (server.cluster_enabled) slotToKeyAdd(key);
 }

第一步:sdsup方法,创建一个sds,就是键:

/* 复制一个SDS */
sds sdsdup(const sds s) {
    /* 这里sdslen方法是根据选择具体的SDS结构 */
    /* 然后通过sdsnewlen方法构造一个新的sds */
    return sdsnewlen(s, sdslen(s));
}

第二步:dictAdd方法,就是往db的dict数组中添加K-V了,成功返回0,失败返回1(这里的具体操作写在了《redis中字典的add操作(hash算法、rehash)》里面,这里不再重复):

int dictAdd(dict *d, void *key, void *val)
{
    /* 创建entry */
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    /* 赋值 */
    dictSetVal(d, entry, val);
    return DICT_OK;
}

第三步:serverAssertWithInfo 可以简单理解成校验是否添加成功,如果添加K-V失败,那么程序会中止。

第四步:如果val指向的redisObject的底层是由list实现的:

/* 如果客户端有key阻塞以等待PUSH,那么把key放入server中的ready_keys中去 */
void signalListAsReady(redisDb *db, robj *key) {
    readyList *rl;

    /* 如果客户端没有key阻塞,那就直接返回不做其他操作 */
    if (dictFind(db->blocking_keys,key) == NULL) return;

    /* key已经存在,那么不需要重复放入ready_keys中 */
    if (dictFind(db->ready_keys,key) != NULL) return;

    /* 下面的操作就是将key放入ready_keys中 */
    rl = zmalloc(sizeof(*rl));
    rl->key = key;
    rl->db = db;
    /* key的引用计数++ */
    incrRefCount(key);
    /* 添加到尾节点 */
    listAddNodeTail(server.ready_keys,rl);

    /* 这里是避免key重复放入 */
    incrRefCount(key);
    /* 判断是否操作成功 */
    serverAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);
}

第五步:如果开启了cluster(集群),那么把key放进相应的槽里,是为了方便在获取key的value时可以通过算法快速找到key所在的位置。这里涉及到redis-cluster的原理,不多做介绍。

自此,添加键值对的操作的完成了。


修改键值对:

来看看它的实现

/* 这个方法不会更新key的expire time
 * 如果key不存在,方法中止
 */
void dbOverwrite(redisDb *db, robj *key, robj *val) {
    /* 根据key,在dict中找到entry节点 */
    dictEntry *de = dictFind(db->dict,key->ptr);
    
    serverAssertWithInfo(NULL,key,de != NULL);
    
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {/* 这一步会设置并更新lru属性 */
        robj *old = dictGetVal(de);
        int saved_lru = old->lru;
        dictReplace(db->dict, key->ptr, val);
        val->lru = saved_lru;
        updateLFU(val);
    } else {
        dictReplace(db->dict, key->ptr, val);
    }
}

第一步:dictFind方法,查找entry

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;
    
    /* 如果ht[0]和ht[1]都是空的,那么说明这两hash表中没有任何key,返回NULL */
    if (d->ht[0].used + d->ht[1].used == 0) return NULL; 
    /* rehash操作 */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    /* 对key进行hash */
    h = dictHashKey(d, key);
    /* 以下就是循环ht[0]和ht[1]中的table数组,查找key,找到就返回entry,否则返回NULL */
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

第二步:dictReplace方法,来看看它的实现:

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing, auxentry;

    /* 这里会尝试去新增一个entry,如果key已经存在了,那么entry==NULL */
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        /* 如果entry创建成功,说明key是不存在的,那么赋值,并且返回1 */
        dictSetVal(d, entry, val);
        return 1;
    }

    /* 赋新值,并且释放旧value的空间,返回0 */
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

至此,修改操作就完成了


当我们输入del msg 命令时,db是怎样实现的呢,下面来看看

删除键:

int dbDelete(redisDb *db, robj *key) {
    return server.lazyfree_lazy_server_del ? dbAsyncDelete(db,key) :
                                             dbSyncDelete(db,key);
}

删除操作和redis的lazyfree_lazy_server_del 参数设置有关。

如果是懒释放,那么采用异步删除操作,否则采用同步删除操作。在expire的相关操作中也用到了这俩方法。


异步删除

/* 从db中删除存在的K-V
 * 异步删除,会将value对象放进一个惰性列表中,由另一个线程异步释放空间
 */
#define LAZYFREE_THRESHOLD 64 /* 惰性释放临界值 */
int dbAsyncDelete(redisDb *db, robj *key) {
    /* 从expire字典数组中删除key
     * 但是暂时不会释放key的空间,因为这个key可能有其他地方会引用 
     */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        /* 获取值对象 */
        robj *val = dictGetVal(de);
        /* 值对象的大小 */
        size_t free_effort = lazyfreeGetFreeEffort(val);

        /* 如果值对象的大小超过了临界值,那么释放这个值对象就太费事了
         * 例如值对象是个list对象,释放时需要循环list去释放,会很消耗性能;
         * 并且值对象的refCount引用计数器的值必须==1,也就是没有其他程序引用它了
         * 这时会另起一个线程去做异步处理
         */
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }
    }

    /* 释放K-V空间,如果value对象已经做了异步删除,那么这里只要释放key对象就行 */
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        /* 开启集群时,需要将槽中的key删除 */
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

我们关注dictUnlink这个方法:(这里我也放上调用的dictGenericDelete实现)

/* 这个方法并不会去释放key对象和value对象,只是从ht中移除这个key,
 * 就像方法名一样,unlinked,除去关联关系。
 * 要真的释放空间,需要调用dictFreeUnlinkedEntry()这个方法
 */
dictEntry *dictUnlink(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);
}

/* 调用的dictGenericDelete方法如下 */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    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)) {
                /* 这里,移除这个entry */
                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; 
}

真正释放空间的方法是dictFreeUnlinkedEntry方法,来看看它的实现:很简单,就是释放key对象、value对象、entry对象的空间

void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    dictFreeKey(d, he);
    dictFreeVal(d, he);
    zfree(he);
}

同步删除

int dbSyncDelete(redisDb *db, robj *key) {
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

调用dictDelete方法,其实最后的实现还是上面说到的dictGenericDelete方法

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}

至此,删除操作完成了。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值