SpringBoot两级缓存实现

Spring Boot 两级缓存(Caffeine + Redis)完整实现(含源码)

本文详细介绍如何在 Spring Boot 2.x / 3.x

项目中实现一个高性能、可广播失效的 两级缓存机制(Caffeine +
Redis)
,并提供完整可运行源码,适合直接集成或二次封装。


完整代码参考gitee地址: https://gitee.com/xizhyu66/twolevelcache-lab

一、设计目标

目标 说明


性能 L1 本地缓存(Caffeine)提供纳秒级访问速度
一致性 L2 Redis 实现跨实例共享,通过 Pub/Sub 保持最终一致
兼容性 兼容 @Cacheable / @CacheEvict 等 Spring Cache 注解
扩展性 可插拔支持不同 L1/L2 实现,例如 Ehcache + Redis


二、整体架构

text 复制代码
                ┌────────────────────────────┐
                │        应用实例 A           │
                │  ┌────────────┐             │
请求 → 缓存查 → │  │ L1:Caffeine │             │
                │  └─────┬──────┘             │
                │        │ 回填               │
                │  ┌─────▼──────┐             │
                │  │ L2:Redis   │◄───┐ 广播   │
                │  └────────────┘    │        │
                └────────────────────┘        │
                        ▲                     │
                        │ Redis Pub/Sub 通知  │
                        │                     ▼
                ┌────────────────────────────┐
                │        应用实例 B           │
                │  ┌────────────┐             │
                │  │ L1:Caffeine │             │
                │  └────────────┘             │
                └────────────────────────────┘

三、核心思路

  1. 读缓存 :先查 L1 → miss → 查 L2 → miss → 回源 DB。若命中
    L2,则回填 L1。
  2. 写更新 :写 DB → 删除 L2 → 通过 Redis Pub/Sub 通知其他节点清理
    L1。
  3. 一致性策略:允许短暂不一致(最终一致),结合延迟双删保障强一致写。

四、完整源码

maven依赖

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

配置(application.yml)

java 复制代码
spring:
  cache:
    type: none   # 我们自定义两级Cache,不用Spring默认单个CacheManager
  redis:
    host: 127.0.0.1
    port: 6379

caffeine:
  spec: maximumSize=10000,expireAfterWrite=5m,recordStats

两级 CacheManager(示例实现要点)

java 复制代码
@Configuration
@EnableCaching
public class CacheConfig {

  @Bean
  public CacheManager cacheManager(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(factory);
    redisTemplate.afterPropertiesSet();

    // L2: Redis Cache
    RedisCacheManager redisMgr = RedisCacheManager.builder(factory)
        .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
           .entryTtl(Duration.ofMinutes(5)))
        .build();

    // L1: Caffeine Cache
    CaffeineCacheManager caffeineMgr = new CaffeineCacheManager();
    caffeineMgr.setCaffeine(Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofMinutes(5)));

    // 组合:先查 L1,再查 L2;put 时写两边
    return new TwoLevelCacheManager(caffeineMgr, redisMgr);
  }

  // 订阅失效广播,清 L1
  @Bean
  public MessageListenerAdapter invalidateListener(TwoLevelCacheManager mgr) {
    return new MessageListenerAdapter((MessageListener) (msg, pattern) -> {
      String key = new String(msg.getBody(), StandardCharsets.UTF_8);
      mgr.invalidateLocal(key);
    });
  }

  @Bean
  public RedisMessageListenerContainer container(RedisConnectionFactory cf,
                                                 MessageListenerAdapter listener) {
    RedisMessageListenerContainer c = new RedisMessageListenerContainer();
    c.setConnectionFactory(cf);
    c.addMessageListener(listener, new PatternTopic("cache:invalidate"));
    return c;
  }
}

📄 TwoLevelCacheManager.java

java 复制代码
package com.example.cache;

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class TwoLevelCacheManager implements CacheManager {

    private final CacheManager l1Manager;
    private final CacheManager l2Manager;
    private final StringRedisTemplate stringRedisTemplate;
    private final String invalidateTopic;
    private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();

    public TwoLevelCacheManager(CacheManager l1Manager, CacheManager l2Manager) {
        this(l1Manager, l2Manager, null, null);
    }

    public TwoLevelCacheManager(CacheManager l1Manager,
                                CacheManager l2Manager,
                                StringRedisTemplate stringRedisTemplate,
                                String invalidateTopic) {
        this.l1Manager = Objects.requireNonNull(l1Manager, "L1 manager required");
        this.l2Manager = Objects.requireNonNull(l2Manager, "L2 manager required");
        this.stringRedisTemplate = stringRedisTemplate;
        this.invalidateTopic = invalidateTopic;
    }

    @Override
    public Cache getCache(String name) {
        return cacheMap.computeIfAbsent(name, n -> {
            Cache l1 = l1Manager.getCache(n);
            Cache l2 = l2Manager.getCache(n);
            return new TwoLevelCache(n, l1, l2, this::publishInvalidate);
        });
    }

    @Override
    public Collection<String> getCacheNames() {
        Set<String> names = new LinkedHashSet<>(l1Manager.getCacheNames());
        names.addAll(l2Manager.getCacheNames());
        return names;
    }

    public void invalidateLocal(String cacheName, Object key) {
        Cache cache = cacheMap.get(cacheName);
        if (cache instanceof TwoLevelCache) ((TwoLevelCache) cache).invalidateLocal(key);
    }

    public void invalidateLocal(String cacheName) {
        Cache cache = cacheMap.get(cacheName);
        if (cache instanceof TwoLevelCache) ((TwoLevelCache) cache).invalidateLocalAll();
    }

    private void publishInvalidate(String cacheName, Object keyOrWildcard) {
        if (stringRedisTemplate == null || invalidateTopic == null) return;
        String payload = keyOrWildcard == null ? cacheName + "::*" : cacheName + "::" + keyOrWildcard;
        stringRedisTemplate.convertAndSend(invalidateTopic, payload);
    }
}

📄 TwoLevelCache.java

java 复制代码
package com.example.cache;

import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import java.util.concurrent.Callable;

class TwoLevelCache implements Cache {

    private final String name;
    private final Cache l1;
    private final Cache l2;
    private final InvalidatePublisher publisher;

    @FunctionalInterface
    interface InvalidatePublisher {
        void publish(String cacheName, Object keyOrWildcard);
    }

    TwoLevelCache(String name, Cache l1, Cache l2, InvalidatePublisher publisher) {
        this.name = name; this.l1 = l1; this.l2 = l2; this.publisher = publisher;
    }

    @Override
    public String getName() { return name; }

    @Override
    public Object getNativeCache() { return Map.of("l1", l1.getNativeCache(), "l2", l2.getNativeCache()); }

    @Override
    public ValueWrapper get(Object key) {
        ValueWrapper v = l1.get(key);
        if (v != null) return v;
        v = l2.get(key);
        if (v != null) l1.put(key, v.get());
        return v;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        ValueWrapper vw = get(key);
        if (vw != null) return (T) vw.get();
        try {
            T val = valueLoader.call();
            if (val != null) { l2.put(key, val); l1.put(key, val); }
            return val;
        } catch (Exception e) { throw new ValueRetrievalException(key, valueLoader, e); }
    }

    @Override
    public void put(Object key, Object value) {
        l2.put(key, value);
        l1.put(key, value);
        publisher.publish(name, key);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        ValueWrapper existing = get(key);
        if (existing != null) return existing;
        put(key, value);
        return new SimpleValueWrapper(value);
    }

    @Override
    public void evict(Object key) {
        l2.evict(key); l1.evict(key);
        publisher.publish(name, key);
    }

    @Override
    public void clear() {
        l2.clear(); l1.clear();
        publisher.publish(name, null);
    }

    void invalidateLocal(Object key) { l1.evict(key); }
    void invalidateLocalAll() { l1.clear(); }
}

五、Redis 订阅配置

java 复制代码
@Bean
public RedisMessageListenerContainer cacheInvalidateListenerContainer(
        RedisConnectionFactory cf, TwoLevelCacheManager twoLevelCacheManager) {
    RedisMessageListenerContainer c = new RedisMessageListenerContainer();
    c.setConnectionFactory(cf);
    c.addMessageListener((message, pattern) -> {
        String payload = new String(message.getBody(), java.nio.charset.StandardCharsets.UTF_8);
        int idx = payload.indexOf("::");
        if (idx > 0) {
            String cacheName = payload.substring(0, idx);
            String key = payload.substring(idx + 2);
            if ("*".equals(key)) twoLevelCacheManager.invalidateLocal(cacheName);
            else twoLevelCacheManager.invalidateLocal(cacheName, key);
        }
    }, new org.springframework.data.redis.listener.ChannelTopic("cache:invalidate"));
    return c;
}

六、使用示例

java 复制代码
@Service
public class UserService {
  @Cacheable(cacheNames = "user", key = "#id") // 读走两级缓存
  public UserDTO findById(Long id) { return repo.findById(id); }

  @Transactional
  public void updateUser(UserDTO u) {
    repo.update(u);
    redisTemplate.delete("user::" + u.getId());               // 删 L2
    stringRedisTemplate.convertAndSend("cache:invalidate",
                                       "user::" + u.getId()); // 广播删 L1
    // 可选:延迟双删
    taskScheduler.schedule(() ->
        redisTemplate.delete("user::" + u.getId()),
        Instant.now().plusMillis(100));
  }
}

七、缓存策略建议

问题类型 解决方案


缓存穿透 缓存空对象 + BloomFilter

缓存击穿 单 Key 互斥锁 + 逻辑过期

缓存雪崩 TTL 加随机抖动

热点 Key 独立 TTL + 预热机制

一致性 延迟双删 + Pub/Sub 广播


八、总结

本方案实现: - ✅ 高性能读写路径(Caffeine + Redis)\

  • ✅ 简洁无侵入整合 Spring Cache\
  • ✅ 支持多实例缓存一致性广播\
  • ✅ 可自由扩展到多级缓存或多数据源缓存场景

推荐使用场景:高并发读多写少的服务,如用户资料、配置中心、商品详情、字典表等。

相关推荐
勇往直前plus4 小时前
学习和掌握RabbitMQ及其与springboot的整合实践(篇一)
spring boot·学习·spring cloud·rabbitmq·java-rabbitmq
lang201509284 小时前
Spring Boot与K8s集成的核心机制
spring boot·后端·kubernetes
摇滚侠13 小时前
Spring Boot 3零基础教程,新特性 ProblemDetails,笔记50
spring boot·笔记
朝新_15 小时前
【SpringBoot】详解Maven的操作与配置
java·spring boot·笔记·后端·spring·maven·javaee
小丁爱养花17 小时前
Redis 内部编码/单线程模型/string
数据库·redis·缓存·1024程序员节
爬山算法17 小时前
Redis(84)如何解决Redis的缓存击穿问题?
java·redis·缓存
程序定小飞17 小时前
基于springboot的电影评论网站系统设计与实现
java·spring boot·后端
苹果醋318 小时前
JAVA面试汇总(二)多线程(五)
运维·vue.js·spring boot·nginx·课程设计
兜兜风d'19 小时前
RabbitMQ 持久性详解
spring boot·分布式·rabbitmq·1024程序员节