在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" 命令,这时会存在两种情况
- msg 这个键不存在,那么新增msg这个键,并且赋值为“hello world”
- 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;
}
至此,删除操作完成了。