1.多个数据库
一个Redis实例可以由多个数据库,默认为16个,Redis客户端默认连接0号数据库,通过select
命令可以切换数据库。
redisServer
结构体中,保存redisDb
数组,初始化服务时,会根据dbnum
属性来创建多少个数据库。
struct redisServer{
......
//保存服务器的所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
//一个链表,保存了所有客户端状态结构
list *clients;
}
redis服务器会为每个客户端创建一个redisClient
结构体(保存在redisServer.client链表中),来保存相关数据,其中有一个属性为db,记录当前选择的数据库。
select命令的原理就是:通过修改redisClinet.db
指针,让它指向不同的数据库,实现select切换功能。
typedef struct redisClient{
//....
//记录客户端当前正在使用的数据库
redisDb *db;
//....
}
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;
}
2.数据库键空间
redis是kv数据库,其中保存着众多的k-v键值对,每个数据库的所有键值对都保存在redisDb.dict
中,这个dict也被称为数据库的键空间。
typedef struct redisDb{
//...
//键空间,保存所有的键值对
dict *dict;
}redisDb
其中,键一般是个字符串对象,其值可以是常见的redis对象,比如string,list,set,szset,hash。
图片地址:http://img.blog.csdn.net/20160505160854320
通过对dict的操作,即对字典的操作,就能实现对整个数据库键值对的操作,比如增加、删除、查询键值对。对字典的操作,可以查阅dict.c
上的源码。
举个栗子:从数据库db中获取指定键lookupKey
,
/*
* 从数据库 db 中取出键 key 的值(对象)
* 如果 key 的值存在,那么返回该值;否则,返回 NULL 。
*/
robj *lookupKey(redisDb *db, robj *key) {
// 查找键空间,dictFind是字典操作的API
dictEntry *de = dictFind(db->dict,key->ptr);
// 节点存在
if (de) {
// 取出值
robj *val = dictGetVal(de);
// 更新LRU时间信息(只在不存在子进程时执行,防止破坏 copy-on-write 机制)
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
val->lru = LRU_CLOCK();
// 返回值
return val;
} else {
// 节点不存在
return NULL;
}
}
此外,还有一系列键值操作的API:
- setKey 设置键值对
- dbExist 判断是否存在key
- dbAdd 增加
- dbDelete 删除
- lookupKey 查询
- …
键空间的其他维护操作
redis命令对数据库进行读写时,会执行一些额外操作。比如:
- 更新服务器的hit和miss属性
- 更新键的LRU时间
- 读取键时,假若是过期的,则需要删除
- 如果对该键有监视,则需要标记为脏,即标记
client.flags |= REDIS_DIRTY_CAS
- 如果开启了通知功能,需要发送通知
3.键的生存时间
通过xpire,pexpire,expireat,pexpireat
命令,可以设置键的生存时间。
注:上述四个命令,最后都转换成pexpireat
命令来实现的。
redisDb结构的expires
属性保存了数据库中所有键的过期时间,expires是一个字典,键是一个指针,指向键空间的某个对象,值是一个毫秒级的时间戳,即过期时间。
过期键的删除
Redis才用了两个过期键删除策略:
- 惰性删除,服务器在读取键时,进行过期检查,过期的话就删除之。
- 定期删除,每隔一段时间,就对数据库进行一个检查,删除里面的过期键。
由db.c/expireIfNeeded
函数实现,读写数据库之前都会调用该函数对键进行检查。
/*
* 检查 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 */
// 如果服务器正在进行载入,那么不进行任何过期检查
if (server.loading) return 0;
// 当服务器运行在 replication 模式时
// 附属节点并不主动删除 key
// 它只返回一个逻辑上正确的返回值
// 真正的删除操作要等待主节点发来删除命令时才执行
// 从而保证数据的同步
if (server.masterhost != NULL) return now > when;
// 运行到这里,表示键带有过期时间,并且服务器为主节点
// 如果未过期,返回 0
if (now <= when) return 0;
server.stat_expiredkeys++;
// 向 AOF 文件和附属节点传播过期信息
propagateExpire(db,key);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id);
// 将过期键从数据库中删除
return dbDelete(db,key);
}
数据库的定期删除由redis.c/activeExpireCycle
函数实现,当redis的周期性操作redis.c/serverCron
执行时,就会调用activeExpireCycle函数,在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires
字典中随机检查一部分键的过期时间,并删除过期键。
源码:见redis.c/activeExpireCycle
4.AOF,RDB,主动对于过期键的处理
RDB
- 生成RDB时,过期键不会被保存到RDB中
- 载入RDB时,主服务器不会载入过期键,从服务器会
AOF
当过期键,被惰性删除或者定期删除后,程序会向AOF追加一个DEL命令,来显式地记录该键被删除。
主从
从服务器的过期键删除动作由主服务器控制:
- 主服务器删除一个过期键,会向从服务器传播一个
DEL
命令 - 从服务器读取到一个过期键,不会进行删除,正常处理,只有在收到主服务器传播过来的
DEL
命令才会删除。
5.Rerference
- 黄健宏. Redis设计与实现[M]. 机械工业出版社, 2014.