缓存雪崩终极防御:Caffeine + Redis 多级缓存

针对缓存雪崩的终极防御,引入 Caffeine(本地进程缓存) + Redis(分布式缓存) 组成多级缓存是目前互联网大厂最主流的落地方案。

当 Redis 不幸宕机,或者发生大面积 Key 集中过期时,请求会先在第一级缓存(Caffeine,在 JVM 内存中)被拦截并直接返回,根本不需要走到 Redis 层,更不会冲垮底层的 MySQL,从而实现完美的高可用兜底。

要深入后端开发和高并发架构,Caffeine 是一个你绝对绕不开的组件。在 Java 领域,它被称为**"缓存之王"**,也是 Spring Boot 官方默认支持和强烈推荐的本地缓存实现(用来全面替代老旧的 Guava Cache 和 Ehcache)。

Caffine 的三大"驱逐策略"(内存满了怎么办?)

因为 JVM 内存有限,Caffeine 不可能无限存储。当缓存达到设定的上限时,它必须把旧的数据踢出去。Caffeine 提供了三种被动淘汰机制:

  1. 基于大小
    • 设置最大容纳多少个 Key(maximumSize)。超过这个量,自动淘汰。
  2. 基于时间
    • expireAfterWrite:从写入开始算起,过一段时间过期(最常用)。
    • expireAfterAccess:从最后一次访问开始算起,过一段时间过期(适合留住热点数据)。
  3. 基于引用
    • 利用 Java 的弱引用(WeakReference)或软引用(SoftReference),在 JVM 内存告急、发生 GC 时,自动配合回收空间(较少主动配置)。
      在 Spring Boot 生态中,最优雅、最常见的整合方案是重写 Spring Cache 的 CacheManager。下面是全套工业级可运行的代码实现:

1. 核心设计架构

  • 一级缓存(L1): Caffeine(本地内存,速度快到飞起,但各节点不共享,容量有限)。
  • 二级缓存(L2): Redis(分布式缓存,容量大,多节点共享,但有网络 I/O 损耗)。

2. 引入依赖

pom.xml 中引入 Spring Cache、Redis 和 Caffeine 的相关 Starter:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

3. 核心:自定义多级缓存实现

我们需要自定义一个 Cache 类,让它在执行 get 时,先查 Caffeine,没有再查 Redis,并回写到 Caffeine。

3.1 编写自定义多级缓存类 DoubleCache

java 复制代码
import com.github.ben-manes.caffeine.cache.Cache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
public class DoubleCache extends AbstractValueAdaptingCache {
    private final String name;
    private final Cache<Object, Object> caffeineCache;
    private final RedisTemplate<Object, Object> redisTemplate;
    private final long redisExpiry; // 二级缓存过期时间(秒)
    public DoubleCache(String name, Cache<Object, Object> caffeineCache,
                       RedisTemplate<Object, Object> redisTemplate, long redisExpiry) {
        super(true); // allowInvocationsWithNullComponents
        this.name = name;
        this.caffeineCache = caffeineCache;
        this.redisTemplate = redisTemplate;
        this.redisExpiry = redisExpiry;
    }
    @Override
    public String getName() {
        return this.name;
    }
    @Override
    public Object getNativeCache() {
        return this;
    }
    // 多级查找的核心
    // 当 Spring 需要拿缓存时,首先会暗中调用这个方法:
    @Override
    protected Object lookup(Object key) {
        String realKey = this.name + ":" + key;
        // 1. 先查一级缓存
        Object value = caffeineCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        // 2. 一级缓存没中,查二级缓存
        value = redisTemplate.opsForValue().get(realKey);
        if (value != null) {
            // 3. 二级缓存中了,回写到一级缓存,方便下次直接用
            caffeineCache.put(key, value);
        }
        return value;
    }
	// 如果 lookup 找了一圈,发现本地没有,Redis 也没有(人称"双漏"),
	// Spring 就会调用这个 get 方法:
    @SuppressWarnings("unchecked")
    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        // Double-Check(双重检查):拿到锁之后,不急着查库,
        // 先再次调用 lookup(key) 看看别人是不是已经查完并写好缓存了。
        Object value = lookup(key);
        if (value != null) {
            return (T) fromStoreValue(value);
        }
        // 4. 二级缓存也没中(双漏),触发锁,去查数据库(Spring Cache 自带同步机制)
        synchronized (key) {
            value = lookup(key);
            if (value != null) return (T) fromStoreValue(value);
            try {
                T loadValue = valueLoader.call();
                put(key, loadValue); // 写入多级缓存
                return loadValue;
            } catch (Exception e) {
                throw new ValueRetrievalException(key, valueLoader, e);
            }
        }
    }
    //双写与"防雪崩"噪点
    @Override
    public void put(Object key, Object value) {
        if (value == null) return;
        // 同时写入一级和二级缓存
        caffeineCache.put(key, value);
        // 防雪崩核心:给 Redis 的过期时间加一个随机数(比如 1~5分钟随机),防止集体撞车过期
        long randomExpiry = redisExpiry + (long) (Math.random() * 300);
        redisTemplate.opsForValue().set(this.name + ":" + key, toStoreValue(value), randomExpiry, TimeUnit.SECONDS);
    }
    //双删(数据一致性)
    @Override
    public void evict(Object key) {
        // 清除缓存
        caffeineCache.invalidate(key);
        redisTemplate.delete(this.name + ":" + key);
    }
    @Override
    public void clear() {
        caffeineCache.invalidateAll();
    }
}

3.2 配置多级缓存管理器 CacheManager

将自定义的缓存注入到 Spring 容器中,让 @Cacheable 注解可以识别它。

java 复制代码
import com.github.ben-manes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching // 开启 Spring Cache 缓存注解支持
public class DoubleCacheConfig {
    @Bean
    public CacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<DoubleCache> caches = new ArrayList<>();
        // 定义一个名为 "product" 的缓存空间
        caches.add(new DoubleCache(
                "product",
                Caffeine.newBuilder()
                        .expireAfterWrite(10, TimeUnit.MINUTES) // 一级缓存本地生存 10 分钟
                        .maximumSize(1000)                      // 本地最多存 1000 条
                        .build(),
                redisTemplate,
                3600 // 二级缓存基准失效时间 1 小时
        ));
        cacheManager.setCaches(caches);
        return cacheManager;
    }
}

4. 业务层无侵入使用(极度丝滑)

配置完多级缓存后,你在 Service 层甚至不需要写一行关于 Redis 或 Caffeine 的 API,直接用 Spring 内置的 @Cacheable 注解即可:

java 复制代码
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
    /**
     * 获取商品详情
     * 当该方法被调用时:
     * 1. 自动去 Caffeine 查有无缓存,有则直接返回。
     * 2. Caffeine 没有,自动去 Redis 查,有则回写 Caffeine 并返回。
     * 3. Redis 也没有(或者 Redis 挂了),才会执行下面方法里的代码(查数据库 MySQL)。
     * 4. 查出结果后,自动写入 Caffeine 和 Redis。
     */
    @Cacheable(value = "product", key = "#productId", sync = true)
    public String getProductDetail(Long productId) {
        System.out.println("====== 缓存未命中,开始极其沉重地查询数据库 MySQL ======");
        return "Product_Detail_Of_" + productId;
    }
    /**
     * 当商品被修改或下架时,清除多级缓存
     */
    @CacheEvict(value = "product", key = "#productId")
    public void updateProduct(Long productId) {
        System.out.println("====== 数据库商品已更新,正在同步清除多级缓存 ======");
    }
}

5. 面试官进阶连环追问:多级缓存的分布式一致性问题

如果你在面试中聊到了这里,面试官一定会丢出一个高级问题:

"你把数据缓存在本地服务器的 Caffeine 内存里,如果后台管理员修改了商品,你用 @CacheEvict 清除了当前 A 节点的内存,但是分布式部署的 B 节点、C 节点的 JVM 内存里还是旧数据,这不就导致数据不一致了吗? 怎么解决?"
大厂正宗解法:Redis Pub/Sub(发布订阅机制)

当某一台机器触发了 evict(清除缓存)时,我们不仅要删掉本地内存和 Redis,还要通过 Redis 的 Pub/Sub 功能发布一条广播消息(比如频道叫 cache:clear:channel,内容是:{"cacheName":"product", "key": 1001})。

其他分布式部署的 Spring Boot 节点全部订阅这个频道,一旦收到广播消息,各自的本地服务立刻执行 caffeineCache.invalidate(key)。这样就能瞬间做到全集群本地缓存的同步清理。

利用 Redis 的 Pub/Sub(发布/订阅) 来同步清理各节点的 Caffeine 缓存,在 Spring Boot 中的核心落地分为三步:

  • 定义消息体(DTO):用来传递要清理的缓存名字和 Key。

  • 改造 DoubleCache 的 evict 方法:在清除本地和 Redis 后,发送广播。

  • 编写 Redis 订阅监听器(Listener):让所有节点都监听这个频道,收到消息就擦除本地 Caffeine。

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CacheMessage implements Serializable {
   private String cacheName; // 缓存名称,如 "product"
   private Object key;       // 缓存的 Key,如 1001
}
java 复制代码
// 注意:这里只贴出修改后的 evict 方法,其他 lookup、get 保持不变

@Override
public void evict(Object key) {
    String realKey = this.name + ":" + key;
    
    // 1. 清除当前机器本地的一级缓存
    caffeineCache.invalidate(key);
    
    // 2. 清除共享的二级缓存 Redis
    redisTemplate.delete(realKey);
    
    // 3. 【核心增强】:发布一条订阅消息,通知其他节点清理本地缓存
    // 频道名固定为:cache:clear:channel
    CacheMessage message = new CacheMessage(this.name, key);
    redisTemplate.convertAndSend("cache:clear:channel", message);
}
java 复制代码
@Configuration
public class CacheSyncConfig {

    /**
     * 配置 Redis 消息监听容器
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        
        // 让监听器订阅指定频道 "cache:clear:channel"
        container.addMessageListener(listenerAdapter, new ChannelTopic("cache:clear:channel"));
        return container;
    }

    /**
     * 配置消息监听适配器:指定收到消息后,由哪个类的哪个方法来处理
     */
    @Bean
    public MessageListenerAdapter listenerAdapter(CacheMessageReceiver receiver) {
        // 让 receiver 对象的 "receiveMessage" 方法来接收并处理消息
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
}
相关推荐
新时代农民工~1 小时前
PostgreSQL 主从故障恢复自动化:实战脚本与最佳实践
数据库·postgresql·自动化
woshilys2 小时前
sql server 查询外键
数据库·sql·sqlserver
瀚高PG实验室3 小时前
开发管理工具打不开No way to find ori gi nal streamhand er for jar protocol
java·数据库·jar·瀚高数据库
__zRainy__3 小时前
Redis系列:缓存抽象封装与最佳实践
数据库·redis·缓存
cen__y3 小时前
Linux13(数据库)
linux·服务器·c语言·开发语言·数据库
happyprince3 小时前
05-Hugging Face Transformers 缓存系统深度分析
java·spring·缓存
__zRainy__3 小时前
Redis系列:核心数据类型与基础 API 解读
数据库·redis·缓存
雨辰AI4 小时前
人大金仓慢 SQL 根治方法论:问题定位 - 分析 - 优化全流程
数据库·后端·sql·mysql·政务
guslegend4 小时前
2.Redis核心数据结构
数据结构·数据库·redis