首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Spring Boot 多级缓存实践:本地缓存 + 分布式缓存的最佳组合

Spring Boot 多级缓存实践:本地缓存 + 分布式缓存的最佳组合

作者头像
wayn
发布2025-11-13 18:40:14
发布2025-11-13 18:40:14
1640
举报
文章被收录于专栏:wayn的程序开发wayn的程序开发

在本文中,我们将深入探讨 Spring Boot 应用中多层缓存的实现思路。具体而言,我们会采用本地一级缓存(L1)远程分布式二级缓存(L2) 的组合方案:其中一级缓存使用较短的过期时间,二级缓存则配置较长的过期时间。

多层缓存的核心逻辑是优先查询本地缓存,若本地缓存未命中,则再查询二级缓存。其核心目标是通过减少与远程服务的往返通信次数,显著提升应用性能。

技术选型说明

  • 一级缓存(L1):采用 Caffeine 缓存库实现。Caffeine 是当前性能最优的本地缓存方案之一,基于 W-TinyLFU 淘汰算法(高命中率),支持高效的内存管理、灵活的过期策略(写入后过期、访问后过期等),且对 Java 8+ 特性适配良好。
  • 二级缓存(L2):采用 Redis 实现。Redis 作为主流的分布式内存数据存储,具备高可用、高并发支持能力,适合作为跨服务共享的二级缓存。

需要说明的是,本文的实现方案具备通用性——只需轻微调整,即可适配 Ehcache、Hazelcast 等其他缓存组件。

一、Caffeine 依赖引入

要在 Spring Boot 项目中使用 Caffeine,需先引入对应的依赖。推荐通过 Spring Boot 官方的依赖管理(spring-boot-dependencies)控制版本,避免版本冲突;若未使用 Spring Boot 父工程,则需手动指定 Caffeine 版本。

Maven 项目依赖

pom.xml 中添加以下依赖:

代码语言:javascript
复制
<!-- 方式1:直接引入 Caffeine 核心依赖(推荐,版本由 Spring Boot 父工程管理) -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

<!-- 方式2:若需使用 Spring 对 Caffeine 的缓存适配(如 CaffeineCacheManager),可引入 spring-boot-starter-cache -->
<!-- 注:spring-boot-starter-cache 已间接包含 Caffeine 依赖(需确保项目已启用缓存抽象) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Caffeine 优势

选择 Caffeine 作为一级缓存,核心原因在于其卓越的性能与灵活性:

  • 高命中率:采用 W-TinyLFU(Window Tiny Least Frequently Used)淘汰算法,在高并发场景下比传统 LRU 算法命中率提升 10%-20%。
  • 低延迟:底层基于 Java 并发容器(如 ConcurrentHashMap),读写操作均为低延迟设计,支持每秒数百万次缓存访问。
  • 灵活的过期策略:支持 expireAfterWrite(写入后过期)、expireAfterAccess(访问后过期)、expireAfter(自定义过期逻辑),满足不同业务场景。
  • 内存安全:支持配置最大缓存容量(maximumSize),当缓存达到阈值时自动淘汰旧数据,避免内存溢出。

二、简单却粗糙的双层缓存实现方案

在 Spring Boot 中,我们通常使用 @Cacheable 注解实现方法结果缓存。若要实现双层缓存,最直接的思路是:手动检查一级缓存(Caffeine),未命中时再查询二级缓存(Redis)。

首先看二级缓存服务的实现(基于 @Cacheable 注解):

代码语言:javascript
复制
public class L2CacheService {

    @Cacheable(value = "myCache", key = "#id")
    public String getFromCache(String id) {
        // 该方法的返回结果会被自动缓存(默认对接二级缓存)
        return "Hello " + id;
    }
}

在此基础上,封装一级缓存逻辑,形成双层缓存调用:

代码语言:javascript
复制
public class L1CacheService {

    @Autowired
    private L2CacheService l2CacheService;

    // 初始化 Caffeine 本地缓存:最大缓存数 100,写入后 10 分钟过期
    private Cache<String, String> caffeineCache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();

    public String getFromCache(String id) {
        // 1. 优先查询一级缓存
        String result = caffeineCache.getIfPresent(id);
        if (result == null) {
            // 2. 一级缓存未命中,查询二级缓存
            result = l2CacheService.getFromCache(id);
            // 3. 若二级缓存命中,同步到一级缓存
            if (result != null) {
                caffeineCache.put(id, result);
            }
        }
        return result;
    }
}

显然,这种方案并不理想:代码冗余度高,且需为每个缓存场景单独创建服务类,极易因手动维护缓存逻辑导致错误(如缓存同步遗漏、过期时间不一致等)。我们有更优的实现方式。

三、基于 Spring 缓存抽象的优化方案

Spring 提供的 CompositeCacheManager(组合缓存管理器)可将多个缓存组件整合,大幅简化双层缓存的配置逻辑。以下是具体实现:

1. 缓存配置类

代码语言:javascript
复制
@Configuration
@EnableCaching// 启用 Spring 缓存抽象
publicclass CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        // 1. 配置一级缓存(Caffeine)
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager("myCache");
        caffeineCacheManager.setCaffeine(caffeineCacheBuilder());
        caffeineCacheManager.setAllowNullValues(false); // 不允许缓存 null 值

        // 2. 配置二级缓存(Redis)
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate());
        redisCacheManager.setUsePrefix(true); // 启用缓存键前缀,避免键冲突

        // 3. 组合缓存管理器:优先使用 Caffeine 缓存,未命中则查询 Redis
        CompositeCacheManager compositeCacheManager = new CompositeCacheManager(caffeineCacheManager, redisCacheManager);
        compositeCacheManager.setFallbackToNoOpCache(true); // 当缓存不存在时,启用无操作缓存降级(避免报错)

        return compositeCacheManager;
    }

    // 配置 Caffeine 缓存策略
    private Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
            .maximumSize(100) // 最大缓存条目数
            .expireAfterWrite(10, TimeUnit.MINUTES); // 写入后 10 分钟过期
    }

    // 配置 Redis 模板(序列化方式)
    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        // 配置键/值的序列化器(避免 Redis 中存储乱码)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    // 配置 Redis 连接工厂(默认使用 Lettuce 客户端)
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        returnnew LettuceConnectionFactory();
    }
}

2. 业务服务实现

只需在方法上添加 @Cacheable 注解,即可自动触发双层缓存逻辑:

代码语言:javascript
复制
@Cacheable(value = "myCache", key = "#id")
public String getFromCache(String id) {
    return "Hello " + id;
}

方案优势与局限性

  • 优势:基于 Spring 原生缓存抽象,无需手动维护缓存调用逻辑,代码简洁且容错性强。
  • 局限性CompositeCacheManager 仅支持“一级缓存未命中则查询二级缓存”,但不会将二级缓存的命中结果同步到一级缓存。若需实现缓存同步,仍需在业务方法中手动编写逻辑:
代码语言:javascript
复制
public String getFromCache(String id) {
    // 1. 查询一级缓存
    String result = caffeineCacheManager.getCache("myCache").get(id, String.class);
    if (result == null) {
        // 2. 一级缓存未命中,查询二级缓存
        result = redisCacheManager.getCache("myCache").get(id, String.class);
        // 3. 同步二级缓存结果到一级缓存
        if (result != null) {
            caffeineCacheManager.getCache("myCache").put(id, result);
        }
    }
    return result;
}

显然,这种“配置+手动代码”的混合模式仍不完美。接下来,我们将通过自定义缓存管理器,实现真正无缝的双层缓存同步。

四、基于自定义 CacheManager 的完美方案

通过实现 Spring 的 CacheManager 接口与 Cache 接口,我们可以完全掌控双层缓存的读写、同步与过期逻辑,实现“一次注解,双向同步”。

1. 自定义缓存管理器(CustomCacheManager)

代码语言:javascript
复制
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import java.util.Collection;

publicclass CustomCacheManager implements CacheManager {

    // 依赖注入一级缓存管理器(Caffeine)与二级缓存管理器(Redis)
    privatefinal CacheManager caffeineCacheManager;
    privatefinal CacheManager redisCacheManager;

    public CustomCacheManager(CacheManager caffeineCacheManager, CacheManager redisCacheManager) {
        this.caffeineCacheManager = caffeineCacheManager;
        this.redisCacheManager = redisCacheManager;
    }

    // 获取缓存实例:返回自定义的双层缓存实现
    @Override
    public Cache getCache(String name) {
        Cache caffeineCache = caffeineCacheManager.getCache(name);
        Cache redisCache = redisCacheManager.getCache(name);
        returnnew CustomCache(caffeineCache, redisCache);
    }

    // 获取所有缓存名称(与一级缓存保持一致)
    @Override
    public Collection<String> getCacheNames() {
        return caffeineCacheManager.getCacheNames();
    }
}

2. 自定义缓存实现(CustomCache)

代码语言:javascript
复制
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import java.util.concurrent.Callable;

// 采用 record 类型简化代码(JDK 16+ 支持),也可使用 class 实现
public record CustomCache(Cache firstLevelCache, Cache secondLevelCache) implements Cache {

    // 缓存名称:与一级缓存保持一致
    @Override
    public String getName() {
        return firstLevelCache.getName();
    }

    // 原生缓存实例:返回一级缓存的原生实例
    @Override
    public Object getNativeCache() {
        return firstLevelCache.getNativeCache();
    }

    // 缓存查询:优先查一级,未命中则查二级,并同步到一级
    @Override
    public ValueWrapper get(Object key) {
        // 1. 查询一级缓存
        ValueWrapper valueWrapper = firstLevelCache.get(key);
        if (valueWrapper == null) {
            // 2. 一级未命中,查询二级缓存
            valueWrapper = secondLevelCache.get(key);
            if (valueWrapper != null) {
                // 3. 同步二级缓存结果到一级
                firstLevelCache.put(key, valueWrapper.get());
            }
        }
        return valueWrapper;
    }

    // 带类型的缓存查询:逻辑与 get(Object key) 一致
    @Override
    public <T> T get(Object key, Class<T> type) {
        T value = firstLevelCache.get(key, type);
        if (value == null) {
            value = secondLevelCache.get(key, type);
            if (value != null) {
                firstLevelCache.put(key, value);
            }
        }
        return value;
    }

    // 带值加载器的查询:优先从一级缓存加载,失败则从二级加载
    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        try {
            // 优先从一级缓存加载
            return firstLevelCache.get(key, valueLoader);
        } catch (Exception e) {
            // 一级缓存加载失败(如无数据),从二级缓存加载
            return secondLevelCache.get(key, valueLoader);
        }
    }

    // 缓存写入:同时写入一级与二级缓存
    @Override
    public void put(Object key, Object value) {
        firstLevelCache.put(key, value);
        secondLevelCache.put(key, value);
    }

    // 缓存删除:同时删除一级与二级缓存的对应键
    @Override
    public void evict(Object key) {
        firstLevelCache.evict(key);
        secondLevelCache.evict(key);
    }

    // 缓存清空:同时清空一级与二级缓存
    @Override
    public void clear() {
        firstLevelCache.clear();
        secondLevelCache.clear();
    }
}

3. 配置自定义缓存管理器

代码语言:javascript
复制
@Configuration
@EnableCaching
publicclass CacheConfig {

    publicstaticfinal String CACHE_NAME = "doubleCachingCache";

    // 配置自定义缓存管理器
    @Bean
    public CacheManager customCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 1. 配置一级缓存(Caffeine)
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(
                Caffeine.newBuilder()
                        .maximumSize(100) // 一级缓存最大条目数
                        .expireAfterWrite(Duration.ofMinutes(10)) // 一级缓存过期时间:10 分钟
        );

        // 2. 配置二级缓存(Redis)
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(
                        RedisCacheConfiguration.defaultCacheConfig()
                                .entryTtl(Duration.ofHours(1)) // 二级缓存过期时间:1 小时
                )
                .build();

        // 3. 返回自定义缓存管理器(整合一级与二级缓存)
        returnnew CustomCacheManager(caffeineCacheManager, redisCacheManager);
    }
}

4. 业务服务使用

无需任何额外代码,仅通过 @Cacheable 注解即可触发完整的双层缓存逻辑:

代码语言:javascript
复制
@Cacheable(value = CacheConfig.CACHE_NAME, key = "#id")
public String getFromCache(String id) {
    return "Hello " + id;
}

核心逻辑说明

  • 查询逻辑:一级缓存命中 → 直接返回;一级未命中 → 查询二级缓存 → 同步到一级缓存后返回。
  • 写入逻辑:数据同时写入一级与二级缓存,确保缓存一致性。
  • 删除/清空逻辑:操作同时作用于两级缓存,避免“一级缓存已删、二级缓存仍存在”的脏数据问题。

以下图表展示了完整的操作流程:

五、总结

本文详细介绍了 Spring Boot 应用中以 Caffeine 为本地一级缓存、Redis 为分布式二级缓存的多层缓存实现,从手动管理的粗糙方案、Spring CompositeCacheManager 的优化方案,到自定义 CacheManager 实现缓存自动同步的完美方案。为提升应用性能、保证缓存一致性提供了可落地的实践指南。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 waynblog 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术选型说明
  • 一、Caffeine 依赖引入
    • Maven 项目依赖
    • Caffeine 优势
  • 二、简单却粗糙的双层缓存实现方案
  • 三、基于 Spring 缓存抽象的优化方案
    • 1. 缓存配置类
    • 2. 业务服务实现
    • 方案优势与局限性
  • 四、基于自定义 CacheManager 的完美方案
    • 1. 自定义缓存管理器(CustomCacheManager)
    • 2. 自定义缓存实现(CustomCache)
    • 3. 配置自定义缓存管理器
    • 4. 业务服务使用
    • 核心逻辑说明
  • 五、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档