前言
Redis是基于内存的Key——Val数据库,可以通过配置设置最大使用内存量,来避免过多占用服务器内存。当可以内存为零的时候,Redis会主动淘汰一些数据来释放内存空间,这篇文章分析一下Redis的主动淘汰数据和被动淘汰数据策略。
正文
Redis中可以对key设置过期时间参数,目前主流删除过期数据有以下三种策略:
- 主动删除:
- 定期扫描:每隔一段时间去扫描数据库的过期数据,然后进行删除
- 定时删除:每次创建过期key的时候设置一个定时器,定时器触发后删除该数据
- 被动删除:每当访问数据的时候检查是否过期,过期则删除
++定时删除会创建很多定时器浪费CPU资源,Redis主服务是单线程运行的,是不友好的;被动删除浪费内存,因为过期不立刻释放内存++。
redis采用了定期扫描和被动删除的结合,服务器启动的时候会运行定时任务,在访问数据以及内存使用满时会主动淘汰数据。
访问时淘汰
在前几篇文章介绍到,当使用get/set方法时候,会用expireIfNeeded来判断key是否过期
int expireIfNeeded(redisDb *db, robj *key) {
//key未过期
if (!keyIsExpired(db,key)) return 0;
//如果是从库只返回装备 不涉及到删除key
if (server.masterhost != NULL) return 1;
server.stat_expiredkeys++;
//命令传播
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
//延迟删除还是直接删除
int retval = server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
if (retval) signalModifiedKey(NULL,db,key);
return retval;
}
复制代码
根据lazyfree_lazy_expire来确定数据是否延迟删除
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
//首先删除expires字典里面的数据
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
dictEntry *de = dictUnlink(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
//返回删除对象所需要的量
size_t free_effort = lazyfreeGetFreeEffort(val);
//如果对象回收工作量大 放到bio线程回收 设置entry为null
if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
dictSetVal(db->dict,de,NULL);
}
}
if (de) {
dictFreeUnlinkedEntry(db->dict,de);
if (server.cluster_enabled) slotToKeyDel(key->ptr);
return 1;
} else {
return 0;
}
}
复制代码
异步删除原理是在删除dict的key-val时候,不对数据进行释放,而是放到一个bio线程里面后台进行
以上是访问数据时进行的过期key删除策略
内存不足时淘汰
当内存不足时,一般采用的淘汰策略是LRU或LFU,前面提到过redisObject上就有lru参数来记录数据
typedef struct redisObject {
unsigned lru:LRU_BITS;
} robj;
复制代码
redis的LRU和LFU和传统的算法并不完全一样,传统的算法会维护一个双向链表,数据在链表中移动。Redis由于数据很多,频繁的移动链表节点会影响性能,所以只用了lru来记录信息而已。
对于LRU算法来说lru记录了对象访问时间,但是对于LFU来说lru不仅仅是访问次数的递增,因为在一段时间内频繁被访问的数据,以后可能不那么频繁。
在使用LFU算法的时候会将lru分为两段:
|前16bit|后8bit|
|——-|——-|——-|
|最近一次计数降低时间|计数|
LFU会随着时间的推移降低计数,为了避免上述情况,从代码中可以看到实现方式。
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
复制代码
前面提到过 set/get等命令会触发key的lru参数改变
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
复制代码
首先调用LFUDecrAndReturn来减少计数 然后增加计数
//计算距离上次改变计数时间
unsigned long LFUTimeElapsed(unsigned long ldt) {
unsigned long now = LFUGetTimeInMinutes();
if (now >= ldt) return now-ldt;
return 65535-ldt+now;
}
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8;
unsigned long counter = o->lru & 255;
//根据lfu_decay_time来计算应该 减去多少计数
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
复制代码
redis支持以下几种配置来设置内存淘汰策略
- noeviction 不要淘汰任何数据
- volatile-random 随机删除设置了过期时间的key
- allkeys-random 删除随机任何key
- volatile-ttl 删除最接近到期时间(较小的TTL)的键。
- volatile-lru 使用近似的LRU淘汰过期key
- allkeys-lru 使用近似的LRU算法淘汰所有key
- volatile-lfu 在设置了过期时间的键中,使用近似的LFU算法淘汰过期key
- allkeys-lfu 使用近似的LFU算法淘汰所有key
if (server.maxmemory && !server.lua_timedout) {
int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
int freeMemoryIfNeededAndSafe(void) {
if (server.lua_timedout || server.loading) return C_OK;
return freeMemoryIfNeeded();
}
复制代码
关于执行入口是在processCommand中判断的,调用freeMemoryIfNeeded方法
下面一步一步看freeMemoryIfNeeded流程
int freeMemoryIfNeeded(void) {
int keys_freed = 0;
//从节点配置了忽略maxmemory
if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
size_t mem_reported, mem_tofree, mem_freed;
mstime_t latency, eviction_latency, lazyfree_latency;
long long delta;
int slaves = listLength(server.slaves);
int result = C_ERR;
//客户端暂停返回OK
if (clientsArePaused()) return C_OK;
//计算是否还有空闲空间
if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
return C_OK;
mem_freed = 0;
latencyStartMonitor(latency);
//不淘汰策略
if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
goto cant_free;
复制代码
开始的部分通过getMaxmemoryState方法计算已使用的空间,以及空闲空间
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level) {
size_t mem_reported, mem_used, mem_tofree;
//获取zmalloc使用的内存
mem_reported = zmalloc_used_memory();
if (total) *total = mem_reported;
//没有配置maxmemory或者没有达到最大内存
int return_ok_asap = !server.maxmemory || mem_reported <= server.maxmemory;
if (return_ok_asap && !level) return C_OK;
mem_used = mem_reported;
//获取aofBuf和client的OutputBuffer
size_t overhead = freeMemoryGetNotCountedMemory();
mem_used = (mem_used > overhead) ? mem_used-overhead : 0;
//计算使用率
if (level) {
if (!server.maxmemory) {
*level = 0;
} else {
*level = (float)mem_used / (float)server.maxmemory;
}
}
if (return_ok_asap) return C_OK;
//还没有到达限制
if (mem_used <= server.maxmemory) return C_OK;
//需要释放多少空间
mem_tofree = mem_used - server.maxmemory;
if (logical) *logical = mem_used;
if (tofree) *tofree = mem_tofree;
return C_ERR;
}
复制代码
通过zmalloc_used_memory方法获取已使用的内存,然后进行计算
size_t zmalloc_used_memory(void) {
size_t um;
atomicGet(used_memory,um);
return um;
}
复制代码
redis使用自己编写的zmalloc方法分配内存,每次分配成功后会增加used_memory,从而统计出使用内存数
//直到释放足够空间为止
while (mem_freed < mem_tofree) {
int j, k, i;
static unsigned int next_db = 0;
sds bestkey = NULL;
int bestdbid;
redisDb *db;
dict *dict;
dictEntry *de;
复制代码
会不断循环,直到释放空间达到mem_tofree为止,以上是定义所需的数据,每次只选中一个Key
if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
{
struct evictionPoolEntry *pool = EvictionPoolLRU;
复制代码
LRU、LFU、TTL算法都是放到一起实现的
struct evictionPoolEntry *pool = EvictionPoolLRU;
while(bestkey == NULL) {
unsigned long total_keys = 0, keys;
for (i = 0; i < server.dbnum; i++) {
db = server.db+i;
//根据策略 ALLKEYS 选择所有key还是仅在过期中选
dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
db->dict : db->expires;
if ((keys = dictSize(dict)) != 0) {
evictionPoolPopulate(i, dict, db->dict, pool);
total_keys += keys;
}
}
//没有key选中
if (!total_keys) break;
/* Go backward from best to worst element to evict. */
for (k = EVPOOL_SIZE-1; k >= 0; k--) {
if (pool[k].key == NULL) continue;
bestdbid = pool[k].dbid;
//根据策略 ALLKEYS 选择所有key还是仅在过期中选
if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
de = dictFind(server.db[pool[k].dbid].dict,
pool[k].key);
} else {
de = dictFind(server.db[pool[k].dbid].expires,
pool[k].key);
}
//删除pool对应key
if (pool[k].key != pool[k].cached)
sdsfree(pool[k].key);
pool[k].key = NULL;
pool[k].idle = 0;
/* If the key exists, is our pick. Otherwise it is
* a ghost and we need to try the next element. */
if (de) {
bestkey = dictGetKey(de);
break;
} else {
//key不存在
}
}
}
复制代码
开头使用了evictionPoolEntry数组来存放需要淘汰的数据,一次选择16个
#define EVPOOL_SIZE 16
#define EVPOOL_CACHED_SDS_SIZE 255
struct evictionPoolEntry {
unsigned long long idle; //LRU代表空闲时间 LFU代表频率
sds key; /* Key name. */
sds cached; /* Cached SDS object for key name. */
int dbid; //数据库id
};
复制代码
可以从上面一大段代码中得知:
- ALLKEYS的区别只在于从普通dict中选择,还是从expires中选择
- 通过evictionPoolPopulate筛选数据放到pool中,然后逆序拿出来获取bestkey
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
int j, k, count;
dictEntry *samples[server.maxmemory_samples];
//获取到count个随机元素
count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
for (j = 0; j < count; j++) {
unsigned long long idle;
sds key;
robj *o;
dictEntry *de;
de = samples[j];
key = dictGetKey(de);
//如果不是删除到期时间 那么应该从dict里面获取对象
if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
if (sampledict != keydict) de = dictFind(keydict, key);
o = dictGetVal(de);
}
//LRU和LFU计算不同
if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(o);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
idle = 255-LFUDecrAndReturn(o);
} else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
//dictGetVal返回的是expiry数据库过期时间 用最大时间-过期时间 过期时间越短 值越大
idle = ULLONG_MAX - (long)dictGetVal(de);
} else {
serverPanic("Unknown eviction policy in evictionPoolPopulate()");
}
//idle排序
k = 0;
while (k < EVPOOL_SIZE &&
pool[k].key &&
pool[k].idle < idle) k++;
//无法插入 插入位置是0
if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
continue;
//空槽可以插入
} else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
} else {
//最右边是空的 移动元素腾出空间插入
if (pool[EVPOOL_SIZE-1].key == NULL) {
sds cached = pool[EVPOOL_SIZE-1].cached;
memmove(pool+k+1,pool+k,
sizeof(pool[0])*(EVPOOL_SIZE-k-1));
pool[k].cached = cached;
} else {
//没有空位 插入到k-1位 需要丢弃
k--;
sds cached = pool[0].cached;
if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
memmove(pool,pool+1,sizeof(pool[0])*k);
pool[k].cached = cached;
}
}
/* 缓存字符串 */
int klen = sdslen(key);
if (klen > EVPOOL_CACHED_SDS_SIZE) {
pool[k].key = sdsdup(key);
} else {
memcpy(pool[k].cached,key,klen+1);
sdssetlen(pool[k].cached,klen);
pool[k].key = pool[k].cached;
}
pool[k].idle = idle;
pool[k].dbid = dbid;
}
}
复制代码
可以总结如下:
- evictionPoolPopulate也是随机选择的,通过dictGetSomeKeys随机选择n个元素,返回实际选择的数量count
- pool是排序的,通过LRU或LFU或者TTL的值来排序,最终排序出16个值得淘汰的数据
dictGetSomeKeys方法就不看了,原理就是随机到dict的一个槽,然后顺序收集n个数据为止
/* 随机删除所有/设置过期的key */
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
{
//通过nextDb来选择遍历
for (i = 0; i < server.dbnum; i++) {
j = (++next_db) % server.dbnum;
db = server.db+j;
dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
db->dict : db->expires;
if (dictSize(dict) != 0) {
de = dictGetRandomKey(dict);
bestkey = dictGetKey(de);
bestdbid = j;
break;
}
}
}
复制代码
随机方法就是真的随机获取一个key
if (bestkey) {
db = server.db+bestdbid;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
//传播过期命令
propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
//以下是删除数据 计算释放的空间
delta = (long long) zmalloc_used_memory();
latencyStartMonitor(eviction_latency);
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
signalModifiedKey(NULL,db,keyobj);
latencyEndMonitor(eviction_latency);
latencyAddSampleIfNeeded("eviction-del",eviction_latency);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
server.stat_evictedkeys++;
notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
keyobj, db->id);
decrRefCount(keyobj);
keys_freed++;
if (slaves) flushSlavesOutputBuffers();
//当使用异步删除的时候 需要检查是否已经释放
if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
/* Let's satisfy our stop condition. */
mem_freed = mem_tofree;
}
}
} else {
goto cant_free; /* nothing to free... */
}
}
result = C_OK;
复制代码
真正的删除逻辑,还是分为异步删除和同步删除
cant_free:
if (result != C_OK) {
latencyStartMonitor(lazyfree_latency);
while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
result = C_OK;
break;
}
usleep(1000);
}
latencyEndMonitor(lazyfree_latency);
latencyAddSampleIfNeeded("eviction-lazyfree",lazyfree_latency);
}
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
return result;
}
复制代码
无法释放内存会sleep主线程
总结:
- LRU/LFU只是标记上的区别,真正筛选还是随机,有了LRU/LFU/TTL只是让随机的前提下更精确。
- ALLKEYS的实现只是dict和expires字典的区别,使用ALLKEYS可能会淘汰掉正常数据。
定期删除
前面介绍的两种都是在执行命令的时候才会进行淘汰数据,redis还定期淘汰数据,和前面介绍的定期rehash一样。
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();
}
}
复制代码
activeExpireCycle方法定时执行
void activeExpireCycle(int type) {
unsigned long
//有效期的活跃度 控制以下参数的
effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
//抽样数量
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,
//CPU使用率
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
2*effort,
//抽样删除百分比
config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
effort;
复制代码
active_expire_effort是可以自定义的,用于控制以下几个参数的大小,范围1-10
根据active_expire_effort范围计算出来以下几个参数的范围:
- config_keys_per_loop抽样数量:[25-65]个
- config_cycle_fast_duration快速时间间隔:[1250-3250]Microseconds
- config_cycle_slow_time_perc慢扫描CPU使用率:[25-43]%
- config_cycle_acceptable_stale抽样百分比:[10-1]%
//用静态参数记录最后选择的DB、上次是否中断、上次调用的时间
static unsigned int current_db = 0;
static int timelimit_exit = 0;
static long long last_fast_cycle = 0;
int j, iteration = 0;
//处理数据库个数默认为16
int dbs_per_call = CRON_DBS_PER_CALL;
long long start = ustime(), timelimit, elapsed;
复制代码
以上是一些参数
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
//如果上次没有提前退出 且 系统key过期估算的百分比小于当前设置的百分比
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;
}
复制代码
如果是指定快速扫描,那么以上情况这一次不用扫描
//扫描的次数不应该大于总db数量 如果上次提前退出了要扫描全db
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
//通过cpu使用率和当前CPU执行效率来计算 此次收集时间限制
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;
//累积数据
long total_sampled = 0;
long total_expired = 0;
复制代码
dbs_per_call和timelimit的条件初始化
接下来是淘汰过程了
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
unsigned long expired, sampled;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
do {
unsigned long num, slots;
// 计算key的平均过期时间
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();
// num/slots 表示已用百分比 该表空间只用了1%不到 则不要扫描这个表 浪费时间
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
expired = 0;
sampled = 0;
ttl_sum = 0;
ttl_samples = 0;
if (num > config_keys_per_loop)
num = config_keys_per_loop;
//访问最大的次数 是 采样数 * 20
long max_buckets = num*20;
long checked_buckets = 0;
while (sampled < num && checked_buckets < max_buckets) {
for (int table = 0; table < 2; table++) {
//循环从两个表扫描 如果没有rehash 跳过扫描table[1]
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;
//扫描槽上所有节点
checked_buckets++;
while(de) {
dictEntry *e = de;
de = de->next;
ttl = dictGetSignedIntegerVal(e)-now;
//尝试删除过期数据 成功增加expired
if (activeExpireCycleTryExpire(db,e,now)) expired++;
if (ttl > 0) {
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;
//跟新平均值 当前计算的权重只占2%
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}
//每16个周期迭代一次 超时了现在中断
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
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);
/* Update our estimate of keys existing but yet to be expired.
* Running average with this sample accounting for 5%. */
double current_perc;
if (total_sampled) {
current_perc = (double)total_expired/total_sampled;
} else
current_perc = 0;
//跟新服务器stat_expired_stale_perc current_perc只影响5%
server.stat_expired_stale_perc = (current_perc*0.05)+
(server.stat_expired_stale_perc*0.95);
复制代码
根据注释,有以下几点:
- 会扫描所有的db,每次会计算时间,如果超时中断并且设置标志
- 会跳过只使用1%不到的表 因为收集这种表得不偿失
- 会记录当前db扫描出平均过期时间,然后跟新字段,只占2%权重
- 最后跟新stat_expired_stale_perc(系统key过期估算的百分比),这一次只占5%权重
int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
long long t = dictGetSignedIntegerVal(de);
if (now > t) {
sds key = dictGetKey(de);
robj *keyobj = createStringObject(key,sdslen(key));
propagateExpire(db,keyobj,server.lazyfree_lazy_expire);
if (server.lazyfree_lazy_expire)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",keyobj,db->id);
trackingInvalidateKey(NULL,keyobj);
decrRefCount(keyobj);
server.stat_expiredkeys++;
return 1;
} else {
return 0;
}
}
复制代码
删除过期key方法,还是分为同步或者异步
以上总结:定期删除会根据cpu使用率计算出运行一次所需要的最大时间,然后通过db上面的expires_cursor参数记录删除过期key的位置,在规定的时间范围内尽可能删除过期key,并且会计算过期率。
总结
- Redis的LRU/LFU主要区别在记录上,实际删除逻辑都是一样的
- Redis异步删除会将释放内存操作放到主线程以外执行
- Redis释放内存只能释放key-val内存,对于主从和AOF等缓冲区则不涉及
- Redis无法释放内存的时候会sleep,这个过程不容易察觉