0. 本文要点
主要讲和Redis 数据库的构造和实现。
1)数据库如何存储对象?redisDb数据库结构体
2)数据库中键的操作
3)键的过期时间
4)过期键的处理
5)过期键与AOF和RDB
1. 数据库结构体
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)*/ //键为阻塞客户端的键,value为因键阻塞的所有客户端(链表串起来)
dict *ready_keys; /* Blocked keys that received a PUSH */ //被阻塞的键
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ //被WATCH命令监控的键,用于事务
int id; /* Database ID */ //数据库的id,每个server有多个数据库,数据库之间通过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;
下文将详细讨论id
、dict
和expires
三个属性。
先讲id。
redis
在启动服务器的时候,会创建CONFIG_DEFAULT_DBNUM
个数据库,CONFIG_DEFAULT_DBNUM
的值在server.h
中定义,为16
,相当于一个包含16
个redisDb
的数组,id
就是某个redisDb
在数组中的下标。
创建数据库的代码如下(server.c):
//initServerConfig函数
server.dbnum = CONFIG_DEFAULT_DBNUM;
//initServer函数
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
不同的客户端可能连接到不同的数据库上,内部程序如AOF根据id就可以知道使用的是哪个数据库,而且切换数据库方便。
2. 数据库中的键
redisDb
使用一个dict
来保存键
和对应的value
,其中:
键是字符串对象;
值是五种对象(字符串,列表,哈希表,集合,有序集合)之一。
图片截取自《redis设计与实现》
键的操作基本上就是哈希表的操作。
2.1 添加键值对
void dbAdd(redisDb *db, robj *key, robj *val) { //往数据库中加入键值对
sds copy = sdsdup(key->ptr);
int retval = dictAdd(db->dict, copy, val);//将key value加入字典
serverAssertWithInfo(NULL,key,retval == DICT_OK);
if (val->type == OBJ_LIST ||
val->type == OBJ_ZSET)
signalKeyAsReady(db, key);// 如果值对象是列表或有序集合类型,有阻塞的命令,因此将key加入ready_keys字典中
if (server.cluster_enabled) slotToKeyAdd(key);
}
2.2 复写
void dbOverwrite(redisDb *db, robj *key, robj *val) { //复写key对应的值
dictEntry *de = dictFind(db->dict,key->ptr);
serverAssertWithInfo(NULL,key,de != NULL);
dictEntry auxentry = *de;
robj *old = dictGetVal(de);
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
val->lru = old->lru;
}
dictSetVal(db->dict, de, val);//设置新值
if (server.lazyfree_lazy_server_del) {
freeObjAsync(old);
dictSetVal(db->dict, &auxentry, NULL);
}
dictFreeVal(db->dict, &auxentry);
}
2.3 查找
robj *lookupKey(redisDb *db, robj *key, int flags) { //查找key,一般被lookupKeyWrite lookupKeyReadWithFlags lookupKeyRead调用
dictEntry *de = dictFind(db->dict,key->ptr);//在字典中查找key
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 &&
!(flags & LOOKUP_NOTOUCH))
{ //更新键的时间
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
return val;//找到返回相应的值
} else {
return NULL;//找不到返回空
}
}
/* Lookup a key for read operations, or return NULL if the key is not found
* in the specified DB.
*
* As a side effect of calling this function:
* 1. A key gets expired if it reached it's TTL. //如果键到达TTL则键设置为过期
* 2. The key last access time is updated. //更新键的最近一次获取时间
* 3. The global keys hits/misses stats are updated (reported in INFO).//全局键的命中/未命中状态会更新
*
* This API should not be used when we write to the key after obtaining
* the object linked to the key, but only for read only operations.
*
* Flags change the behavior of this command:
*
* LOOKUP_NONE (or zero): no special flags are passed.
* LOOKUP_NOTOUCH: don't alter the last access time of the key.
*
* Note: this function also returns NULL if the key is logically expired //如果一个键存在但是过期了,函数会返回空
* but still existing, in case this is a slave, since this API is called only
* for read operations. Even if the key expiry is master-driven, we can
* correctly report a key is expired on slaves even if the master is lagging
* expiring our key via DELs in the replication link. */
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val;
if (expireIfNeeded(db,key) == 1) {
/* Key expired. If we are in the context of a master, expireIfNeeded()
* returns 0 only when the key does not exist at all, so it's safe
* to return NULL ASAP. */
if (server.masterhost == NULL) {
server.stat_keyspace_misses++;
return NULL;
}
/* However if we are in the context of a slave, expireIfNeeded() will
* not really try to expire the key, it only returns information
* about the "logical" status of the key: key expiring is up to the
* master in order to have a consistent view of master's data set.
* //从节点中调用expireIfNeeded不会将键设置为过期,以保持和主节点的一致性,设置键的过期由主节点完成
* However, if the command caller is not the master, and as additional
* safety measure, the command invoked is a read-only command, we can
* safely return NULL here, and provide a more consistent behavior
* to clients accessign expired values in a read-only fashion, that
* will say the key as non existing.
*
* Notably this covers GETs when slaves are used to scale reads. */
if (server.current_client &&
server.current_client != server.master &&
server.current_client->cmd &&
server.current_client->cmd->flags & CMD_READONLY)
{
server.stat_keyspace_misses++;
return NULL;
}
}
val = lookupKey(db,key,flags);
if (val == NULL)
server.stat_keyspace_misses++;
else
server.stat_keyspace_hits++;
return val;
}
3. 键的过期时间
3.1 设置键的过期时间
可以通过EXPIRE
、PEXPIRE
、EXPIREAT
和PEXPIREAT
四个命令设置键的过期时间.
命令 | 含义 |
---|---|
EXPIRE | 以秒为单位设置键的生存时间 |
PEXPIRE | 以毫秒为单位设置键的生存时间 |
EXPIREAT | 以秒为单位,设置键的过期UNIX 时间戳 |
PEXPIREAT | 以毫秒为单位,设置键的过期UNIX 时间戳 |
虽然设置键的过期时间(生存时间)的命令有四个,但是最终存储过期信息的时候,都是存储的"以毫秒为单位的过期UNIX 时间戳".键的过期时间存储在字典expires中.
3.2 expires
这是一个字典,字典的key是指向键的指针,字典的值是对应键的过期时间,以long long 类型表示。
expires 字典的值只保存“以毫秒为单位的过期UNIX 时间戳” ,通过进行转换,所有命令的效果最后都和PEXPIREAT
命令的效果一样。
3.3 过期键的判断方法
判断方法比较简单:
- 检查键是否存在于expires 字典:如果存在,那么取出键的过期时间;
- 检查当前UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则,
键未过期
4. 过期键的删除
过期键的删除有三种方法:
- 定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达时,由事件处理 器自动执行键的删除操作。
- 惰性删除:放任键过期不管,但是在每次从dict 字典中取出键值时,要检查键是否过 期,如果过期的话,就删除它,并返回空;如果没过期,就返回键值。
- 定期删除:每隔一段时间,对expires 字典进行检查,删除里面的过期键。
定时删除对内存友好,能够保证过期的键被及时删除,不占用过多内存,但是对CPU不友好,需要为每一个键添加一个定时事件.
惰性删除对CPU友好,但是比较耗内存.
定期删除是上面两者的折中.
Redis 使用的过期键删除策略是惰性删除加上定期删除,这两个策略相互配合,可以很好地在合理利用CPU 时间和节约内存空间之间取得平衡。