3、缓存策略深度优化:Redis集群、本地缓存与多级缓存架构

缓存这东西,说白了就是用空间换时间。但在大规模发证场景下,缓存用不好,数据库照样会被打垮。我见过太多团队,上来就怼一个 Redis 单节点,结果流量一上来,Redis 先扛不住,数据库跟着雪崩。

今天咱们聊聊怎么把缓存玩透。从 Redis 集群到本地缓存,再到多级缓存架构,一层层把数据库的压力卸掉。

3.1 Redis 集群:不只是分片

很多人觉得 Redis 集群就是数据分片,其实没那么简单。我当年接手一个发证项目,日均发证量 500 万,单节点 Redis 内存飙到 40GB,RDB 持久化一次卡死 3 秒。后来切到集群,才算喘过气来。

3.1.1 集群拓扑选型

Redis 集群主流有三种玩法:

方案 数据分片 高可用 一致性 适合场景
Redis Cluster 自动分片(16384 slots) 主从 + 哨兵 最终一致性 大规模读写分离
Codis Proxy 分片 主从 + ZK 强一致性(需配置) 兼容 Redis 协议
Twemproxy 一致性哈希 无 弱一致性 轻量代理

我个人习惯用 Redis Cluster。原因很简单——原生支持,运维成本低。Codis 虽然功能强,但多了一层 Proxy,延迟会增加 1-2ms。在发证这种高并发场景下,每一毫秒都很关键。

3.1.2 Slot 迁移与热点处理

集群部署后,最头疼的是热点问题。我记得有一次,某个证书模板的缓存 key 被大量并发访问,导致单个节点 CPU 冲到 95%。

解决办法有两个:

  • 散列 key:在 key 后面加随机后缀,把热点打散到不同 slot
  • 本地缓存兜底:热点数据在应用层再缓存一层,减少 Redis 压力

核心原则:Redis 集群不是万能的。当单个 key 的 QPS 超过 10 万时,必须考虑本地缓存介入。

3.2 本地缓存:被低估的利器

很多人觉得本地缓存不够「高级」,其实不然。在发证场景中,证书模板、配置参数这类变化极少的元数据,用本地缓存效果出奇的好。

3.2.1 Caffeine 实战

Java 生态里,Caffeine 是目前性能最好的本地缓存库。我做过压测,Caffeine 的读写吞吐是 Guava Cache 的 3 倍以上。

// 发证场景下的 Caffeine 配置
Cache<String, CertificateTemplate> templateCache = Caffeine.newBuilder()
    .maximumSize(10_000)          // 最多缓存 1 万个模板
    .expireAfterWrite(5, TimeUnit.MINUTES)  // 写入后 5 分钟过期
    .recordStats()                // 开启统计,方便监控
    .build();

// 使用方式
CertificateTemplate template = templateCache.get(templateId, key -> {
    // 缓存未命中时,从数据库加载
    return certificateDao.getTemplate(key);
});

这里有个坑:过期时间不能太长。我曾经把模板缓存设为 30 分钟,结果运营改了模板内容,用户拿到的还是旧证书。后来改成 5 分钟 + 主动失效机制,才算解决。

3.2.2 本地缓存 vs 分布式缓存

维度 本地缓存 Redis 集群
延迟 纳秒级 毫秒级(网络开销)
容量 受限于 JVM 堆 可水平扩展
一致性 节点间不一致 集群内一致
适用数据 只读 / 极少变更 读写频繁 / 共享数据

我的经验:本地缓存适合缓存「每个节点独立且变化慢」的数据。比如证书模板、字典表、配置项。而用户会话、发证流水这类共享数据,必须用 Redis。

3.3 多级缓存架构:层层拦截

单靠一种缓存,很难应对所有场景。真正的生产环境,需要构建多级缓存体系。

3.3.1 架构设计

下面这张图是我在项目中实际落地的多级缓存架构:

多级缓存架构流程图 客户端请求 L1: CDN / 浏览器缓存 L2: 本地缓存 (Caffeine) L3: Redis 集群 L4: 数据库 (MySQL) 请求路径 L1 → L2 → L3 → L4 命中即返回,不再往下走 每层延迟递增,容量递减

你看,请求进来后,先过 CDN,再查本地缓存,然后才到 Redis,最后才是数据库。每一层命中就直接返回,不再往下走。这样数据库几乎只处理缓存未命中的请求。

3.3.2 缓存穿透与击穿

多级缓存最怕什么?穿透和击穿。

  • 缓存穿透:查询一个不存在的数据,所有缓存都查不到,直接打到数据库
  • 缓存击穿:某个热点 key 过期,大量并发同时去数据库加载

我曾经踩过一个坑:用户查询一个已注销的证书 ID,每次请求都穿透到数据库,导致数据库连接池被打满。后来加了布隆过滤器,才把这个问题堵住。

避坑指南:

  • 穿透:用布隆过滤器拦截非法 key,或者缓存空值(设置短过期时间)
  • 击穿:用互斥锁(如 Redisson)控制并发加载,或者用「永不过期 + 异步刷新」策略

3.4 缓存一致性:终极难题

多级缓存最头疼的问题,就是一致性。本地缓存和 Redis 数据不一致怎么办?

3.4.1 最终一致性方案

在发证场景中,我推荐使用「Cache Aside Pattern + 消息队列」:

  1. 更新数据库时,先写数据库,再发一条 MQ 消息
  2. 消费者收到消息后,删除 Redis 中的对应缓存
  3. 本地缓存设置较短过期时间(如 5 分钟),到期自动失效

这样做的好处是:数据库和 Redis 最终一致,本地缓存最多有 5 分钟的不一致窗口。对于发证场景来说,这个窗口是可以接受的。

3.4.2 强一致性场景

如果业务要求强一致性(比如证书状态变更),那就不能用多级缓存了。我建议:

  • 直接查 Redis,不走本地缓存
  • Redis 和数据库之间用分布式事务(如 Seata)保证一致性
  • 或者干脆不缓存,直接查数据库(牺牲性能保一致性)

记住:没有银弹。多级缓存适合「读多写少、允许短暂不一致」的场景。发证业务中,证书模板、配置参数、字典表都符合这个特征。但证书状态、用户余额这类数据,请谨慎使用多级缓存。

3.5 监控与调优

缓存系统上线后,不是万事大吉。我习惯在每层缓存都埋点监控:

指标 本地缓存 Redis 集群
命中率 Caffeine.recordStats() INFO commandstats
延迟 微秒级(JVM 内) 毫秒级(slowlog)
内存使用 JVM 堆监控 INFO memory
热点 key 自定义计数器 hotkeys 参数

我记得有一次,监控发现 Redis 的命中率从 95% 掉到了 70%。排查后发现是本地缓存过期时间设置太短,大量请求穿透到 Redis。调整过期时间后,命中率又回到了 90% 以上。

调优口诀:本地缓存保热点,Redis 集群扛流量,数据库只做兜底。每层命中率低于 80% 就要排查原因。

好了,缓存策略这块就聊到这儿。记住,多级缓存不是堆砌技术,而是根据数据特征做分层。用对了,数据库压力能降 90% 以上。