redis 内部也像传统的关系型数据库一样,有 database 的概念。主要用于实现数据隔离,这个在生产环境中是非常必要的。多个进程共同使用一个 redis 服务,如果没有数据隔离的机制,对于开发、运维都会有很大的挑战。
今天来看一下 redis 中 database 的实现
database
redisDb
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 初始化、以及构造 client 相关的代码,我们知道每个 client 都会有一个指定的 db,该 client 后续所有的操作,都是发生在其指定的 database 内的。
查找
要在一个 database 中查找 key 只需要在其 dict 内查找即可
robj *lookupKey(redisDb *db, robj *key) {
// 查找键空间
dictEntry *de = dictFind(db->dict,key->ptr);
// 节点存在
if (de) {
// 取出值
robj *val = dictGetVal(de);
// 如果有子进程,不更新 lru 时间戳,避免 COW
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
val->lru = LRU_CLOCK();
// 返回值
return val;
} else {
// 节点不存在
return NULL;
}
}
添加
void dbAdd(redisDb *db, robj *key, robj *val) {
// 复制键名
sds copy = sdsdup(key->ptr);
// 尝试添加键值对
int retval = dictAdd(db->dict, copy, val);
// 键不能已经存在于 dict
redisAssertWithInfo(NULL,key,retval == REDIS_OK);
// 如果开启了集群模式,那么将键保存到槽里面
if (server.cluster_enabled) slotToKeyAdd(key);
}
void dbOverwrite(redisDb *db, robj *key, robj *val) {
dictEntry *de = dictFind(db->dict,key->ptr);
// 节点必须存在,否则中止
redisAssertWithInfo(NULL,key,de != NULL);
// 覆写旧值
dictReplace(db->dict, key->ptr, val);
}
// high level 的 set 操作
void setKey(redisDb *db, robj *key, robj *val) {
// 添加或覆写数据库中的键值对
if (lookupKeyWrite(db,key) == NULL) {
dbAdd(db,key,val);
} else {
dbOverwrite(db,key,val);
}
// 增加对 val 的引用计数
incrRefCount(val);
// 移除键的过期时间
removeExpire(db,key);
// 发送键修改通知
signalModifiedKey(db,key);
}
void signalModifiedKey(redisDb *db, robj *key) {
touchWatchedKey(db,key);
}
注意我们一定要使用 sdsdup 对 key 进行 deep copy。
删除
int dbDelete(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;
}
}
清空数据库
long long emptyDb(void(callback)(void*)) {
int j;
long long removed = 0;
// 清空所有数据库
for (j = 0; j < server.dbnum; j++) {
// 记录被删除键的数量
removed += dictSize(server.db[j].dict);
// 删除所有键值对
dictEmpty(server.db[j].dict,callback);
// 删除所有键的过期时间
dictEmpty(server.db[j].expires,callback);
}
// 如果开启了集群模式,那么还要移除槽记录
if (server.cluster_enabled) slotToKeyFlush();
// 返回键的数量
return removed;
}
键过期
redis 支持客户端指定缓存的过期时间,这一点对于一个缓存服务分外重要。因为 redis 往往作为一个缓存服务使用,但是在分布式架构中,很容易出现缓存与数据库数据出现不一致的情况,能让缓存自动过期,给了我们一种简单的修正方法。
来看一下 expire 相关命令如何实现:
// expire command 族的底层实现函数
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. */
// param 是 when 参数,获取其底层数值
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
return;
// 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
if (unit == UNIT_SECONDS) when *= 1000;
// redis 提供使用相对时间和绝对时间两种方式设定过期时间
// param 使用绝对时间时,basetime == 0
// param 使用相对时间时,basetime == 当前时间
when += basetime;
// 如果 database 中 key 不存在则直接返回
if (lookupKeyRead(c->db,key) == NULL) {
addReply(c,shared.czero);
return;
}
if (when <= mstime() && !server.loading && !server.masterhost) {
// 可能在接受到 expire 命令时候,key 的 ttl(time to live) 已经为负
// 代表 key 已经过期,在本节点没有在载入数据而去不是从节点时,需要立即删除 key
robj *aux;
redisAssertWithInfo(c,key,dbDelete(c->db,key));
server.dirty++;
// 构造 DEL command,传播 DEL 命令到从节点
aux = createStringObject("DEL",3);
rewriteClientCommandVector(c,2,aux,key);
decrRefCount(aux);
signalModifiedKey(c->db,key);
// notify 与 pub/sub 相关,先略过
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c, shared.cone);
return;
} else {
// 设置键的过期时间,注意这个 key 还是可能是过期的,但是这种情况下
// 如果我们是从节点,等待 master 发送 DEL 过来即可,不要自己去删除
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 过期(相对时间)
void expireCommand(redisClient *c) {
expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
// 在指定的时间戳达到以后,key 过期(绝对时间)
void expireatCommand(redisClient *c) {
expireGenericCommand(c,0,UNIT_SECONDS);
}
在 database 查询某个键的时候,redis 会检查 key 是否过期:
void propagateExpire(redisDb *db, robj *key) {
robj *argv[2];
// 构造一个 DEL key 命令
argv[0] = shared.del;
argv[1] = key;
incrRefCount(argv[0]);
incrRefCount(argv[1]);
// 传播到 AOF
if (server.aof_state != REDIS_AOF_OFF)
feedAppendOnlyFile(server.delCommand,db->id,argv,2);
// 传播到所有附属节点
replicationFeedSlaves(server.slaves,db->id,argv,2);
decrRefCount(argv[0]);
decrRefCount(argv[1]);
}
long long getExpire(redisDb *db, robj *key) {
dictEntry *de;
// 未设置过期时间,返回 -1
if (dictSize(db->expires) == 0 ||
(de = dictFind(db->expires,key->ptr)) == NULL) return -1;
// 设置了过期时间的 key 肯定是存在于 database 键空间的
redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
// 返回过期时间
return dictGetSignedIntegerVal(de);
}
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 */
// 如果服务器正在进行载入,那么不进行任何过期检查
if (server.loading) return 0;
// 如果是在执行一个 lua 脚本,那么我们只在脚本开始的时候,记录一次时间
// 后续脚本中所有 key 的过期都按照脚本开始时间来计算
now = server.lua_caller ? server.lua_time_start : mstime();
// 如果我们只是一个从节点,我们不会主动的去 expire 删除 key,而是等待
// master 发送 DEL 命令过来,所以立即返回
if (server.masterhost != NULL) return now > when;
// 如果未过期,返回 0
if (now <= when) return 0;
// 删除这个 key
server.stat_expiredkeys++;
// 向 AOF 文件和附属节点传播过期信息
propagateExpire(db,key);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
// 将过期键从数据库中删除
return dbDelete(db,key);
}
总结
- database 提供数据隔离功能,每一个 client 的操作都是与某一个 database 绑定的
- 当执行一个 lua 脚本时,key 是否过期都是按照 lua 脚本开始执行时时间计算的,而不会持续更新当前时间