携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
redis是一个基于内存的存储系统,所以其内存受机器的物理内存影响.参考redis官网
如果配置是0,那么默认是电脑的内存,如果是32bit 隐式大小为3G。 maxmemory redis占用的最大内存 maxmemory 100mb.如果我们不淘汰,那么它的数据就会满,满了肯定就不能再放数据,发挥 不了redis的作用! 所以我们需要通过一定的方式去保证我们Redis的可用 性。
Redis过期使用
在redis中对某个key设置过期时间如下:
- pexpire key millisecond
- expire key seconds 前者是设置毫秒,后者是设置秒,那么获取某个key的过期时间如下:
- ttl key
- pttl key 前者获取毫秒,后者是获取秒.
过期原理
那么什么是过期策略。首先,我们知道Redis有一个特性,就是Redis中的 数据我都是可以设置过期时间的,如果时间到了,这个数据就会从我们的 Redis中删除。 那么过期策略,就是讲的我怎么把Redis中过期的数据从我们Redis服务中 移除的
惰性过期
惰性过期是指: 当我们访问某个key时,如果key已经过期那么就直接删除,删除可以是同步或者异步删除.具体配置方式依赖redis.conf中
lazyfree-lazy-user-del no
但是这种却对内存非常不友好。因为如果没有再次访问,该过期删除的就可能 一直堆积在内存里面!从而不会被清除,占用大量内存。所以存在定期过期的策略进行删除.
定期过期
那么多久去清除一次,我们在讲Rehash的时候,有个方法是serverCron ,执行频率根据redis.conf中的hz配置
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10
定时任务入口位于server.c中
void databasesCron(void) {
/* Expire keys by random sampling. Not required for slaves
* as master will synthesize DELs for us. */
if (server.active_expire_enabled) {
if (iAmMaster()) {
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
} else {
expireSlaveKeys();
}
}
当前节点为master时触发过期操作.过期key位于之前提到的RedisDb中
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 */
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;
expires中存储了所有的过期key以及其过期时间.以下代码为具体过期逻辑逻辑
void activeExpireCycle(int type) {
unsigned long
effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
// 20
config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
// 25+2*1
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
2*effort,
// 10
config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
effort;
static long long last_fast_cycle = 0; /* When last fast cycle ran. */
int j, iteration = 0;
int dbs_per_call = CRON_DBS_PER_CALL; //16
long long start = ustime(), timelimit, elapsed;
if (checkClientPauseTimeoutAndReturnIfPaused()) return;
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
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;
}
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
// 25*100000/
timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = config_cycle_fast_duration; /* in microseconds. */
long total_sampled = 0;
long total_expired = 0;
// dbs_per_call=16 遍历16个库
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
/* Expired and checked in a single loop. */
unsigned long expired, sampled;
redisDb *db = server.db+(current_db % server.dbnum);
/* Increment the DB now so we are sure if we run out of time
* in the current DB we'll restart from the next. This allows to
* distribute the time evenly across DBs. */
current_db++;
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
iteration++;
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime();
if (slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
expired = 0;
sampled = 0;
ttl_sum = 0;
ttl_samples = 0;
// 不能超过20
if (num > config_keys_per_loop)
// 20
num = config_keys_per_loop;
long max_buckets = num*20;
long checked_buckets = 0;
while (sampled < num && checked_buckets < max_buckets) {
for (int table = 0; table < 2; table++) {
if (table == 1 && !dictIsRehashing(db->expires)) break;
unsigned long idx = db->expires_cursor;
idx &= db->expires->ht[table].sizemask;
dictEntry *de = db->expires->ht[table].table[idx];
long long ttl;
/* Scan the current bucket of the current table. */
checked_buckets++;
while(de) {
/* Get the next entry now since this entry may get
* deleted. */
dictEntry *e = de;
de = de->next;
ttl = dictGetSignedIntegerVal(e)-now;
if (activeExpireCycleTryExpire(db,e,now)) expired++;
if (ttl > 0) {
/* We want the average TTL of keys yet
* not expired. */
ttl_sum += ttl;
ttl_samples++;
}
sampled++;
}
}
db->expires_cursor++;
}
total_expired += expired;
total_sampled += sampled;
/* Update the average TTL stats for this database. */
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);
}
if ((iteration & 0xf) == 0) {
elapsed = ustime()-start;
if (elapsed > timelimit) {
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break;
}
}
} while (sampled == 0 ||
(expired*100/sampled) > config_cycle_acceptable_stale);
}
elapsed = ustime()-start;
server.stat_expire_cycle_time_used += elapsed;
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
double current_perc;
if (total_sampled) {
current_perc = (double)total_expired/total_sampled;
} else
current_perc = 0;
server.stat_expired_stale_perc = (current_perc*0.05)+
(server.stat_expired_stale_perc*0.95);
}
根据上述代码存在三个循环.影响循环因素:
- timelimit_exit 前15次过循环中单次过期耗时(微秒)超过timelimit
- 每个db最多400个哈希桶checked_buckets < max_buckets
- 每个db单次扫描哈希桶中key的数量最多20个.sampled < num
- 假设某个db中连续出现400个空桶,那么只能等待超过指定耗时才能结束该任务.
- 定时serverCron方法去执行清理,执行频率根据redis.conf中的hz配置 的值
- 执行清理的时候,不是去扫描所有的key,而是去扫描所有设置了过期 时间的key(redisDb.expires)
- 如果每次去把所有过期的key都拿过来,那么假如过期的key很多,就会 很慢,所以也不是一次性拿取所有的key
- 根据hash桶的维度去扫描key(扫描游标为expires_cursor),扫到20(可配)个key为止。假如第一个桶 是15个key ,没有满足20,继续扫描第二个桶,第二个桶20个key,由 于是以hash桶的维度扫描的,所以第二个扫到了就会全扫,总共扫描 35个key
- 找到扫描的key里面过期的key,并进行删除
- 如果取了400个空桶,或者扫描的删除比例跟扫描的总数超过10%,继续 执行4、5步。
- 也不能无限的循环,循环16次后回去检测时间,超过指定时间会跳出。
结语: 不积跬步无以至千里.