前置知识: Redis

内存淘汰策略

3 min中级

Redis 内存淘汰策略详解:LRU、LFU、Random、TTL 四类八种策略的原理、配置与适用场景。

# 内存淘汰策略


1. 内存淘汰概述

1.1 触发条件

当 Redis 使用内存超过 maxmemory 配置时,触发淘汰策略:

# 设置最大内存
CONFIG SET maxmemory 4gb

# 查看当前内存使用
INFO memory
# used_memory: 3.8GB
# maxmemory: 4GB

1.2 八种淘汰策略

策略淘汰范围算法适用场景
noeviction不淘汰-数据不能丢失
allkeys-lru所有键LRU通用缓存
allkeys-lfu所有键LFU热点数据明显
allkeys-random所有键随机无访问偏好
volatile-lru有TTL的键LRU混合使用
volatile-lfu有TTL的键LFU混合使用
volatile-random有TTL的键随机混合使用
volatile-ttl有TTL的键最短TTL优先业务有明确TTL

2. LRU 算法

2.1 传统 LRU

传统 LRU 维护一个按访问时间排序的链表:

访问顺序: A → B → C → D → E

最近访问的在头部,最久未访问的在尾部
淘汰时删除尾部元素

问题: 需要大量内存维护链表指针

2.2 Redis 近似 LRU

Redis 使用采样近似 LRU,不维护全局链表:

1. 随机采样 N 个键(N = maxmemory-samples,默认5)
2. 淘汰其中最久未访问的键
3. 重复直到内存低于阈值

2.3 LRU 时钟

每个 Redis 对象头包含一个 24 位的 LRU 时钟:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:24;    // LRU 时钟(秒级精度,LFU 模式下复用为 16位时间+8位计数器)
    int refcount;
    void *ptr;
} robj;
LRU 时钟分辨率: 1000ms(lru 字段以秒为单位累计)
24位最大值: 2^24 = 16777216 秒 ≈ 194 天循环回绕

计算空闲时间: 当前全局时钟 - object.lru(差值取模处理回绕)

2.4 采样数对效果的影响

maxmemory-samples = 3:  接近真实LRU的 80%
maxmemory-samples = 5:  接近真实LRU的 90%  ← 默认
maxmemory-samples = 10: 接近真实LRU的 95%
maxmemory-samples = 20: 接近真实LRU的 98%

采样数越大,越接近真实LRU,但CPU开销也越大

3. LFU 算法

3.1 LFU 原理

LFU(Least Frequently Used)根据访问频率淘汰,比 LRU 更适合热点数据场景:

LRU: 最近访问的保留 → 偶尔访问的大文件可能挤掉频繁访问的小数据
LFU: 频繁访问的保留 → 真正的热点数据不会被淘汰

3.2 Redis LFU 实现

Redis 4.0+ 引入 LFU,复用 lru 字段的 24 位:

24位 lru 字段:
  高16位: 最后衰减时间(分钟级)
  低8位:  对数计数器(logarithmic counter)

计数器范围: 0-255
实际频率范围: 1-约100万次/分钟

3.3 对数计数器

计数器不是「每访问一次加一」,而是以递减的概率递增:counter 越大, 再涨一分越难。8 位(0-255)因此足以表达从个位数到百万级的访问频率。

新对象初始 counter 为 LFU_INIT_VAL = 5(避免新建键立刻被当作冷数据淘汰)。

更新规则(概率递增):

uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;          // 已饱和
    double r = (double)rand() / RAND_MAX;    // 0~1 随机数
    double baseval = counter - LFU_INIT_VAL; // LFU_INIT_VAL = 5
    if (baseval < 0) baseval = 0;
    // lfu-log-factor 越大 / counter 越高 → 概率越低,增长越慢
    double p = 1.0 / (baseval * server.lfu_log_factor + 1);
    if (r < p) counter++;
    return counter;
}

直观理解(lfu-log-factor=10 时):counter 从 5 涨到 10 很快(几十次访问), 从 10 涨到 100 则需要数十万次访问——高段位天然代表「极热」。

3.4 衰减机制

LFU 计数器随时间衰减,避免历史热点永远不被淘汰:

衰减规则:
  每经过 lfu-decay-time 分钟,counter 减 1
  lfu-decay-time 默认为 1 分钟

示例:
  counter=10, 5分钟无访问 → counter=5
  counter=10, 持续访问 → counter 保持或增长

3.5 LFU 配置

# 淘汰策略
CONFIG SET maxmemory-policy allkeys-lfu

# 衰减时间(分钟):每过 N 分钟未访问,counter 减 1
CONFIG SET lfu-decay-time 1

# 对数增长因子:越大 counter 增长越慢(区分度更高)
CONFIG SET lfu-log-factor 10

4. 策略选择

4.1 决策流程

flowchart TD
    T0["是否有必须保留的键?"]
    T1["是 → 使用 volatile-* 策略"]
    T2["这些键不设TTL,不会被淘汰"]
    T3["访问模式?"]
    T4["热点明显 → volatile-lfu"]
    T5["均匀访问 → volatile-random"]
    T6["有TTL偏好 → volatile-ttl"]
    T7["否 → 使用 allkeys-* 策略"]
    T8["访问模式?"]
    T9["热点明显 → allkeys-lfu"]
    T10["近期访问优先 → allkeys-lru"]
    T11["均匀访问 → allkeys-random"]
    T0 --> T1
    T6 --> T7
    T7 --> T8
    T8 --> T9
    T8 --> T10
    T8 --> T11

4.2 常见场景推荐

场景推荐策略理由
纯缓存allkeys-lfu热点数据保留
会话缓存allkeys-lru近期活跃保留
消息队列volatile-ttl过期自动清理
持久数据+缓存volatile-lru持久数据不设TTL
数据不能丢失noeviction写入报错不淘汰

4.3 监控与调优

# 查看淘汰统计
INFO stats
# evicted_keys: 1234  ← 被淘汰的键数量

# 查看内存使用
INFO memory
# used_memory: 3.8GB
# maxmemory: 4GB
# mem_fragmentation_ratio: 1.2

# 调优建议:
# 1. evicted_keys 持续增长 → 增大 maxmemory 或优化策略
# 2. 缓存命中率低 → 考虑换策略(LRU → LFU)
# 3. 内存碎片率高 → 重启或使用 activedefrag

触发条件

基本写法:设置最大内存 CONFIG SET maxmemory <bytes>

# 设置 Redis 最大内存为 4GB
CONFIG SET maxmemory 4gb

基本写法:查看内存使用 INFO memory

# 查看当前内存使用情况
INFO memory

淘汰策略配置

基本写法:不淘汰策略 CONFIG SET maxmemory-policy noeviction

# 内存不足时拒绝写入,返回错误
CONFIG SET maxmemory-policy noeviction

基本写法:所有键 LRU 策略 CONFIG SET maxmemory-policy allkeys-lru

# 所有键中淘汰最久未访问的键
CONFIG SET maxmemory-policy allkeys-lru

基本写法:所有键 LFU 策略 CONFIG SET maxmemory-policy allkeys-lfu

# 所有键中淘汰访问频率最低的键
CONFIG SET maxmemory-policy allkeys-lfu

基本写法:所有键随机策略 CONFIG SET maxmemory-policy allkeys-random

# 所有键中随机淘汰
CONFIG SET maxmemory-policy allkeys-random

基本写法:有 TTL 的键 LRU 策略 CONFIG SET maxmemory-policy volatile-lru

# 有过期时间的键中淘汰最久未访问的键
CONFIG SET maxmemory-policy volatile-lru

基本写法:有 TTL 的键 LFU 策略 CONFIG SET maxmemory-policy volatile-lfu

# 有过期时间的键中淘汰访问频率最低的键
CONFIG SET maxmemory-policy volatile-lfu

基本写法:有 TTL 的键随机策略 CONFIG SET maxmemory-policy volatile-random

# 有过期时间的键中随机淘汰
CONFIG SET maxmemory-policy volatile-random

基本写法:有 TTL 的键最短 TTL 优先策略 CONFIG SET maxmemory-policy volatile-ttl

# 有过期时间的键中淘汰 TTL 最短的键
CONFIG SET maxmemory-policy volatile-ttl

LRU 算法

基本写法:设置 LRU 采样数 CONFIG SET maxmemory-samples <N>

# 设置 LRU 采样数为 5(默认值)
CONFIG SET maxmemory-samples 5

结构定义写法:redisObject LRU 时钟字段 struct redisObject { unsigned lru:24; }

// 每个 Redis 对象头包含一个 24 位的 LRU 时钟
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:24;    // LRU 时钟(秒级精度,LFU 模式下复用为 16位时间+8位计数器)
    int refcount;
    void *ptr;
} robj;

LFU 算法

基本写法:设置 LFU 衰减时间 CONFIG SET lfu-decay-time <minutes>

# 设置 LFU 计数器衰减时间为 1 分钟
CONFIG SET lfu-decay-time 1

基本写法:设置 LFU 计数器因子 CONFIG SET lfu-log-factor <factor>

# 设置 LFU 计数器对数因子为 10
CONFIG SET lfu-log-factor 10

函数源码写法:LFU 对数计数器更新 uint8_t LFULogIncr(uint8_t counter)

// LFU 对数计数器更新函数
uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;
    double r = (double)rand() / RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;  // LFU_INIT_VAL = 5
    if (baseval < 0) baseval = 0;
    double p = 1.0 / (baseval * 10 + 1);  // 概率递减
    if (r < p) counter++;
    return counter;
}

监控与调优

基本写法:查看淘汰统计 INFO stats

# 查看键淘汰统计信息
INFO stats