第1章:缓存选型决策 — Redis vs Memcached vs 本地缓存

缓存是系统性能的倍增器,但选错缓存的代价往往要到流量高峰期才会暴露。本章从数据结构、性能特征、场景适配三个维度拆解三种主流缓存方案的工程落地差异,并给出可执行的选型决策树。

1.1 核心差异总览

维度RedisMemcached本地缓存
数据结构String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、GEO、Stream仅 Key-Value(值最大 1MB)通常为 Map(HashMap / ConcurrentHashMap)
持久化RDB + AOF 支持无持久化依赖 JVM 进程
数据淘汰策略8 种(LRU、LFU、TTL、随机等)仅 LRU需要手动实现或使用 Guava/Caffeine
集群能力原生 Cluster / Sentinel客户端分片(一致性哈希)无
典型延迟0.1~1ms(本地网络)0.1~1ms(本地网络)< 0.01ms(堆内)
最大内存取决于物理内存(建议 ≤ 64GB/节点)取决于物理内存受限于堆大小
选型直觉: 需要复杂数据结构(排行榜、消息队列、计数器)→ 选 Redis;只需要纯 K-V + 极致内存效率 → 选 Memcached;延迟敏感且数据量可控 → 本地缓存兜底。

1.2 性能对比:吞吐与延迟分布

我们以 1KB 随机 value、并发 50 连接、100 万次请求为基准,给出典型压测结果(仅供参考,实际依赖硬件和网络):

缓存类型QPS(读)QPS(写)P99 延迟内存碎片率
Redis(单实例)~120,000~100,0001.2ms1.0~1.5
Memcached(单实例)~150,000~130,0000.8ms1.0~1.2
Caffeine 本地缓存~2,500,000~2,200,0000.02msN/A(堆内)
关键发现: Memcached 在纯 KV 场景下吞吐比 Redis 高 20%~30%,但缺少数据结构和持久化能力。本地缓存的延迟是远程缓存的两个数量级,但容量受限于 JVM 堆,且多节点间数据不一致。

1.3 数据结构选择与工程代价

Redis 之所以成为缓存首选,在于其丰富的数据结构能直接承载业务逻辑,减少多次网络交互:

  • String:常规缓存、计数器、分布式锁(SETNX)
  • Hash:对象属性缓存(用户信息、商品详情)
  • List:消息队列(LPUSH + BRPOP)、时间线
  • Sorted Set:排行榜、延时队列、限流滑窗
  • Set / HyperLogLog:去重统计、UV 统计
  • Stream:可靠消息队列(类似于 Kafka 的消费者组)
踩坑实录: 某团队用 Memcached 存储用户 session,为了支持批量读取,不得不将多个 session 序列化到一个 key 的 value 中,导致 1MB 限制遗漏,且更新单个字段需要反序列化整个大对象。改用 Redis Hash 后,单字段更新延迟从 5ms 降到 0.3ms。

1.4 本地缓存:不可被替代的“第一层缓冲”

即使部署了 Redis/Memcached,大部分 80% 的热点数据仍应使用本地缓存拦截,避免网络 IO 和序列化开销。常见方案:

本地缓存库淘汰算法统计能力适用场景
Guava CacheLRU(基于 Segment)命中率、加载耗时中小型项目、短 TTL
CaffeineW-TinyLFU(近最优)命中率、逐出统计高并发、低延迟、强一致性要求低
EhcacheLRU / LFU / FIFO支持持久化、集群Java EE、需要磁盘溢出
推荐组合: Caffeine 作为进程内 L1 缓存 + Redis 作为跨进程 L2 缓存,配合“先本地、后远程”的旁路写入模式,可在 2ms 内完成一次缓存查询(99% 命中本地)。

1.5 选型决策流程

缓存选型决策树 是否需要缓存? 需要复杂数据结构? Redis 需要持久化/集群? Redis 延迟 <0.05ms? 本地缓存(Caffeine) Memcached 说明:本地缓存优先用于 L1,Redis/Memcached 作为 L2。混合使用时应设置不同的 TTL,避免本地缓存穿透。 实际落地还需考虑运维复杂度、接入成本、团队技术栈。

1.6 工程配置实战

以 Spring Boot 项目集成 Caffeine + Redis 为例,配置片段如下:

# application.yml
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=5000,expireAfterWrite=60s
    redis:
      time-to-live: 600s
      cache-null-values: false
      use-key-prefix: true
      key-prefix: "myapp:"

Java 配置类:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(30, TimeUnit.SECONDS)
            .recordStats());
        return manager;
    }

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .disableCachingNullValues();
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
    }
}
最佳实践: 预热阶段使用本地缓存提高启动速度;稳定运行后让本地缓存 TTL 短于 Redis(如本地 30s、Redis 5min),以此实现“热数据由本地拦截,冷数据由 Redis 兜底”的分层效果。

1.7 常见选型错误与规避

  • 所有数据都放入远程缓存:导致网络成为瓶颈。应该先用本地缓存拦截热点。
  • Memcached 存储 Session:Session 需要失效过期和更新部分字段,Memcached 缺乏 Hash 结构,只能全量替换。
  • Redis 当作万能存储:滥用 Sorted Set 导致内存暴涨,或使用 Keys 命令阻塞实例。
  • 本地缓存不设淘汰策略:默认无限增长导致 OOM,必须配置 maximumSize 和 TTL。
  • 忽略序列化开销:将 Java 对象直接序列化为 JSON 存入 Redis,每次读取都需要反序列化,应改用 Protobuf 或 Kryo。
线上事故案例: 某电商团队将商品详情页数据全部存放在 Redis String 中(每个 key 约 50KB),并发 2000 时 Redis 带宽跑满,响应从 2ms 飙升到 200ms。解决方案:引入本地 Caffeine 缓存商品摘要信息,Redis 只存储完整详情,并将大对象拆分为多个 Hash 字段按需读取。

1.8 章末总结

缓存选型没有银弹。Redis 适合数据结构丰富、持久化要求高的场景;Memcached 在纯 KV 吞吐上有优势;本地缓存是降低延时的第一道防线。工程落地时,建议按照“本地缓存 L1 → Redis/Memcached L2 → 数据库 L3”的分层模型,根据数据访问特征动态调整 TTL 和容量。下一章将深入 Redis 的内存模型与淘汰策略实战,教你如何最大化利用有限内存。