强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Memcached 完全指南 / 第 7 章:LRU 淘汰策略

第 7 章:LRU 淘汰策略

7.1 为什么需要淘汰策略?

Memcached 使用固定大小的内存池。当内存用尽且没有空闲 chunk 时,必须淘汰旧数据为新数据腾出空间。

内存用尽时的选择:
├── 策略 A:拒绝新写入 → 返回 SERVER_ERROR
├── 策略 B:淘汰最旧的数据 → LRU ✓(Memcached 默认)
└── 策略 C:随机淘汰 → 不可预测

7.2 经典 LRU(Single LRU)

在 Memcached 1.5 之前,每个 Slab Class 只维护一条 LRU 链表:

HEAD ←→ [Item A] ←→ [Item B] ←→ [Item C] ←→ [Item D] ←→ TAIL
         最近访问                          最久未访问
         (安全)                            (淘汰候选)

LRU 操作规则

操作LRU 链表动作
get 命中Item 移到链表头部(标记为最近使用)
set 新值新 Item 插入链表头部
delete从链表中移除 Item
内存不足从链表尾部开始扫描,淘汰过期或最久未使用的 Item

经典 LRU 的问题

场景:大量短 TTL 的临时数据 + 长期缓存的热数据

经典 LRU:
HEAD ← [tmp1,1s] ← [tmp2,1s] ← [hot1,3600s] ← [hot2,3600s] ← TAIL
                                      ↑
  问题:tmp1/tmp2 大量涌入,占据链表头部,
        导致 hot1/hot2 被推向尾部并被淘汰!

这就是 "cache pollution"(缓存污染)问题。

7.3 LRU Maintainer 模式(推荐)

Memcached 1.5+ 引入 lru_maintainer 模式,将单条 LRU 拆分为四条独立的 LRU 链表。

四级 LRU 结构

每个 Slab Class 维护四条 LRU 链表:

┌──────────────────────────────────────────────┐
│               HOT LRU                        │
│  最近频繁访问的 Item,通常是"热数据"         │
│  HEAD ← [A] ← [B] ← [C] ← TAIL             │
├──────────────────────────────────────────────┤
│               WARM LRU                       │
│  被访问过但不够热,从 HOT 降级下来            │
│  HEAD ← [D] ← [E] ← TAIL                    │
├──────────────────────────────────────────────┤
│               COLD LRU                       │
│  从未被访问或很久未访问,默认新 Item 进入此处  │
│  HEAD ← [F] ← [G] ← [H] ← [I] ← TAIL      │
├──────────────────────────────────────────────┤
│               TEMP LRU                       │
│  TTL 很短的临时 Item(默认 ≤ 61s + 有足够Item)│
│  HEAD ← [T1] ← [T2] ← TAIL                  │
│  独立淘汰,不影响其他 LRU                     │
└──────────────────────────────────────────────┘

Item 在 LRU 间流转

新 Item 进入:
├── TTL ≤ 61秒(且 Item 数量足够多)→ TEMP LRU
└── 其他情况 → COLD LRU

get 命中:
├── 在 COLD 中命中 → 提升到 WARM
├── 在 WARM 中命中 → 留在 WARM(头部)
├── 在 HOT 中命中 → 留在 HOT(头部)
└── 在 TEMP 中命中 → 留在 TEMP(头部)

降级(由 lru_maintainer 线程执行):
├── HOT 满 → 尾部 Item 降级到 WARM
├── WARM 满 → 尾部 Item 降级到 COLD
└── COLD 满 → 尾部 Item 被淘汰(eviction)

启用 lru_maintainer

# 启动时启用
memcached -o lru_maintainer

# 与 slab_automove 配合使用(推荐组合)
memcached -o lru_maintainer,slab_automove,slab_reassign

查看各级 LRU 状态

echo "stats items" | nc localhost 11211

# STAT items:1:number 523           ← Slab Class 1 的总 Item 数
# STAT items:1:number_hot 100       ← HOT LRU 中的 Item 数
# STAT items:1:number_warm 150      ← WARM LRU 中的 Item 数
# STAT items:1:number_cold 250      ← COLD LRU 中的 Item 数
# STAT items:1:number_temp 23       ← TEMP LRU 中的 Item 数
# STAT items:1:age 1234             ← COLD LRU 尾部 Item 的年龄(秒)
# STAT items:1:evicted 50           ← 被淘汰的 Item 数
# STAT items:1:evicted_nonzero 40   ← 非 0 TTL 被淘汰的 Item 数
# STAT items:1:evicted_time 300     ← 最近被淘汰 Item 的年龄
# STAT items:1:outofmemory 5        ← 内存分配失败次数
# STAT items:1:tailrepairs 10       ← LRU 尾部修复次数

7.4 TEMP LRU 详解

TEMP LRU 的选择条件

Item 进入 TEMP LRU 的条件(全部满足):
1. TTL > 0
2. TTL ≤ 临时阈值(默认 61 秒)
3. 当前 Slab Class 的 Item 总数 > 临时数量阈值
# 自定义 TEMP TTL 阈值
memcached -o lru_maintainer,temp_ttl=120

# 默认值: temp_ttl=61

TEMP LRU 的优势

场景:验证码缓存(TTL=60秒)+ 用户信息缓存(TTL=3600秒)

不使用 TEMP LRU:
  验证码大量涌入 → 占据 COLD LRU 前端 → 用户信息被淘汰
  结果:热数据丢失,大量回源

使用 TEMP LRU:
  验证码 → TEMP LRU(独立淘汰)→ 不影响 WARM/COLD
  用户信息 → COLD/WARM LRU → 安全

7.5 LRU Crawler(LRU 爬虫)

LRU Crawler 是一个后台线程,定期扫描 LRU 链表尾部,清理已过期的 Item。

工作原理

LRU Crawler 扫描流程:

1. 从某个 Slab Class 的 LRU 尾部开始
2. 检查每个 Item 的 TTL
   ├── 已过期 → 删除 Item,释放 chunk
   └── 未过期 → 跳过
3. 每次扫描一批 Item(默认 15ms 工作 + 10ms 休眠)
4. 扫描完所有 Class 后,等待一段时间再开始下一轮

启用 LRU Crawler

# 启用爬虫
memcached -o lru_crawler,lru_maintainer

# 自定义爬虫参数
memcached -o lru_crawler,lru_maintainer,lru_crawler_sleep=200,lru_crawler_tocrawl=0
参数默认值说明
lru_crawler_sleep100每批 Item 扫描后的休眠时间(ms)
lru_crawler_tocrawl0(自动)每次扫描的 Item 数量上限

手动触发 LRU Crawler

# 启动手动爬虫模式
memcached -o lru_crawler,lru_crawler_sleep=0

# 手动触发爬虫(通过 stats 命令)
echo "lru_crawler crawl 1,2,3" | nc localhost 11211
# OK

# 查看爬虫状态
echo "stats settings" | nc localhost 11211 | grep lru_crawler
# STAT lru_crawler true

7.6 过期机制详解

两种过期检查方式

方式一:惰性检查(Lazy Expiration)
  get key → 检查 TTL → 过期则删除并返回 NOT_FOUND
  优点:不需要额外线程
  缺点:未被访问的过期 Item 占用内存

方式二:主动检查(Active Expiration)
  LRU Crawler 后台扫描 → 发现过期则删除
  优点:及时回收内存
  缺点:需要额外 CPU 开销

Memcached 默认使用方式一,可选启用方式二(推荐)。

过期时间计算

# exptime = 0: 永不过期
set key1 0 0 5
value

# exptime ≤ 2592000 (30天): 相对时间(秒)
set key2 0 3600 5    # 3600秒后过期
value

# exptime > 2592000 (30天): Unix 时间戳
set key3 0 1735689600 5    # 在指定时间过期
value
import time

# 计算过期时间戳
expire_at = int(time.time()) + 3600  # 1小时后
mc.set("key", "value", time=expire_at)

7.7 缓存失效模式

Cache Aside Pattern(旁路缓存)

最常见的缓存模式,由应用层管理缓存:

def get_user(user_id):
    # 1. 先查缓存
    cache_key = f"user:{user_id}"
    data = mc.get(cache_key)
    if data:
        return json.loads(data)

    # 2. 缓存未命中,查数据库
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    if not user:
        # 缓存空值,防止缓存穿透
        mc.set(cache_key, json.dumps(None), time=60)
        return None

    # 3. 写入缓存
    mc.set(cache_key, json.dumps(user), time=3600)
    return user

def update_user(user_id, updates):
    # 1. 先更新数据库
    db.execute("UPDATE users SET ... WHERE id = %s", user_id, updates)

    # 2. 再删除缓存(不是更新!)
    mc.delete(f"user:{user_id}")

Read/Write Through Pattern

由缓存层统一管理读写:

class UserCache:
    def __init__(self, mc, db):
        self.mc = mc
        self.db = db

    def get(self, user_id):
        data = self.mc.get(f"user:{user_id}")
        if data:
            return json.loads(data)
        # 缓存层负责从 DB 加载
        user = self.db.query_user(user_id)
        self.mc.set(f"user:{user_id}", json.dumps(user), time=3600)
        return user

    def set(self, user_id, user):
        # 先写 DB
        self.db.update_user(user_id, user)
        # 再写缓存
        self.mc.set(f"user:{user_id}", json.dumps(user), time=3600)

Write Behind Pattern(异步写回)

import queue
import threading

write_queue = queue.Queue()

def async_writer():
    """后台线程:批量写入数据库"""
    batch = []
    while True:
        try:
            item = write_queue.get(timeout=1)
            batch.append(item)
            if len(batch) >= 100:
                db.batch_update(batch)
                batch = []
        except queue.Empty:
            if batch:
                db.batch_update(batch)
                batch = []

def update_user_async(user_id, user):
    mc.set(f"user:{user_id}", json.dumps(user), time=3600)
    write_queue.put({"id": user_id, "data": user})

7.8 缓存常见问题

缓存穿透(Cache Penetration)

问题:查询不存在的 Key,每次都回源数据库。

# 恶意请求不存在的用户 ID
GET user:999999999 → MISS → DB 查询 → 不存在
GET user:999999999 → MISS → DB 查询 → 不存在
GET user:999999999 → MISS → DB 查询 → 不存在
# 数据库被打爆

解决方案:缓存空值 + 布隆过滤器

def get_user_safe(user_id):
    cache_key = f"user:{user_id}"
    data = mc.get(cache_key)

    if data is not None:
        if data == "NULL":  # 空值标记
            return None
        return json.loads(data)

    user = db.query_user(user_id)
    if user:
        mc.set(cache_key, json.dumps(user), time=3600)
    else:
        # 缓存空值,短 TTL
        mc.set(cache_key, "NULL", time=60)

    return user

缓存击穿(Cache Breakdown)

问题:热点 Key 过期瞬间,大量并发请求同时回源。

import threading

_locks = {}

def get_hot_data(key, ttl=3600):
    data = mc.get(key)
    if data:
        return json.loads(data)

    # 分布式锁:只有一个请求回源
    lock_key = f"lock:{key}"
    if mc.add(lock_key, "1", time=10):  # 获取锁
        try:
            data = load_from_db(key)
            mc.set(key, json.dumps(data), time=ttl)
            return data
        finally:
            mc.delete(lock_key)
    else:
        # 等待其他请求加载完毕
        import time
        for _ in range(100):
            time.sleep(0.05)
            data = mc.get(key)
            if data:
                return json.loads(data)
        # 超时则直接查询
        return load_from_db(key)

缓存雪崩(Cache Avalanche)

问题:大量 Key 同时过期,瞬间大量请求涌入数据库。

import random

def set_with_jitter(key, data, base_ttl=3600):
    """设置缓存时添加随机抖动,防止同时过期"""
    jitter = random.randint(-300, 300)  # ±5 分钟
    ttl = max(60, base_ttl + jitter)
    mc.set(key, json.dumps(data), time=ttl)

7.9 LRU 调优参数汇总

# 推荐的生产配置
memcached \
    -o lru_maintainer \       # 启用四级 LRU
    -o slab_automove \        # 自动 Slab 迁移
    -o slab_reassign \        # 允许 Slab 重分配
    -o lru_crawler \          # 启用 LRU 爬虫
    -o temp_ttl=61            # TEMP LRU TTL 阈值

扩展阅读

小结

要点内容
推荐配置lru_maintainer 启用四级 LRU(HOT/WARM/COLD/TEMP)
TEMP LRUTTL 很短的 Item 独立淘汰,避免缓存污染
LRU Crawler后台清理过期 Item,推荐启用
核心原则新 Item 进 COLD,访问后提升到 WARM/HOT
缓存失效优先使用 Cache Aside + 缓存空值 + 随机 TTL 抖动