切换数据库
默认情况下,Redis客户端的目标数据库为0号数据库
通过修改client.db指针,指向不同数据库,从而实现切换目标数据库的功能
客户端切换目标数据库命令: SELECT 数据库序号
在处理多数据库程序时,为了避免误操作(特别像FLUSHDB这样的危险命令),
最好先执行一个SELECT命令,显式切换到指定的数据库,然后再执行别的命令
数据库键空间
- 键空间是一个字典,所有针对数据库的操作实际上都是通过对键空间字典进行操作来实现的,
- 添加新键:将一个新键值对添加到键空间字典里面
- 删除键:在键空间里面删除键所对应的键值对对象
- 更新键:对键空间里面键所对应的值对象进行更新
- 对键取值:在键空间中取出键所对应的值对象
- 读写键空间的维护操作
-
在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中次数(keyspace_hits)和键空间不命中次数(keyspace_misses)
INFO stats 命令:查看keyspace_hits、keyspace_misses属性 -
在读取一个键之后,服务器会更新键的LRU时间
OBJECT idletime 命令:查看键的空转时长(闲置时间) -
如果服务器在读取一个键时发现键已经
过期
,那么服务器会先删除
这个过期键,然后才执行余下的其他操作 -
如果有客户端使用watch命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键
标记为脏
,从而让事务程序注意到这个键已经被修改过 -
服务器每次修改一个键之后,都会对
脏键计数器
的值增1,这个计数器会触发服务器的持久化以及复制操作 -
如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知
保存过期时间
- 设置过期时间
生存时间: 键可以存在多久
expire命令: 将键key的生存时间设置为ttl秒
pexpire命令: 将键key的生存时间设置为ttl毫秒
过期时间: 键什么时候会被删除
expireat命令: 将键key的过期时间设置为timestamp所指定的秒数时间戳
pexpireat命令: 将键key的过期时间设置为timestamp所指定的毫秒数时间戳
实际上expire、pexpire、expireat三个命令底层都是用pexpireat来实现的
- 移除过期时间
persist命令: 移除一个键的过期时间,
pexpireat命令的反操作,解除过期字典中给定键和对应值的关联
- 计算并返回剩余生存时间
ttl命令: 以秒为单位返回键的剩余生存时间
pttl命令: 以毫秒为单位返回键的剩余生存时间
ttl、pttl命令若返回-1,表示键没有设置过期时间
ttl、pttl命令若返回-2,表示键不存在于数据库
- 过期键的判定
- 检查给定的键
是否存在
于过期字典: 如果存在,那么取得键的过期时间- 检查当前UNIX时间戳
是否大于
键的过期时间: 如果是的话,那么键已经过期; 否则的话,键未过期
过期键删除策略
如果一个键过期了,有三种删除策略:定时删除、惰性删除、定期删除,
- 定时删除: 对内存最友好,对CPU时间最不友好,占用太多的CPU时间,影响服务器的响应时间和吞吐量
- 惰性删除: 对内存最不友好,对CPU时间最友好,浪费太多内存,有内存泄漏的危险
- 定期删除:定时删除和惰性删除的一种整合和折中,通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响,通过定期删除过期键有效地减少因为过期键而带来的内存浪费
- Redis服务器实际使用的是惰性删除和定期删除两种策略
通过配合使用,服务器可以很好地在合理使用CPU时间和避免浪费空间之间取得平衡
RDB持久化功能对过期键的处理
数据库中包含过期键不会
对 生成新的RDB文件
或者 载入RDB文件的服务器
造成影响
-
生成RDB文件
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会
被保存到新创建的RDB文件中 -
载入RDB文件
-
如果服务器以
主服器
模式运行,对文件中保存的键进行检查,忽略过期键
,然后将未过期的键载入数据库中 -
如果服务器以
从服务器
模式运行,对文件中保存的键进行检查,全部键
都载入数据库中,主从数据同步的时候才会删除过期键
-
AOF持久化功能对过期键的处理
数据库中包含过期键不会
对 AOF文件
或者 AOF重写
造成影响
-
AOF文件写入
当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地记录该键已被删除 -
AOF重写
在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会
被保存到重写后的AOF文件中
复制功能对过期键的处理
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制
,保证主从服务器数据的一致性
-
主服务器在删除一个过期键之后,会
显式
地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键 -
从服务器
在执行客户端发送的读命令时,即使碰到过期键也不会主动将过期键删除
,而是继续像处理为过期的键一样来处理过期键 -
从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键
数据库通知
服务器配置server.notify_keyspace_events 决定了服务器所发送通知的类型
- 键空间通知:某个键执行了什么命令
- 键事件通知:某个命令被什么键执行了
定义
// 自动间隔性保存配置:记录了保存条件的秒数和修改数
struct saveparam {
// 秒数
time_t seconds;
// 修改数
int changes;
};
// 数据库状态结构
typedef struct redisDb {
// ...
// 数据库键空间,保存着数据库中的所有键值对,与用户所见的数据库是直接对应的
dict *dict; /* The keyspace for this DB */
// 数据库序号,序号范围:[0, 配置文件中的databases选项)
int id;
// 过期字典,保存着数据库中所有键的过期时间
// 过期字典的键是一个指针,指向键空间中的某个键对象(也就是某个数据库键)
// 过期字典的值是一个long long类型的整数,保存键所指向的数据库键的过期时间(一个毫秒精度的UNIX时间戳)
dict *expires;
// ...
} redisDb;
// 服务器状态结构
struct redisServer {
// ...
// 数据库数组: 保存着服务器中的所有数据库
redisDb *db;
// 数据库数量,可通过配置文件中的databases选项进行调整,默认为16
int dbnum;
// 服务器所发送通知的类型
int notify_keyspace_events;
// 自动间隔性保存配置:记录了保存条件配置的数组
struct saveparam *saveparams;
// 修改计数器:记录距离上一次成功执行save命令 或者 bgsave命令之后,
// 服务器对数据库状态(服务器中所有的数据库)进行了多少次修改(包括写入、删除、更新等操作)
long long dirty;
// 上一次执行保存的时间: 是一个uninx时间戳,记录服务器上一次成功执行save命令 或者 bgsave命令的时间
time_t lastsave;
// AOF缓冲区:保存对数据库状态修改的写命令
sds aof_buf;
// AOF重写缓冲区:保存服务器创建子进程之后对数据库状态修改的写命令
list *aof_rewrite_buf_blocks;
// ...
};
// 客户端状态结构
typedef struct client {
// ...
// 当前客户端目标数据库指针:记录客户端当前正在使用的数据库
redisDb *db;
// ...
} client;
/**
* 数据库通知发送
* type 当前想要发送的通知的类型
* event 事件的名称
* key 产生事件的键
* dbid 产生事件的数据库号码
*/
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
sds chan;
robj *chanobj, *eventobj;
int len = -1;
char buf[24];
/* If any modules are interested in events, notify the module system now.
* This bypasses the notifications configuration, but the module engine
* will only call event subscribers if the event type matches the types
* they are interested in. */
// 如果任何模块对事件调用,立即通知模块系统。这将绕过通知配置,
// 但模块引擎将仅在事件类型与其允许的类型匹配时调用事件订阅者。
moduleNotifyKeyspaceEvent(type, event, key, dbid);
/* If notifications for this class of events are off, return ASAP. */
// 如果给定的通知不是服务器允许发送的通知,那么直接返回,不做任何动作
if (!(server.notify_keyspace_events & type)) return;
// 事件字符串对象
eventobj = createStringObject(event,strlen(event));
/* __keyspace@<db>__:<key> <event> notifications. */
// 发送键空间通知
if (server.notify_keyspace_events & NOTIFY_KEYSPACE) {
// 将通知发送给频道__keyspace@<dbid>__:<key>
// 内容为键所发生的事件<event>
// 构建频道名字
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(OBJ_STRING, chan);
// 发送通知
pubsubPublishMessage(chanobj, eventobj);
// 释放频道字符串对象
decrRefCount(chanobj);
}
/* __keyevent@<db>__:<event> <key> notifications. */
// 发送键事件通知
if (server.notify_keyspace_events & NOTIFY_KEYEVENT) {
// 将通知发送给频道__keyspace@<dbid>__:<event>
// 内容为键所发生的事件<key>
// 构建频道名字
chan = sdsnewlen("__keyevent@",11);
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(OBJ_STRING, chan);
// 发送通知
pubsubPublishMessage(chanobj, key);
// 释放频道字符串对象
decrRefCount(chanobj);
}
// 释放事件字符串对象
decrRefCount(eventobj);
}
源码阅读
- 服务器:src/server.h 、 src/server.c
- 键的过期时间:src/expire.c
- 过期键惰性删除策略:src/db.c/expireIfNeeded函数
- 过期键定期删除策略:src/expire.c/activeExpireCycle函数
- 发送通知: src/notify.c/notifyKeyspaceEvent函数