DB结构体
Redis默认有16个数据库,存储数据前必须先通过SELECT INDEX来指定DB(默认index为0,DB结构体对应server.h/redisDb),DB主要存储并维护键值对信息。值得注意的是Redis目前没有命令可以获取当前正在操作的库,所以比较好的做法是每次操作前select。
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
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 */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
dict字典又称键空间,存储了所有键值对,值是五大类型之一;expires存储的是设置了过期时间的key与其失效时间的键值对;
Key读写
当用户读取某个Key时,如果dict中不存在该Key,则miss次数+1并返回客户端空;否则hit次数+1并且更新键的最后一次使用时间,通过这个值可以计算出key的闲置时间,闲置时间的长短又会影响回收策略,如果Key已失效则同时清除dict与expires中的键值对,并向Slave发送DEL命令。通过info stats可以查看hist和miss等信息,通过Object idletime key可以查看key的闲置时间。
当写入某个Key时,如果dict中已经存在该key则进行对应值的更新,否则存入新的键值对。对设置了过期时间的Key写入时同时也存入一份到expires字典当中,删除时同样也清除expires中对应键值对。
Key过期时间设置
redis有四个命令expire、pexpire、expireat、pexpireat可以绑定key的过期时间(persist可以解除绑定)。expire和pexpire设置key可以存活的时间,expireat和pexpireat 设置key失效的时间点,前者以秒为单位,后者以毫秒为单位,前三个命令底层使用的都是pexpireat。
Key过期清理策略
- 定时清理:在设置k/v对时,同时开启一个定时器,或将失效时间点注入某个定时器,在有效时间到达后移除该键值对。这种方式一般能尽快释放被占用的内存,但对CPU性能消耗很严重,特别是在大量数据或大量连续请求的情况下,根本没这么多CPU资源拿来消耗,实际中并不常见。
- 惰性清理:在客户端请求某个key时才检测其有效性。这种方式不会像定时清理一样大量消耗CPU资源,但对于内存却可能产生内存泄漏,如果客户端永远不请求那些失效的key,那么这些key就会一直驻留在内存中,永远无法释放(flush等操作除外)。
- 定期清理:周期性的检测key的有效性(不一定是全量key),这种方式即不会大量消耗CPU资源,也不会导致内存泄漏,但key基本上不会被及时清理,实际中清理的时机与频率并不容易确定。
Redis综合采用了惰性清理与定期清理策略,关于惰性清理在上方的已经说明了,这里分析一下定期清理的源码,在3.2.8版本中源码位于server.c/activeExpireCycle,这个函数通过周期性调用serverCron().databasesCron()来间接调用,频率默认为
#define CONFIG_DEFAULT_HZ 10 /* Time interrupt calls/sec. */ 每秒10次,源码如下:
/*
*type=ACTIVE_EXPIRE_CYCLE_FAST表示快清理; type=ACTIVE_EXPIRE_CYCLE_SLOW表示慢清理;
*/
void activeExpireCycle(int type) {
static unsigned int current_db = 0; /* 当前清理的DB */
static int timelimit_exit = 0; /* 执行时长是否已到 */
static long long last_fast_cycle = 0; /* 上次运行时间. */
int j, iteration = 0;
int dbs_per_call = CRON_DBS_PER_CALL; /* 默认请理DB数量:16*/
long long start = ustime(), timelimit;
if (clientsArePaused()) return;
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
/* 上次快清理尚未结束,本次不开启 */
if (!timelimit_exit) return;
if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
last_fast_cycle = start;
}
/* 确定DB数量 */
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
/* 慢清理的执行时长: 1000000 * 25/10/100 = 25000 == 25毫秒 */
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
/* 快清理执行时长:1毫秒 */
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
/* 迭代DB */
for (j = 0; j < dbs_per_call; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
/* DB索引加1,指向下一个要处理的DB */
current_db++;
/* 从当前DB中随机抽取最多20个key
* 1:获取每个key的ttl,算出平均ttl
* 2:清理失效的key,并记录失效key的个数,如果失效key的个数不超过1/4(少于5个),则认为当前DB暂不需要清理,执行下个DB的处理。
* 3:每执行16次,则检测是否执行时长已到,执行时间已达上限则退出本次清理。等待下次周期性调用
*/
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime();
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
/* 随机抽取最多20个key */
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
while (num--) {
dictEntry *de;
long long ttl;
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;
if (activeExpireCycleTryExpire(db,de,now)) expired++;/* 记录失效个数 */
if (ttl > 0) {
ttl_sum += ttl;
ttl_samples++;
}
}
if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}
/* 每执行16次检测一次执行时长 */
iteration++;
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
long long elapsed = ustime()-start;
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
if (elapsed > timelimit) timelimit_exit = 1;
}
/* 执行时长已到,退出本次清理. */
if (timelimit_exit) return;
/* 失效个数>1/4则继续,否则结束对当前DB的清理. */
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
}
基本上这个函数的工作模式可以概括如下:
1)迭代一定数量的DB,最大数量16。
2)从每个DB中随机抽取最多20个key
* 1:获取每个key的ttl,用于统计求平均ttl
* 2:清理失效的key,并记录失效key的个数,如果失效key的个数不超过1/4(少于5个),则认为当前DB暂不需要清理,执行下个DB的处理。
* 3:为了防止长时间的阻塞,每执行16次检测是否执行时长已到,执行时间已达上限则退出本次清理,等待下次周期性调用。但当DB数量不足16个时,此步不会被执行,此时即使执行时长已到,也不会结束,直到最后清理完成才会终止。
RDB和AOF对过期Key的处理
RDB和AOF重写本质上都是存储当前内存中的数据,只不过RDB以数据文件的形式存储,而重写AOF则是以命令形式存储,但是对于内存中那些已经失效的Key,两者都不会存储,对于RDB来说只需要保存可用数据,没必要在加载后再进行清理无效数据,对于AOF来说只需要保存最小指令集,也没必要保存写入和删除两条指令。但正常的AOF对于失效Key,会追加一条Del指令到aof文件中。
主从节点对过期的Key的处理
对于从节点而言由于它的数据来源于主节点,为了保持与主节点状态一致,在Master发送删除指令之前,它不会采用任何策略来清理失效的key, 但针对失效Key的所有请求都会返回空。之所以返回空,是因为从节点在key失效但尚未接收到或丢失主节点del命令的情况下,需要与主节点保持一致的响应。
总结:
- Redis数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,键是一个字符串,值是五大类型之一;expires字典则负责保存键与过期时间对。所以对键的操作都是建立在字典操作之上
- Redis使用惰性删除和定期删除两种策略来删除过期的键
- 一个失效键被删除之后,会追加一条DEL到现有AOF文件中,但对于RDB或AOF重写都不会包含已经过期的键
- 主节点删除某个键时,会向所有从节点发送一条DEL命令;对于从节点来说,为了保持与主节点的一致性,在未接收到DEL命令前不会采用任何策略删除它,但对失效key的所有请求都返回空。