4. 缓存层设计:Redis容器化部署、缓存策略(LRU/TTL)与缓存与搜索的读写流程

在搜索缓存一致性架构中,Redis 作为缓存层承担着“流量缓冲”与“数据加速”的核心角色。容器化部署使 Redis 实例的扩缩容、配置管理和故障恢复变得标准化,而 LRU 与 TTL 策略则决定了缓存命中率与数据新鲜度之间的平衡。本节将深入剖析 Redis 容器化部署的最佳实践、缓存淘汰策略的选择依据,以及缓存与搜索引擎之间的完整读写数据流。

4.1 Redis 容器化部署:从单机到哨兵集群

生产环境下的 Redis 容器化部署通常采用 StatefulSet 控制器,以保证稳定的网络标识(Pod 名称)和持久化存储。以下是一个典型的 Redis 哨兵模式部署清单(关键片段):

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-master
spec:
  serviceName: redis-master-svc
  replicas: 1
  selector:
    matchLabels:
      app: redis
      role: master
  template:
    metadata:
      labels:
        app: redis
        role: master
    spec:
      containers:
      - name: redis
        image: redis:7.2-alpine
        ports:
        - containerPort: 6379
        volumeMounts:
        - name: redis-data
          mountPath: /data
        resources:
          requests:
            memory: "2Gi"
            cpu: "500m"
          limits:
            memory: "4Gi"
            cpu: "1"
  volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  name: redis-master-svc
spec:
  ports:
  - port: 6379
    targetPort: 6379
  selector:
    app: redis
    role: master
部署要点: 哨兵模式下,至少需要 3 个 Sentinel 实例形成奇数节点,避免脑裂。每个 Redis 实例应配置 maxmemory-policy allkeys-lru 和 maxmemory 2gb,防止内存溢出导致 Pod 被 OOMKill。

4.2 缓存策略:LRU 与 TTL 的协同机制

Redis 提供多种内存淘汰策略,在搜索缓存场景中,LRU(Least Recently Used) 与 TTL(Time To Live) 是最常用的组合。LRU 负责在内存达到上限时淘汰最久未访问的键,TTL 则确保数据不会无限期驻留,避免“脏数据”长期存在。

策略 触发条件 淘汰对象 适用场景
allkeys-lru 内存达到 maxmemory 所有键中最近最少使用的 热点搜索词缓存,无固定过期需求
volatile-lru 内存达到 maxmemory 仅设置了 TTL 的键中最近最少使用的 混合缓存:部分永久键 + 部分过期键
allkeys-random 内存达到 maxmemory 随机淘汰任意键 缓存重要性均等,无热点倾斜
noeviction 内存达到 maxmemory 不淘汰,写入返回错误 禁止丢失数据的场景(如计数器)

TTL 的设置需要根据搜索数据的更新频率来定。对于商品搜索,建议 TTL 设置在 30 秒~5 分钟 之间;对于用户画像类缓存,可延长至 1 小时。过短的 TTL 会导致缓存穿透,过长的 TTL 则增加数据不一致风险。

常见陷阱: 不要对所有键设置相同的 TTL,否则大量键同时过期会导致“缓存雪崩”。建议在 TTL 基础上增加 ±10%~±20% 的随机偏移量,例如 SETEX key 300 + random(0, 60)。

4.3 缓存与搜索的读写流程

缓存层位于应用与搜索引擎(如 Elasticsearch)之间,采用 Cache-Aside 模式。读请求优先查询 Redis,命中则直接返回;未命中则回源到搜索引擎,并将结果写入缓存。写请求则采用“先更新搜索引擎,再删除缓存”的策略,以最小化不一致窗口。

缓存与搜索读写流程(Cache-Aside 模式) 应用服务 读流程 ① 查询缓存 (GET key) ② 缓存命中? ③ 是:直接返回数据 ④ 否:回源搜索引擎 ⑤ 写入缓存 (SETEX) ⑥ 返回数据给客户端 写流程 ① 更新搜索引擎 ② 删除缓存 (DEL) ③ 返回写入成功 先删缓存再写 ES 会导致短暂不一致,推荐“延迟双删”策略

4.4 延迟双删:解决写操作中的缓存一致性问题

在 Cache-Aside 写流程中,如果先删除缓存再更新搜索引擎,在删除缓存后、更新完成前,另一个读请求可能将旧数据重新写入缓存,导致缓存与搜索引擎长期不一致。延迟双删 策略通过两次删除来消除这个窗口:

  1. 第一次删除: 在更新搜索引擎之前,立即删除缓存中的对应键。
  2. 更新搜索引擎: 执行实际的索引更新操作。
  3. 延迟等待: 休眠一个短时间(通常 200~500ms,大于可能的并发读耗时)。
  4. 第二次删除: 再次删除缓存键,清除在更新期间可能被回填的旧数据。
// 延迟双删伪代码
public void updateSearchAndCache(String key, Object newData) {
    // 1. 第一次删除缓存
    redisTemplate.delete(key);

    // 2. 更新搜索引擎
    searchEngine.updateIndex(key, newData);

    // 3. 延迟等待(异步或线程休眠)
    Thread.sleep(500);

    // 4. 第二次删除缓存
    redisTemplate.delete(key);
}
生产建议: 延迟双删中的休眠操作会阻塞当前线程,建议使用 延迟队列(如 RabbitMQ 延迟消息)或 Redis 的 Keyspace Notification 异步执行第二次删除,避免影响写请求的吞吐量。

4.5 缓存穿透、击穿与雪崩的防护

在搜索缓存场景中,以下三种异常情况需要重点防范:

异常类型 现象 防护措施
缓存穿透 查询一个不存在的数据,每次都会穿透到搜索引擎 布隆过滤器(Bloom Filter)预判 key 是否存在;缓存空值(null)并设置短 TTL
缓存击穿 热点 key 在失效瞬间,大量并发请求同时回源 互斥锁(SETNX)控制回源线程;热点 key 设置永不过期 + 后台异步更新
缓存雪崩 大量 key 同时过期,导致搜索引擎负载飙升 TTL 增加随机偏移量;多级缓存(本地缓存 + Redis);限流降级
缓存异常防护架构 应用请求 布隆过滤器 Redis 缓存层 搜索引擎 (ES) 穿透防护:布隆过滤器拦截不存在 key 击穿防护:互斥锁 + 热点 key 永不过期 雪崩防护:TTL 随机偏移 + 多级缓存

4.6 容器化环境下的监控与故障排查

在 Kubernetes 中运行 Redis 时,需要关注以下关键指标:

  • 内存使用率: used_memory / maxmemory,超过 80% 应触发告警。
  • 缓存命中率: keyspace_hits / (keyspace_hits + keyspace_misses),低于 85% 需检查缓存策略。
  • 逐出键数量: evicted_keys,突增说明内存不足或 TTL 设置不合理。
  • 慢查询日志: 通过 SLOWLOG GET 100 分析耗时命令,常见于 KEYS * 或大 key 操作。
容器化排查技巧: 使用 kubectl exec -it redis-pod -- redis-cli INFO stats 实时查看缓存命中率;通过 kubectl top pod redis-pod 监控 CPU/内存资源使用;配置 Prometheus + Redis Exporter 实现长期趋势分析。