当商品详情接口的 QPS 从 500 涨到 8000,数据库开始频繁告警时,我们决定引入多级缓存。本文是这个改造项目的架构复盘。

为什么需要两级缓存

单纯 Redis 集群在万级 QPS 下响应已经足够快(P99 约 2ms),但网络往返 + 序列化开销仍然占据了接口耗时的大头。对于读多写少、允许秒级不一致的数据,进程内缓存能把命中路径缩短到微秒级。

  • L1 - Caffeine 本地缓存:容量受限、各节点独立、适合热点数据的毫秒级容忍场景。
  • L2 - Redis 集群:全局共享、容量大、作为本地缓存的回源层。
  • DB:只在两级都未命中时访问,并把回源结果异步写回两级缓存。

整体架构设计

本地缓存使用 Caffeine 的 LoadingCache,设置 maximumSize 与 refreshAfterWrite;Redis 侧按业务域划分 key 前缀,统一封装 CacheService 屏蔽两级细节。业务代码只依赖抽象接口,为后续替换实现留出空间。

java
1@Service2public class ProductCacheService {34    private final LoadingCache<Long, Product> localCache = Caffeine.newBuilder()5            .maximumSize(10_000)6            .refreshAfterWrite(Duration.ofSeconds(30))7            .expireAfterWrite(Duration.ofMinutes(5))8            .build(this::loadFromRedis);910    public Product getProduct(Long id) {11        return localCache.get(id);12    }1314    private Product loadFromRedis(Long id) {15        String key = "product:info:" + id;16        Product product = redisTemplate.opsForValue().get(key);17        if (product == null) {18            product = productMapper.selectById(id);  // 防击穿:分布式锁回源19            if (product != null) {20                redisTemplate.opsForValue().set(key, product,21                        Duration.ofMinutes(30).plus(RandomDelay.ofSeconds(0, 300)));22            }23        }24        return product;25    }26}

三大经典问题的应对

穿透:查不存在的数据

恶意请求不存在的商品 ID 会直接穿透到数据库。我们采用「布隆过滤器前置 + 空值短缓存」的组合:过滤器拦截绝大多数无效 ID,漏网的空结果缓存 60 秒并随机化过期时间。

击穿:热点 key 过期瞬间

爆款商品过期瞬间数千请求同时回源。解决方式是分布式锁互斥回源:拿到锁的线程查库写缓存,其他线程自旋 50ms 后重读缓存。配合 Caffeine 的 refreshAfterWrite,热点 key 在逻辑过期后由单线程异步刷新,读请求永远不打到数据库。

一致性:延迟双删实践

我们选择了「先更新数据库,再删缓存,并辅以延迟双删」的策略,避免复杂的双写事务。关键在第二次删除的延迟窗口设计——必须覆盖一次读请求「查库 + 写缓存」的最长耗时。

java
1@Transactional2public void updateProduct(Product product) {3    productMapper.updateById(product);45    // 第一次删除:清掉旧缓存6    cacheService.evict(product.getId());78    // 延迟双删:交给延迟队列,500ms 后再删一次9    // 500ms > 读请求「查库 + 回写缓存」的 P99 耗时10    delayQueue.submit(new CacheEvictTask(product.getId()), 500, TimeUnit.MILLISECONDS);11}
缓存一致性的本质是「在性能与正确性之间做工程取舍」。能容忍一秒不一致就别上分布式事务,把复杂度留给真正需要的场景。

上线效果与反思

改造后商品详情接口 P99 从 45ms 降到 3ms,数据库 QPS 下降 92%。反思最深的一点:缓存代码必须集中封装,散落在业务里的缓存读写是后来所有一致性事故的源头。