数据库存储在 redisDb 结构中,而服务端 redisServer 结构中保存着 redisDb 对象和个数,个数可以在配置文件中进行更新。
数据结构
typedef struct redisDb { // 保存 k,v 数据 dict *dict; /* The keyspace for this DB */ // 保存 k, exprie 时间 dict *expires; /* Timeout of keys with a timeout set */ // 阻塞的 key dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ // dict *ready_keys; /* Blocked keys that received a PUSH */ // 监控的 key dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ int id; /* Database ID */ long long avg_ttl; /* Average TTL, just for stats */ unsigned long expires_cursor; /* Cursor of the active expire cycle. */ list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */ } redisDb;
而 redisServer 中都包含了 redisDb 数据结构。表示当前使用的是那个 db。
struct redisServer { /* General */ pid_t pid; /* Main process pid. */ pthread_t main_thread_id; /* Main thread id */ char *configfile; /* Absolute config file path, or NULL */ char *executable; /* Absolute executable file path. */ char **exec_argv; /* Executable argv vector (copy). */ int dynamic_hz; /* Change hz value depending on # of clients. */ // ... };
数据库切换
数据库切换是使用 select 命令来执行,实际上第 n 个 db 指向客户端的 db 对象 c -> db = &server.db[id]
int selectDb(client *c, int id) { if (id < 0 || id >= server.dbnum) return C_ERR; c->db = &server.db[id]; return C_OK; }
键空间
由前文可以知道 redis 可以有多个数据库,而不同数据库之间是不相互影响的,如何做到的呢?键空间可以理解成 C++ 里面的命名空间,用来隔离,数据存在 redisDb 中的dict 对象,因此对于键的操作,基本都是基于键空间来操作的。
本质其实就是每个 redisDb 中的 dict 对象。很好理解,因为选择了不同的 db,肯定下面的键也不一样。
void dbAdd(redisDb *db, robj *key, robj *val) { sds copy = sdsdup(key->ptr); int retval = dictAdd(db->dict, copy, val); serverAssertWithInfo(NULL,key,retval == DICT_OK); signalKeyAsReady(db, key, val->type); if (server.cluster_enabled) slotToKeyAdd(key->ptr); }
过期键
- 在 redis 中过期键保存在 redisD中的 expres 变量里中, expires 是 dict 指针类型
- 存储方式
- key 保存的是数据库的键对象
- value 保存的是数据对象的过期时间,长整型 Unix 时间。
void setExpire(client *c, redisDb *db, robj *key, long long when) { dictEntry *kde, *de; /* Reuse the sds from the main dict in the expire dict */ kde = dictFind(db->dict,key->ptr); serverAssertWithInfo(NULL,key,kde != NULL); dictSetSignedIntegerVal(de,when); de = dictAddOrFind(db->expires,dictGetKey(kde)); int writable_slave = server.masterhost && server.repl_slave_ro == 0; if (c && writable_slave && !(c->flags & CLIENT_MASTER)) rememberSlaveKeyWithExpire(db,key); }
- 设置时间
相对方式:expire
绝对方式:expireat
如 expire 命令:
setExpire(c, c->db, key, when)
---> kde = dictFind(db->dict,key->ptr);
---> de = dictAddOrFind(db->expires,dictGetKey(kde));
- 删除过期时间
persist
- 查看过期时间:ttl
lookupKeyReadWithFlags --> getExpire --> ttl = expire-mstime();
- 键过期策略
- 定期删除,每间隔一段时间,程序对数据进行一次勘查,删除里面的过期键。对内存友好,尽可能的删掉一些过期的键,但是对 CPU 不友好,而且需要设置好定期时间以及每次删除数量
- 定时删除,在设计过期键的时候,建立定时器,让定时器在键过期时间达到时,立即删除对键的操作,可以看作一种时删除的一种,但是该方案坑你创建较多的定时器,而且 redis 定时器采用的是链表,查找某个 key 时间负载度为 O(n)
- 惰性删除:每次在获取键的时候,来处理过期键,确定是对内存不友好
- 触发清理策略:当设置了 maxmemory , 且超过过期时间。
redis 中采用的是定期删除和惰性删除两种
- 定期删除分为两种模式,分别在不同的场景下使用。因此结束判断不一样。
在定时任务 serverCorn 中的 databaseCron 中,为 ACTIVE_EXPIRE_CYCLF_SLOW, 这就意味着我们可以花费更多的时间来处理。而在十佳循环之前的 beforeSleep 函数中则为 ACTIVE_EXPIRE_CYCLE_FAST,因为不能影响处理时间,还做一个优化
if (type == ACTIVE_EXPIRE_CYCLE_FAST) { /* Don't start a fast cycle if the previous cycle did not exit * for time limit, unless the percentage of estimated stale keys is * too high. Also never repeat a fast cycle for the same period * as the fast cycle total duration itself. */ if (!timelimit_exit && server.stat_expired_stale_perc < config_cycle_acceptable_stale) return; if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2) return; last_fast_cycle = start; }
- REDIS_EXPIRELOOKUPS_TIME_PERC 是单位时间内分配给 activeExpireCycle 函数执行的 CPU 比例, 默认为。25
- 每次循环最多 16 个库。
- 每个库要求找到过期键达到 5 个就行(注意的是,因为那个库随机选取一个key 的,所以数量不能太少,太少随机效果不好)。
- 每个库里面,每次随机选取 20 个 key 。 检查是否过期键,过期就记数一次。每间隔 16 次检查一下时间是否超过限制,如果超过需要推出玄幻。不在继续查找。
对于过期键 RDB/AOF/主从的影响,后期讲述。
通知和事件
数据库通知主要是利用 redis 中支持发布订阅模式,让客户端通过订阅给顶的频道或者模式(保存在 client 和 server 的 pubsub_channels 和 pubsub_patterns, 但保存的内容不一样)来获取数据库中的键的变化。主要是分为键空间通知和事件通知两类,要开启该功能需要在配置文件中设置参数/动态设置 notify-keyspace-events
键空间通知
keyspace@ 某个键执行了什么命令
事件通知
keyevent@ 某个事件被哪些命令执行
启用事件通知有两种方式:
- 配置文设置:格式:
notify-keyspace-events
Elg. 如果要配置 K 和 E 必须要开启一个。
- 命令设置
config set notify-keyspace-events Elg
3、查看发布订阅的 key
PSUBSCRICE __keyevent@*__:expired
4、也可以使用 tcpdump 抓包来查看