目录

多级缓存架构设计与性能优化: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);
}

八、总结

多级缓存架构的关键要点:

  1. 分层设计:L1本地 + L2分布式 + L3数据库
  2. 更新策略:Cache-Aside为主,延迟双删为辅
  3. 问题防护:布隆过滤器防穿透,互斥锁防击穿,随机过期防雪崩
  4. 监控告警:命中率、延迟、异常率全面监控

通过多级缓存架构,我们实现了:

  • 响应时间缩短90%,用户体验显著提升
  • 数据库负载降低95%,为业务增长预留充足空间
  • 系统具备支撑高并发的架构能力,从500 TPS扩展至5000 TPS

这套架构设计不仅解决了当前性能瓶颈,更为业务持续发展提供了坚实的技术基础。

希望这篇文章对你有所帮助!


相关文章: