多级缓存架构设计与性能优化:Redis+Caffeine实战
目录
前言
在短链推广平台项目中,我们面临了一个典型的读多写少场景:短链跳转查询占业务流量的绝大部分,高峰期数据库连接池频繁耗尽,查询响应时间逐渐劣化。
通过设计多级缓存架构(Caffeine+Redis),我们提前为业务增长预留了充足的性能余量,平均响应时间从200ms降至20ms,系统具备了支撑高并发流量的架构能力。本文将详细分享我们的实战经验。
多级缓存架构总览
缓存穿透防护流程
一、为什么需要多级缓存?
1.1 单层缓存的局限性
纯本地缓存(Caffeine):
- ✅ 访问速度极快(微秒级)
- ❌ 服务间数据不一致
- ❌ 重启后数据丢失
- ❌ 单机容量有限
纯分布式缓存(Redis):
- ✅ 数据一致性好
- ✅ 容量可扩展
- ❌ 网络开销(毫秒级)
- ❌ 序列化/反序列化开销
1.2 多级缓存的优势
L1: Caffeine本地缓存(微秒级)
↓ miss
L2: Redis分布式缓存(毫秒级)
↓ miss
L3: Database(十毫秒级)优势:
- ✅ 95%请求命中L1缓存,性能接近本地
- ✅ L2保证多机数据一致性
- ✅ L3兜底,保证数据可靠性
二、架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────────┐
│ 应用层 │
│ ┌────────────────────────────────────────────────────┐ │
│ │ MultiLevelCacheManager │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Caffeine │ │ Redis │ │ DB │ │ │
│ │ │ (L1) │ │ (L2) │ │ (L3) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘2.2 缓存更新策略
Cache-Aside(旁路缓存)模式:
读操作:
1. 先读L1缓存
2. L1 miss -> 读L2缓存
3. L2 miss -> 读数据库
4. 回填L1、L2缓存
写操作:
1. 更新数据库
2. 删除L1、L2缓存为什么不是先删缓存再更新数据库?
- 避免并发场景下的数据不一致
- 延迟双删策略作为补充
三、核心实现
3.1 多级缓存管理器
@Component
public class MultiLevelCacheManager {
@Autowired
private Cache<String, Object> localCache;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String REDIS_KEY_PREFIX = "cache:";
/**
* 获取缓存
*/
public <T> T get(String key, Class<T> type) {
// 1. 尝试从L1获取
Object value = localCache.getIfPresent(key);
if (value != null) {
log.debug("L1缓存命中,key={}", key);
return (T) value;
}
// 2. 尝试从L2获取
String redisKey = REDIS_KEY_PREFIX + key;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (redisValue != null) {
log.debug("L2缓存命中,key={}", key);
T result = deserialize(redisValue, type);
// 回填L1
localCache.put(key, result);
return result;
}
return null;
}
/**
* 获取缓存(带Loader)
*/
public <T> T get(String key, Class<T> type, CacheLoader<String, T> loader) {
T value = get(key, type);
if (value != null) {
return value;
}
// 加锁防止缓存击穿
synchronized (key.intern()) {
// 双重检查
value = get(key, type);
if (value != null) {
return value;
}
// 加载数据
try {
value = loader.load(key);
if (value != null) {
put(key, value);
}
} catch (Exception e) {
log.error("加载缓存失败,key={}", key, e);
}
}
return value;
}
/**
* 写入缓存
*/
public void put(String key, Object value) {
put(key, value, Duration.ofMinutes(30), Duration.ofHours(2));
}
public void put(String key, Object value, Duration localExpire, Duration redisExpire) {
// 写入L1
localCache.put(key, value);
// 写入L2
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.opsForValue().set(redisKey, serialize(value), redisExpire);
}
/**
* 删除缓存
*/
public void evict(String key) {
// 删除L1
localCache.invalidate(key);
// 删除L2
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.delete(redisKey);
// 发送缓存失效消息(集群环境下)
publishEvictEvent(key);
}
/**
* 清空所有缓存
*/
public void clear() {
localCache.invalidateAll();
// 使用Lua脚本批量删除(避免大Key)
String luaScript = "redis.call('del', unpack(redis.call('keys', ARGV[1])))";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Collections.emptyList(), REDIS_KEY_PREFIX + "*");
}
}3.2 Caffeine配置
@Configuration
public class CaffeineConfig {
@Bean
public Cache<String, Object> localCache() {
return Caffeine.newBuilder()
// 最大容量
.maximumSize(10000)
// 写入后30分钟过期
.expireAfterWrite(30, TimeUnit.MINUTES)
// 访问后10分钟过期
.expireAfterAccess(10, TimeUnit.MINUTES)
// 启用统计
.recordStats()
// 移除监听器
.removalListener((key, value, cause) -> {
log.debug("L1缓存移除,key={},原因={}", key, cause);
})
.build();
}
}3.3 注解式缓存(类似Spring Cache)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiCacheable {
String value(); // 缓存名称
String key() default ""; // SpEL表达式
long localExpire() default 30; // L1过期时间(分钟)
long redisExpire() default 120; // L2过期时间(分钟)
}
@Aspect
@Component
public class MultiCacheAspect {
@Autowired
private MultiLevelCacheManager cacheManager;
@Around("@annotation(multiCacheable)")
public Object around(ProceedingJoinPoint point, MultiCacheable multiCacheable) throws Throwable {
// 生成缓存Key
String cacheKey = generateKey(point, multiCacheable);
// 尝试获取缓存
Object cached = cacheManager.get(cacheKey, Object.class);
if (cached != null) {
return cached;
}
// 执行方法
Object result = point.proceed();
// 写入缓存
if (result != null) {
cacheManager.put(cacheKey, result,
Duration.ofMinutes(multiCacheable.localExpire()),
Duration.ofMinutes(multiCacheable.redisExpire()));
}
return result;
}
private String generateKey(ProceedingJoinPoint point, MultiCacheable multiCacheable) {
// 实现SpEL解析...
return multiCacheable.value() + ":" + point.getArgs()[0];
}
}使用示例:
@Service
public class ShortLinkService {
@MultiCacheable(value = "shortlink", key = "#shortCode",
localExpire = 10, redisExpire = 60)
public ShortLink getByShortCode(String shortCode) {
return shortLinkMapper.selectByShortCode(shortCode);
}
}四、缓存问题解决方案
4.1 缓存穿透
问题: 查询不存在的数据,每次都打到数据库
解决方案: 布隆过滤器 + 缓存空值
@Component
public class CachePenetrationGuard {
@Autowired
private BloomFilter<String> bloomFilter;
@Autowired
private MultiLevelCacheManager cacheManager;
/**
* 安全获取(防穿透)
*/
public <T> T getWithGuard(String key, Class<T> type, CacheLoader<String, T> loader) {
// 1. 布隆过滤器检查
if (!bloomFilter.mightContain(key)) {
log.debug("布隆过滤器判定key不存在,key={}", key);
return null;
}
// 2. 尝试获取缓存
T value = cacheManager.get(key, type);
if (value != null) {
return value;
}
// 3. 检查是否是空值缓存
String nullKey = key + ":null";
if (cacheManager.get(nullKey, String.class) != null) {
return null;
}
// 4. 加载数据
value = loader.load(key);
if (value != null) {
cacheManager.put(key, value);
} else {
// 缓存空值(防穿透)
cacheManager.put(nullKey, "null", Duration.ofMinutes(5), Duration.ofMinutes(10));
}
return value;
}
}4.2 缓存击穿
问题: 热点Key过期瞬间,大量请求打到数据库
解决方案: 互斥锁 + 逻辑过期
@Component
public class CacheBreakdownGuard {
@Autowired
private RedissonClient redissonClient;
@Autowired
private MultiLevelCacheManager cacheManager;
/**
* 防击穿获取(互斥锁方案)
*/
public <T> T getWithLock(String key, Class<T> type, CacheLoader<String, T> loader) {
// 1. 尝试获取缓存
T value = cacheManager.get(key, type);
if (value != null) {
return value;
}
// 2. 获取分布式锁
RLock lock = redissonClient.getLock("lock:cache:" + key);
try {
// 尝试获取锁(等待100ms,持有10s)
boolean locked = lock.tryLock(100, 10000, TimeUnit.MILLISECONDS);
if (!locked) {
// 没获取到锁,直接返回(或者重试)
return cacheManager.get(key, type);
}
// 3. 双重检查
value = cacheManager.get(key, type);
if (value != null) {
return value;
}
// 4. 加载数据
value = loader.load(key);
if (value != null) {
cacheManager.put(key, value);
}
return value;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 防击穿获取(逻辑过期方案)
*/
public <T> T getWithLogicalExpire(String key, Class<T> type,
CacheLoader<String, T> loader, Duration expire) {
// 1. 获取缓存(永不过期)
String redisKey = "cache:logical:" + key;
String json = redisTemplate.opsForValue().get(redisKey);
if (json == null) {
// 缓存不存在,加载数据
T value = loader.load(key);
saveWithLogicalExpire(redisKey, value, expire);
return value;
}
// 2. 解析数据和过期时间
LogicalData<T> data = JSON.parseObject(json, new TypeReference<>() {});
if (data.getExpireTime().isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return data.getData();
}
// 3. 已过期,获取锁重建缓存
RLock lock = redissonClient.getLock("lock:cache:rebuild:" + key);
boolean locked = lock.tryLock();
if (locked) {
// 开启异步线程重建缓存
CompletableFuture.runAsync(() -> {
try {
T value = loader.load(key);
saveWithLogicalExpire(redisKey, value, expire);
} finally {
lock.unlock();
}
});
}
// 4. 返回过期数据(保证可用性)
return data.getData();
}
}
@Data
public class LogicalData<T> {
private T data;
private LocalDateTime expireTime;
}4.3 缓存雪崩
问题: 大量Key同时过期,数据库压力激增
解决方案: 随机过期时间 + 熔断降级
@Component
public class CacheAvalancheGuard {
@Autowired
private MultiLevelCacheManager cacheManager;
/**
* 随机过期时间(防止同时过期)
*/
public void putWithRandomExpire(String key, Object value,
Duration baseExpire) {
// 添加随机偏移(±10%)
long baseSeconds = baseExpire.getSeconds();
long randomSeconds = (long) (baseSeconds * 0.1 * (Math.random() - 0.5));
Duration finalExpire = Duration.ofSeconds(baseSeconds + randomSeconds);
cacheManager.put(key, value, finalExpire.dividedBy(4), finalExpire);
}
/**
* 多级降级获取
*/
@CircuitBreaker(name = "cacheService", fallbackMethod = "getFromLocalOrDefault")
public <T> T getWithDegrade(String key, Class<T> type, CacheLoader<String, T> loader) {
try {
return cacheManager.get(key, type, loader);
} catch (Exception e) {
log.error("获取缓存失败,降级处理", e);
throw e;
}
}
/**
* 降级方法
*/
public <T> T getFromLocalOrDefault(String key, Class<T> type,
CacheLoader<String, T> loader, Exception ex) {
// 1. 尝试从本地缓存获取
T value = cacheManager.getFromLocal(key, type);
if (value != null) {
return value;
}
// 2. 返回默认值
return getDefaultValue(type);
}
}五、实战案例:短链平台优化
5.1 业务场景
核心查询: 短链码 -> 原始链接
@Service
public class ShortLinkRedirectService {
@Autowired
private MultiLevelCacheManager cacheManager;
@Autowired
private ShortLinkMapper shortLinkMapper;
/**
* 短链跳转(优化前:200ms,优化后:20ms)
*/
public String redirect(String shortCode) {
return cacheManager.get("shortlink:" + shortCode, String.class,
key -> {
ShortLink link = shortLinkMapper.selectByShortCode(shortCode);
return link != null ? link.getOriginalUrl() : null;
});
}
}5.2 性能数据
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 平均响应时间 | 200ms | 20ms | -90% |
| P99延迟 | 800ms | 50ms | -94% |
| 数据库负载 | 高(频繁查询) | 低(缓存命中) | -95% |
| 缓存命中率 | 0% | 98% | +98% |
| 系统吞吐量 | 500 TPS | 5000 TPS | +900% |
5.3 监控大盘
@Component
public class CacheMetrics {
@Autowired
private MeterRegistry meterRegistry;
@Scheduled(fixedRate = 60000) // 每分钟记录
public void recordMetrics() {
CacheStats stats = caffeineCache.stats();
// L1命中率
meterRegistry.gauge("cache.l1.hit.rate", stats.hitRate());
meterRegistry.gauge("cache.l1.load.count", stats.loadCount());
meterRegistry.gauge("cache.l1.eviction.count", stats.evictionCount());
// L2命中率(通过Redis INFO命令获取)
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("stats");
long keyspaceHits = Long.parseLong(info.getProperty("keyspace_hits"));
long keyspaceMisses = Long.parseLong(info.getProperty("keyspace_misses"));
double hitRate = (double) keyspaceHits / (keyspaceHits + keyspaceMisses);
meterRegistry.gauge("cache.l2.hit.rate", hitRate);
}
}六、架构决策总结
6.1 为什么不用Spring Cache?
Spring Cache虽然简单,但:
- ❌ 只支持单层缓存
- ❌ 不支持多级缓存自动管理
- ❌ 缓存问题解决方案不完善
我们的方案:
- ✅ 封装多级缓存管理器
- ✅ 提供完整的防穿透/击穿/雪崩方案
- ✅ 支持注解式和编程式两种使用方式
6.2 缓存一致性策略选择
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Cache-Aside | 简单直观 | 可能短暂不一致 | 读多写少(推荐) |
| Read-Through | 应用代码简洁 | 缓存层复杂 | 简单应用 |
| Write-Through | 强一致性 | 写性能差 | 强一致性场景 |
| Write-Behind | 写性能高 | 可能丢数据 | 非关键数据 |
七、最佳实践
7.1 缓存Key设计
// 好的Key设计
cacheKey = "module:business:id"
// 示例:shortlink:redirect:abc123
// user:profile:10086
// order:detail:20240207:12345
// 避免大Key
// ❌ 缓存整个列表
cacheKey = "article:list" // value可能几MB
// ✅ 分页缓存
cacheKey = "article:list:page:1:size:20"7.2 缓存过期策略
// 热点数据:短过期时间 + 访问续期
cacheManager.put(key, value, Duration.ofMinutes(10), Duration.ofHours(1));
// 冷数据:长过期时间
cacheManager.put(key, value, Duration.ofHours(1), Duration.ofDays(1));
// 配置数据:永不过期,手动更新
cacheManager.put(key, value, Duration.ofDays(365), Duration.ofDays(365));7.3 大Value处理
// Value > 10KB,考虑压缩
public void putCompressed(String key, Object value) {
byte[] bytes = serialize(value);
if (bytes.length > 10240) { // 10KB
bytes = compress(bytes);
}
redisTemplate.opsForValue().set(key, bytes);
}八、总结
多级缓存架构的关键要点:
- 分层设计:L1本地 + L2分布式 + L3数据库
- 更新策略:Cache-Aside为主,延迟双删为辅
- 问题防护:布隆过滤器防穿透,互斥锁防击穿,随机过期防雪崩
- 监控告警:命中率、延迟、异常率全面监控
通过多级缓存架构,我们实现了:
- 响应时间缩短90%,用户体验显著提升
- 数据库负载降低95%,为业务增长预留充足空间
- 系统具备支撑高并发的架构能力,从500 TPS扩展至5000 TPS
这套架构设计不仅解决了当前性能瓶颈,更为业务持续发展提供了坚实的技术基础。
希望这篇文章对你有所帮助!
相关文章: