缓存
缓存是提升系统性能的核心手段,通过将热点数据存储在更快的介质中,减少对数据库等慢速资源的访问。缓存分为本地缓存(进程内)和分布式缓存(独立服务),各有适用场景。
本地缓存:Guava Cache
Guava Cache 是 Google Guava 库提供的本地缓存实现,底层基于 ConcurrentHashMap 实现分段锁。
基本用法
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return "Hello: " + key;
}
});数据结构
核心数据结构类似 ConcurrentHashMap,使用分段锁提高并发性能。每个 Segment 继承 ReentrantLock,包含 ReferenceEntry 数组、引用队列和权重值统计。
put 操作流程
不同 Segment 可并发写入,同一 Segment 串行写入。
get 操作流程
public V get(Object key) {
int hash = hash(key);
return segmentFor(hash).get(key, hash);
}加载缓存时如果不存在,调用 CacheLoader.load(key) 自动加载。
淘汰策略
| 策略 | 说明 |
|---|---|
maximumSize | 基于容量淘汰 |
maximumWeight + weigher | 基于权重淘汰 |
expireAfterWrite | 写入后过期 |
expireAfterAccess | 访问后过期 |
refreshAfterWrite | 写入后刷新(配合 expire 使用) |
RemovalListener
监听缓存移除事件,可用于清理关联资源:
CacheBuilder.newBuilder()
.removalListener(RemovalListeners.asSynchronous(notification -> {
System.out.println("移除: " + notification.getKey());
}));与 ConcurrentHashMap 对比
| 特性 | Guava Cache | ConcurrentHashMap |
|---|---|---|
| 过期策略 | 支持 | 不支持 |
| 淘汰策略 | 支持 | 不支持 |
| 统计 | 支持 | 不支持 |
| 加载 | 支持(Loader) | 不支持 |
| 并发 | 分段锁 | CAS + 分段锁 |
Guava Cache 适合需要过期、淘汰、自动加载的缓存场景;纯 KV 高并发读写用 ConcurrentHashMap。
性能优化
- 合理设置并发度:默认 4,根据实际并发调整
- 避免大 value:增加内存压力和序列化成本
- 选择合适的淘汰策略:根据业务场景选择
- 使用弱引用:对于内存敏感场景
Redis 缓存问题
Redis 缓存在带来性能提升的同时,也引入了三个经典问题:缓存穿透、缓存击穿、缓存雪崩。
缓存穿透
大量请求查询不存在的数据,缓存中没有,数据库也没有,每次都直击数据库。
| 方案 | 做法 | 适用场景 |
|---|---|---|
| 缓存空值 | 查询不到的数据写入空值,设置过期时间 | 数据变化不频繁 |
| 布隆过滤器 | 请求先过布隆过滤器,不存在直接返回 | 数据量可控 |
缓存击穿
某个热点 key 非常热点,在失效瞬间大量请求击穿缓存直接请求数据库。与穿透不同,击穿是单个热点 key 的问题。
| 方案 | 做法 |
|---|---|
| 永不过期 | 热点数据不设置过期时间,通过异步更新 |
| 分布式锁 | 只允许一个请求重建缓存,其他等待 |
| 本地锁 | 使用本地互斥锁,单机场景 |
| 提前重建 | 定时任务在过期前主动更新缓存 |
缓存雪崩
缓存集中失效或缓存服务全盘宕机,导致大量请求直接打到数据库。与击穿不同,雪崩是批量 key 或全局 的问题。
| 阶段 | 措施 | 说明 |
|---|---|---|
| 事前 | Redis 高可用 | 主从+哨兵,Redis Cluster |
| 事中 | 限流降级 | 本地 ehcache + Hystrix 限流,数据库不死 |
| 事后 | 持久化恢复 | RDB/AOF,重启后快速恢复 |
三者对比
| 问题 | 原因 | 特点 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 绕过了缓存层 |
| 缓存击穿 | 热点 key 失效 | 单个 key 失效 |
| 缓存雪崩 | 大量 key 失效或服务宕机 | 批量 key 或全局 |
预防措施
| 措施 | 作用 |
|---|---|
| 热点数据永不过期 | 防止击穿 |
| 过期时间加随机值 | 防止集中失效 |
| Redis 高可用部署 | 防止宕机 |
| 限流降级兜底 | 保护数据库 |
| 布隆过滤器 | 防止穿透 |
根据业务场景选择方案:数据变化不频繁用缓存空值,数据量可控用布隆过滤器,高并发热点数据用分布式锁+永不过期。
常见问题处理
缓存与数据库一致性
- 采用 Cache Aside Pattern:读时先查缓存,miss 则查库并写缓存;写时先更新数据库,再删除缓存
- 延迟双删:写库后删缓存,延迟一段时间再删一次,防止并发读写导致脏数据
- 最终一致性方案:通过 Canal 监听 binlog 异步更新缓存
Guava Cache 内存溢出
- 合理设置
maximumSize,避免缓存无限增长 - 使用
weakKeys()或weakValues()允许 GC 回收 - 监控缓存命中率,过低说明缓存策略不合理