本文对Redis的数据库文件进行简要介绍,包括数据库的选择,键的新建更新删除、Redis过期策略以及事件通知等。
Redis在redis.h中定义了数据库结构redisDb ,如下所示:
/* 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 */
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
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 */
// 正在被 WATCH 命令监视的键
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
// 数据库号码
int id; /* Database ID */
// 数据库的键的平均 TTL ,统计信息
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
Redis服务端定义的结构体如下所示:
struct redisServer {
// 数据库
redisDb *db;
int dbnum; /* Total number of configured DBs */
};
其中dbnum默认为16,redis服务端在初始化时会默认创建16个数据库,创建出来的数据库用db成员保存。各个数据库之间的数据完全独立,客户端默认的目标数据库为0号数据库,可以通过以下命令完成数据库切换:
SELECT 2 //切换目标数据库为2号数据库
Redis是key-value型的内存数据库,其中每一个键值对都存在redisDb的dict词典中,其中dict被称为键空间。存放了列表键、哈希键和字符串键的键空间如下所示:
所以,每次键空间新建键、获取键的值、更新键内容、删除键的操作均是调用词典的API实现。
Expire命令可以给一个key设置过期时间,ttl命令可以获取key的过期时间,过期时间到达后,redis会对过期的key进行删除。persist命令移除一个键的过期时间。
set wyl 520
expire wyl 10
ttl
10
persist wyl
ttl
-1
数据库中所有键的过期时间均保存在redisDb 的词典expires中,示意图如下:
Redis过期删除策略包括惰性删除策略和定期删除策略两种。 a.惰性删除策略:每一次对数据库的键进行读写时都先检查key是否过期,过期则删除key并不操作。 b.定期删除策略:周期性的从数据库中选取一部分key检查是否过期,将过期的key删除。
Redis客户端可以订阅给定的频道或者模式,来从数据库获取通知。数据库通知分为两种,键空间通知和键事件通知。 键空间通知会告诉用户关注的键执行了什么命令,如下所示:
键事件通知会告诉用户关注的命令被那些键执行了,如下所示:
键空间和键事件通知函数定义在notify.c中,定义如下:其中type为事件类型,event为事件名,key为键名称dbid是数据库所在ID。
/* The API provided to the rest of the Redis core is a simple function:
*
* notifyKeyspaceEvent(char *event, robj *key, int dbid);
*
* 'event' is a C string representing the event name.
*
* event 参数是一个字符串表示的事件名
*
* 'key' is a Redis object representing the key name.
*
* key 参数是一个 Redis 对象表示的键名
*
* 'dbid' is the database ID where the key lives.
*
* dbid 参数为键所在的数据库
*/
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
sds chan;
robj *chanobj, *eventobj;
int len = -1;
char buf[24];
/* If notifications for this class of events are off, return ASAP. */
// 如果服务器配置为不发送 type 类型的通知,那么直接返回
if (!(server.notify_keyspace_events & type)) return;
// 事件的名字
eventobj = createStringObject(event,strlen(event));
/* __keyspace@<db>__:<key> <event> notifications. */
// 发送键空间通知
if (server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {
// 构建频道对象
chan = sdsnewlen("__keyspace@",11);
len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, key->ptr);
chanobj = createObject(REDIS_STRING, chan);
// 通过 publish 命令发送通知
pubsubPublishMessage(chanobj, eventobj);
// 释放频道对象
decrRefCount(chanobj);
}
/* __keyevente@<db>__:<event> <key> notifications. */
// 发送键事件通知
if (server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT) {
// 构建频道对象
chan = sdsnewlen("__keyevent@",11);
// 如果在前面发送键空间通知的时候计算了 len ,那么它就不会是 -1
// 这可以避免计算两次 buf 的长度
if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, eventobj->ptr);
chanobj = createObject(REDIS_STRING, chan);
// 通过 publish 命令发送通知
pubsubPublishMessage(chanobj, key);
// 释放频道对象
decrRefCount(chanobj);
}
// 释放事件对象
decrRefCount(eventobj);
}
过期检查函数定义在db.c中,在惰性过期策略中,每次执行键操作时都会调用它。每次定义如下:
/* 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)
*
* 返回给定 key 的过期时间。
*
* 如果键没有设置过期时间,那么返回 -1 。
*/
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). */
redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
// 返回过期时间
return dictGetSignedIntegerVal(de);
}
/*
* 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
*
* 返回 0 表示键没有过期时间,或者键未过期。
*
* 返回 1 表示键已经因为过期而被删除了。
*/
int expireIfNeeded(redisDb *db, robj *key) {
// 取出键的过期时间
mstime_t when = getExpire(db,key);
mstime_t now;
// 没有过期时间
if (when < 0) return 0; /* No expire for this key */
/* Don't expire anything while loading. It will be done later. */
// 如果服务器正在进行载入,那么不进行任何过期检查
if (server.loading) return 0;
/* If we are in the context of a Lua script, we claim that time is
* blocked to when the Lua script started. This way a key can expire
* only the first time it is accessed and not in the middle of the
* script execution, making propagation to slaves / AOF consistent.
* See issue #1525 on Github for more information. */
now = server.lua_caller ? server.lua_time_start : mstime();
/* If we are running in the context of a slave, 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. */
// 当服务器运行在 replication 模式时
// 附属节点并不主动删除 key
// 它只返回一个逻辑上正确的返回值
// 真正的删除操作要等待主节点发来删除命令时才执行
// 从而保证数据的同步
if (server.masterhost != NULL) return now > when;
// 运行到这里,表示键带有过期时间,并且服务器为主节点
/* Return when this key has not expired */
// 如果未过期,返回 0
if (now <= when) return 0;
/* Delete the key */
server.stat_expiredkeys++;
// 向 AOF 文件和附属节点传播过期信息
propagateExpire(db,key);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
// 将过期键从数据库中删除
return dbDelete(db,key);
}
通过select命令切换目标数据库函数实现如下:
/*
* 将客户端的目标数据库切换为 id 所指定的数据库
*/
int selectDb(redisClient *c, int id) {
// 确保 id 在正确范围内
if (id < 0 || id >= server.dbnum)
return REDIS_ERR;
// 切换数据库(更新指针)
c->db = &server.db[id];
return REDIS_OK;
}
void selectCommand(redisClient *c) {
long id;
// 不合法的数据库号码
if (getLongFromObjectOrReply(c, c->argv[1], &id,
"invalid DB index") != REDIS_OK)
return;
if (server.cluster_enabled && id != 0) {
addReplyError(c,"SELECT is not allowed in cluster mode");
return;
}
// 切换数据库
if (selectDb(c,id) == REDIS_ERR) {
addReplyError(c,"invalid DB index");
} else {
addReply(c,shared.ok);
}
}
设置过期时间函数实现如下:
void expireCommand(redisClient *c) {
expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
/* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT
* and PEXPIREAT. Because the commad second argument may be relative or absolute
* the "basetime" argument is used to signal what the base time is (either 0
* for *AT variants of the command, or the current time for relative expires).
*
* 这个函数是 EXPIRE 、 PEXPIRE 、 EXPIREAT 和 PEXPIREAT 命令的底层实现函数。
*
* 命令的第二个参数可能是绝对值,也可能是相对值。
* 当执行 *AT 命令时, basetime 为 0 ,在其他情况下,它保存的就是当前的绝对时间。
*
* unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
* the argv[2] parameter. The basetime is always specified in milliseconds.
*
* unit 用于指定 argv[2] (传入过期时间)的格式,
* 它可以是 UNIT_SECONDS 或 UNIT_MILLISECONDS ,
* basetime 参数则总是毫秒格式的。
*/
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
robj *key = c->argv[1], *param = c->argv[2];
long long when; /* unix time in milliseconds when the key will expire. */
// 取出 when 参数
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
return;
// 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
if (unit == UNIT_SECONDS) when *= 1000;
when += basetime;
/* No key, return zero. */
// 取出键
if (lookupKeyRead(c->db,key) == NULL) {
addReply(c,shared.czero);
return;
}
/* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
* should never be executed as a DEL when load the AOF or in the context
* of a slave instance.
*
* 在载入数据时,或者服务器为附属节点时,
* 即使 EXPIRE 的 TTL 为负数,或者 EXPIREAT 提供的时间戳已经过期,
* 服务器也不会主动删除这个键,而是等待主节点发来显式的 DEL 命令。
*
* Instead we take the other branch of the IF statement setting an expire
* (possibly in the past) and wait for an explicit DEL from the master.
*
* 程序会继续将(一个可能已经过期的 TTL)设置为键的过期时间,
* 并且等待主节点发来 DEL 命令。
*/
if (when <= mstime() && !server.loading && !server.masterhost) {
// when 提供的时间已经过期,服务器为主节点,并且没在载入数据
robj *aux;
redisAssertWithInfo(c,key,dbDelete(c->db,key));
server.dirty++;
/* Replicate/AOF this as an explicit DEL. */
// 传播 DEL 命令
aux = createStringObject("DEL",3);
rewriteClientCommandVector(c,2,aux,key);
decrRefCount(aux);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c, shared.cone);
return;
} else {
// 设置键的过期时间
// 如果服务器为附属节点,或者服务器正在载入,
// 那么这个 when 有可能已经过期的
setExpire(c->db,key,when);
addReply(c,shared.cone);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
server.dirty++;
return;
}
}
查找key对应值函数实现如下:
/*
* 从数据库 db 中取出键 key 的值(对象)
*
* 如果 key 的值存在,那么返回该值;否则,返回 NULL 。
*/
robj *lookupKey(redisDb *db, robj *key) {
// 查找键空间
dictEntry *de = dictFind(db->dict,key->ptr);
// 节点存在
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. */
// 更新时间信息(只在不存在子进程时执行,防止破坏 copy-on-write 机制)
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
val->lru = LRU_CLOCK();
// 返回值
return val;
} else {
// 节点不存在
return NULL;
}
}
本文对Redis中数据库的实现做了简要介绍,如有错误,多多指教。