1. 缓存选型决策
缓存是系统性能的倍增器,但选错缓存的代价往往要到流量高峰期才会暴露。本章从数据结构、性能特征、场景适配三个维度拆解三种主流缓存方案的工程落地差异,并给出可执行的选型决策树。
1.1 核心差异总览
| 维度 | Redis | Memcached | 本地缓存 |
|---|---|---|---|
| 数据结构 | 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,000 | 1.2ms | 1.0~1.5 |
| Memcached(单实例) | ~150,000 | ~130,000 | 0.8ms | 1.0~1.2 |
| Caffeine 本地缓存 | ~2,500,000 | ~2,200,000 | 0.02ms | N/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 Cache | LRU(基于 Segment) | 命中率、加载耗时 | 中小型项目、短 TTL |
| Caffeine | W-TinyLFU(近最优) | 命中率、逐出统计 | 高并发、低延迟、强一致性要求低 |
| Ehcache | LRU / LFU / FIFO | 支持持久化、集群 | Java EE、需要磁盘溢出 |
推荐组合: Caffeine 作为进程内 L1 缓存 + Redis 作为跨进程 L2 缓存,配合“先本地、后远程”的旁路写入模式,可在 2ms 内完成一次缓存查询(99% 命中本地)。
1.5 选型决策流程
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 的内存模型与淘汰策略实战,教你如何最大化利用有限内存。