环境说明: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就都会执行;