Redis的maxmemory
支持的内存淘汰机制使得其成为一种有效的缓存方案,成为memcached的有效替代方案。
当内存达到maxmemory
后,Redis会按照maxmemory-policy
启动淘汰策略。
Redis 3.0中已有淘汰机制:
maxmemory-policy | 含义 | 特性 |
---|---|---|
noeviction | 不淘汰 | 内存超限后写命令会返回错误(如OOM, del命令除外) |
allkeys-lru | 所有key的LRU机制 在 | 所有key中按照最近最少使用LRU原则剔除key,释放空间 |
volatile-lru | 易失key的LRU | 仅以设置过期时间key范围内的LRU(如均为设置过期时间,则不会淘汰) |
allkeys-random | 所有key随机淘汰 | 一视同仁,随机 |
volatile-random | 易失Key的随机 | 仅设置过期时间key范围内的随机 |
volatile-ttl | 易失key的TTL淘汰 | 按最小TTL的key优先淘汰 |
其中LRU(less recently used)经典淘汰算法在Redis实现中有一定优化设计,来保证内存占用与实际效果的平衡,这也体现了工程应用是空间与时间的平衡性。
PS:值得注意的,在主从复制模式Replication下,从节点达到maxmemory时不会有任何异常日志信息,但现象为增量数据无法同步至从节点。
Redis中LRU是近似LRU实现,并不能取出理想LRU理论中最佳淘汰Key,而是通过从小部分采样后的样本中淘汰局部LRU键。
Redis 3.0中近似LRU算法通过增加待淘汰元素池的方式进一步优化,最终实现与精确LRU非常接近的表现。
精确LRU会占用较大内存记录历史状态,而近似LRU则用较小内存支出实现近似效果。
以下是理论LRU和近似LRU的效果对比:
总结图中展示规律,
结论:
采样值设置通过maxmemory-samples指定,可通过CONFIG SET maxmemory-samples 动态设置,也可启动配置中指定maxmemory-samples
源码解析
int freeMemoryIfNeeded(void){
while (mem_freed < mem_tofree) {
if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
return REDIS_ERR; /* We need to free memory, but policy forbids. */
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{......}
/* volatile-random and allkeys-random policy */
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
{......}
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
// 淘汰池函数
evictionPoolPopulate(dict, db->dict, db->eviction_pool);
while(bestkey == NULL) {
evictionPoolPopulate(dict, db->dict, db->eviction_pool);
// 从后向前逐一淘汰
for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
if (pool[k].key == NULL) continue;
de = dictFind(dict,pool[k].key); // 定位目标
/* Remove the entry from the pool. */
sdsfree(pool[k].key);
/* Shift all elements on its right to left. */
memmove(pool+k,pool+k+1,
sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
/* Clear the element on the right which is empty
* since we shifted one position to the left. */
pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
pool[REDIS_EVICTION_POOL_SIZE-1].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 {
/* Ghost... */
continue;
}
}
}
}
/* volatile-ttl */
else if (server.maxmemory_policy == EDIS_MAXMEMORY_VOLATILE_TTL) {......}
// 最终选定待删除键bestkey
if (bestkey) {
long long delta;
robj *keyobj = createStringObject(bestkey,sdslenbestkey)); // 目标对象
propagateExpire(db,keyobj);
latencyStartMonitor(eviction_latency); // 延迟监控开始
dbDelete(db,keyobj); // 从db删除对象
latencyEndMonitor(eviction_latency);// 延迟监控结束
latencyAddSampleIfNeeded("eviction-del",iction_latency); // 延迟采样
latencyRemoveNestedEvent(latency,eviction_latency);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta; // 释放内存计数
server.stat_evictedkeys++; // 淘汰key计数,info中可见
notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted", keyobj, db->id); // 事件通知
decrRefCount(keyobj); // 引用计数更新
keys_freed++;
// 避免删除较多键导致的主从延迟,在循环内同步
if (slaves) flushSlavesOutputBuffers();
}
}
}
从Redis4.0开始,新增LFU淘汰机制,提供更好缓存命中率。LFU(Least Frequently Used)通过记录键使用频率来定位最可能淘汰的键。
对比LRU与LFU的差别:
Redis 4.0中新增两种LFU淘汰机制:
LFU使用Morris counters计数器占用少量位数来评估每个对象的访问频率,并随时间更新计数器。此机制实现与近似LRU中采样类似。但与LRU不同,LFU提供明确参数来指定计数更新频率。
这两个因子形成一种平衡,通过少量访问 VS 多次访问 的评价标准最终形成对键重要性的评判。
u-decay-time:衰减周期,单位分钟,计数器衰减的分钟数
这两个因子形成一种平衡,通过少量访问 VS 多次访问 的评价标准最终形成对键重要性的评判。