过期策略和内存淘汰策略是 Redis 中两个不同的内存管理机制, 它们的目标和触发条件完全不同
过期策略:处理已经设置 TTL 的 key,在 key 过期的时候删除它们(src/expire.c)Redis
内存淘汰策略:当内存达到 maxmemory 限制时,根据策略选择 key 进行删除(src/evict)
过期策略
Redis 的过期策略分为 2 种,被动过期和主动过期
被动过期
当用户访问一个 key 的时候,Redis 会检查 key 的状态,检查过期并删除 key
kvobj *lookupKey(redisDb *db, robj *key, int flags, dictEntryLink *link) {
kvobj *val = dbFindByLink(db, key->ptr, link);
if (val) {
int is_ro_replica = server.masterhost && server.repl_slave_ro;
int expire_flags = 0;
if (flags & LOOKUP_WRITE && !is_ro_replica)
expire_flags |= EXPIRE_FORCE_DELETE_EXPIRED;
if (flags & LOOKUP_NOEXPIRE)
expire_flags |= EXPIRE_AVOID_DELETE_EXPIRED;
if (flags & LOOKUP_ACCESS_EXPIRED)
expire_flags |= EXPIRE_ALLOW_ACCESS_EXPIRED;
// 过期删除 key
if (expireIfNeeded(db, key, val, expire_flags) != KEY_VALID) {
/* The key is no longer valid. */
val = NULL;
if (link) *link = NULL;
}
}
...
return val;
}
主动过期
Redis 的主动过期采用 双循环的设计,在后台定期扫描并删除过期的 key,防止内存泄漏
在 src/server.c:1198-1207 有写,redis 会基于 hz 参数周期性地执行 serverCron() ,该函数调用 activeExpireCycle() 检查和删除过期的键。 hz 默认值为 10,代表每秒执行 10 次。
内存淘汰策略
内存淘汰策略略指的是:当内存使用达到 maxmemory 限制时, 用于决定删除哪些键以释放内存的机制。这与过期策略不同, 淘汰策略是被动触发的内存管理手段。
在 redis.conf 中可以设置 maxmemory ,表示内存达到 maxmemory 字节时根据所选的策略删除 key
如果 Redis 无法根据策略移除键,或者策略设置为 noeviction ,Redis 将开始对那些需要使用更多内存的命令(如 SET、LPUSH 等)返回错误,并继续响应只读命令(如 GET)。
redis 提供了 8 种不同的淘汰策略(src/server.h:658-665):
#define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU)
#define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU)
#define MAXMEMORY_VOLATILE_TTL (2<<8)
#define MAXMEMORY_VOLATILE_RANDOM (3<<8)
#define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_ALLKEYS_RANDOM ((6<<8)|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_NO_EVICTION (7<<8)
只淘汰设置了过期时间的 key(volatile-):
- volatile-lru:近似 LRU 算法
- volatile-lfu:近似 LFU 算法
- volatitle-random:随机淘汰
- volatile-ttl:优先淘汰 ttl 最短的
针对所有 key(allkeys-):
- allkeys-lru:近似 LRU 算法
- allkeys-lfu:近似 LFU 算法
- allkeys-random:随机淘汰
不淘汰: noeviction , 不淘汰任何键,写操作返回错误
LRU:最近最少使用。打个比方,每次访问一个东西,把它放到榜首。如果排行榜满了,就把榜尾的东西踢掉,因为它已经很久没被用过了。
特点:记住的是“最近”的使用情况,不关心它被用了多少次。
适用场景:经常访问的网站、经常打开的文件
LFU:最不经常使用。每次访问一个东西,就给它加一分(增加使用频率)。如果排行榜满了,就把分数最低的东西踢掉。
特点:记住的是 “总共”的使用情况,看谁用的次数最少,不关心它最近有没有被用过。
适用场景:适合那些长期经常使用的数据。
为什么是近似 LRU/LFU
从性能、内存、可扩展性 3 个层面来考量
性能&内存:精确的 LRU/LFU 需要维护一个全局的数据结构来跟踪所有 key 的使用情况,每次访问 key 都要更新这个数据结构,这会带来一部分开销。而且还需要额外的内存来存储每个 key 的信息,这本身就是一种浪费。
可扩展性:精确的 LRU/LFU 很难扩展到大规模的集群环境。
所以 Redis 基于 采样(sampling) 的方式来实现近似 LRU/LFU
核心流程
以近似 LRU 为例
采样与评分
从数据库中随机采样 N 个 key(N = redis.conf的maxmemory-samples)
默认采样 5 个 key
根据策略计算每个 key 的“淘汰分数”:
- LRU:计算空闲时间,空闲时间越长分数越高
- LFU:计算反向频率(255-频率),频率越低分数越高
- TTL:使用反向过期时间,越快过期分数越高
src/evict.c:139-158
/* Calculate the idle time according to the policy. This is called
* idle just because the code initially handled LRU, but is in fact
* just a score where a higher score means better candidate. */
if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(kv);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
/* When we use an LRU policy, we sort the keys by idle time
* so that we expire keys starting from greater idle time.
* However when the policy is an LFU one, we have a frequency
* estimation, and we want to evict keys with lower frequency
* first. So inside the pool we put objects using the inverted
* frequency subtracting the actual frequency to the maximum
* frequency of 255. */
idle = 255-LFUDecrAndReturn(kv);
} else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
/* In this case the sooner the expire the better. */
idle = ULLONG_MAX - kvobjGetExpire(kv);
} else {
serverPanic("Unknown eviction policy in evictionPoolPopulate()");
}
插入淘汰池
redis 维护一个 “淘汰池(EvictionPool)”,里面存放的是适合被淘汰的 key 的候选者。淘汰池按照 升序排序,每次需要淘汰的时候,就从这个池子里面选分数最高的,达到近似的效果。
每个参与评分的 key 都 可能进入到淘汰池:
- 如果池没满,直接插入池中
- 如果池满了:将分数更高的 key 插入,替换分数低的
#define EVPOOL_SIZE 16
#define EVPOOL_CACHED_SDS_SIZE 255
struct evictionPoolEntry {
unsigned long long idle; /* Object idle time (inverse frequency for LFU) */
sds key; /* Key name. */
sds cached; /* Cached SDS object for key name. */
int dbid; /* Key DB number. */
int slot; /* Slot. */
};
删除与传播
/* 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;
kvstore *kvs;
if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
kvs = server.db[bestdbid].keys;
} else {
kvs = server.db[bestdbid].expires;
}
de = kvstoreDictFind(kvs, pool[k].slot, pool[k].key);
/* Remove the entry from the pool. */
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 = kvobjGetKey(dictGetKV(de));
break;
} else {
/* Ghost... Iterate again. */
}
}
将高分的 key 从 pool 中删除后,会调用 deleteEvictedKeyAndPropagate() 把 key 从内存中删掉,并传播到从节点。
淘汰由主节点处理,通过 DEL 命令同步到从节点。