@Cacheable 注解 + Caffeine 的使用


Caffeine 配置管理

@Configuration
public class CaffeineConfig {

    public static final String CACHE_PREFIX_ONE = "ONE";
    public static final String CACHE_PREFIX_TWO = "TWO";

    @Bean("caffeineCacheManager")
    public CacheManager caffeineCacheManager(BusinessCacheConfig cacheConfig) {
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                .initialCapacity(20)
                .maximumSize(500)
                .expireAfterWrite(cacheConfig.getCache().getCacheDuration(), TimeUnit.SECONDS);

        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setAllowNullValues(Boolean.FALSE);
        caffeineCacheManager.setCaffeine(caffeine);
        caffeineCacheManager.setCacheNames(Lists.newArrayList(CACHE_PREFIX_ONE));
        return caffeineCacheManager;
    }

    /**
     * 打卡点列表专用缓存管理
     *
     * @param cacheConfig 缓存配置
     * @return
     */
    @Bean("caffeineCacheManagerForPointList")
    public CacheManager caffeineCacheManagerForPointList(BusinessCacheConfig cacheConfig) {
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                .initialCapacity(20)
                .maximumSize(500)
                .expireAfterWrite(cacheConfig.getCache().getPointListCacheDuration(), TimeUnit.SECONDS);

        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setAllowNullValues(Boolean.FALSE);
        caffeineCacheManager.setCaffeine(caffeine);
        caffeineCacheManager.setCacheNames(Lists.newArrayList(CACHE_PREFIX_TWO));
        return caffeineCacheManager;
    }
}

​ 这段示例代码是 Spring Boot 中基于 Caffeine 实现的缓存管理器配置,通过@Configuration@Bean定义了两个不同的 Caffeine 缓存管理器(caffeineCacheManagercaffeineCacheManagerForStationList),核心目的是为不同业务场景提供定制化的本地缓存策略

1. Caffeine 核心配置项解读

先拆解Caffeine.newBuilder()中的关键配置,这是理解 “为什么这么定义” 的基础:

配置项作用
initialCapacity(20)缓存初始容量(预分配空间),减少扩容带来的性能损耗
maximumSize(500)缓存最大条目数(基于 LRU 算法淘汰超出的条目),防止缓存占用过多内存
expireAfterWrite(...)写入后过期时间(TTL),缓存条目写入后,超过指定时间自动失效
setAllowNullValues(false)禁止缓存 null 值,避免空值缓存导致的业务逻辑异常(如判断 null 时误判)
setCacheNames(...)指定缓存名称(缓存分区),不同业务可通过缓存名隔离缓存数据

2. 两个缓存管理器的差异

  • 两个 Bean 的核心区别仅在于过期时间

    • caffeineCacheManager:使用cacheConfig.getCache().getCacheDuration()(通用缓存过期时间);

    • caffeineCacheManagerForPointList:使用cacheConfig.getCache().getPointListCacheDuration()(打卡点列表专用过期时间)。

      其余配置(初始容量、最大容量、禁止 null 值)完全一致,目的是为 “打卡点列表” 这一高频 / 特殊业务提供独立的缓存策略。

二、为什么要这样定义 Caffeine?

1. 核心原因:业务场景差异化需求

本地缓存的核心是 “适配业务特性”,不同业务对缓存的 “过期时效、内存占用、访问频率” 要求不同,分开定义能精准匹配需求:

  • 过期时间差异化:打卡点列表(PointList)可能是 “低频更新、高频访问”(如站点基础信息),或 “高频更新、需快速失效”(如打卡点实时状态),因此需要独立配置过期时间,而不是复用通用缓存的时效;
  • 缓存隔离:通过不同 Bean 名称(@Bean("xxx"))和缓存名,实现缓存数据的物理隔离,避免不同业务的缓存互相干扰(如通用缓存失效不会影响站点列表缓存);
  • 配置中心化:过期时间通过BusinessCacheConfig读取(而非硬编码),支持配置文件动态调整,无需修改代码重启服务。

2. 技术层面的合理性

  • 初始容量 + 最大容量initialCapacity(20)预分配空间,减少缓存初始化后频繁扩容的性能开销;maximumSize(500)限制缓存上限,防止缓存无限制增长导致 OOM(内存溢出);
  • 写入后过期(expireAfterWrite):适用于 “数据写入后,超过一定时间就失效” 的场景(如非实时性业务数据),相比expireAfterAccess(访问后过期),更适合 “数据有明确有效期” 的场景;
  • 禁止 null 值缓存:避免业务代码中因缓存 null 值导致的逻辑错误(例如:查询不到数据时缓存 null,后续查询直接返回 null,而不是重新查询数据库)。

三、具体使用场景

结合代码中 “通用缓存” 和 “站点列表专用缓存” 的设计,列举典型适用场景:

场景 1:通用缓存(caffeineCacheManager)

适用于无特殊时效要求、访问频率中等的业务数据,例如:

  • 充值业务的基础配置(如充值金额档位、手续费比例):这类数据更新频率低,写入缓存后可保留较长时间(如 30 分钟 / 1 小时);
  • 用户充值记录的摘要信息(非实时):查询后缓存,避免频繁查询数据库,过期时间可配置为 5-10 分钟;
  • 支付渠道的基础信息(如渠道名称、支持的支付方式):更新频率极低,缓存时效可设为 1 小时以上。

场景 2:站点列表专用缓存(caffeineCacheManagerForPointList)

适用于站点相关、时效要求更精准的业务数据,例如:

  • 充电桩站点基础列表(如站点名称、地址、充电桩数量):这类数据可能每天更新 1-2 次,缓存过期时间可设为 30 分钟,既减少数据库查询,又保证数据不会过期太久;
  • 站点实时状态摘要(如站点是否可用、空闲充电桩数):若状态更新频率较高(如 5 分钟一次),可将缓存过期时间设为 5 分钟,平衡 “性能” 和 “实时性”;
  • 打卡点区域分组列表(如按城市 / 区域划分的站点列表):更新频率低,但需独立缓存,避免和通用缓存混用导致失效时机不一致。

场景 3:Caffeine 配置项对应的通用场景扩展

除上述业务场景,代码中的 Caffeine 配置还适用于以下通用本地缓存场景:

配置项适配场景
maximumSize(500)缓存条目数有限的场景(如中小型应用的本地缓存,避免内存占用过高)
initialCapacity(20)缓存初始化后会快速填充数据的场景(如服务启动后批量加载基础数据)
expireAfterWrite数据有 “写入后有效期” 的场景(如活动配置、临时促销规则)
setAllowNullValues(false)所有需要 “缓存有效数据、避免空值干扰” 的场景(如查询类业务、数据统计类业务)

四、补充说明(优化点 / 注意事项)

  1. 缓存名称优化:代码中两个缓存管理器都设置了ChargeConstants.CACHE_PREFIX作为缓存名,建议为站点列表单独设置缓存名(如ChargeConstants.STATION_LIST_CACHE),彻底隔离缓存数据;
  2. 过期策略选择:若站点列表需要 “访问后过期”(如用户访问一次后,再延长有效期),可改用expireAfterAccess
  3. 缓存刷新:若需要 “缓存过期前自动刷新”(如避免缓存失效时的查询峰值),可添加refreshAfterWrite配置(需结合LoadingCache使用);
  4. 监控与兜底:生产环境中需监控缓存命中率(Caffeine 支持统计命中率),若命中率过低,需调整缓存容量 / 过期时间;同时需做好缓存失效后的兜底(直接查询数据库)。

总结

这段代码的核心设计思路是 “按业务场景定制缓存策略”:通过拆分不同的缓存管理器,适配不同业务数据的时效、访问特性;通过配置化的方式让缓存策略可动态调整;通过 Caffeine 的核心配置保证缓存的性能和稳定性。这种设计适用于 “多业务模块、缓存需求差异化” 的后端服务(如充电桩 / 充值类业务系统),是本地缓存在 Spring Boot 中典型的最佳实践。


@Cacheable 注解 + Caffeine 的使用

在 Spring Boot 中配置好 Caffeine 缓存管理器(CacheManager)后,核心使用方式分为注解式(声明式)编程式 两种,以下结合代码中两个不同的CacheManager Bean(通用 / 站点列表专用),详细说明使用方法、场景和注意事项。

一、核心前提:Spring 缓存注解基础

Spring 提供了@EnableCaching注解来开启缓存功能,必须先在启动类 / 配置类上添加该注解,否则缓存注解不会生效:

@SpringBootApplication
@EnableCaching // 开启缓存功能
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

二、方式 1:注解式使用(推荐,声明式)

通过 Spring 缓存注解(@Cacheable/@CachePut/@CacheEvict)结合指定的CacheManager Bean,实现缓存的自动读写 / 失效,无需手动操作缓存 API。

1. 通用缓存(caffeineCacheManager)的使用

在业务方法上通过cacheManager指定通用缓存管理器,value指定缓存名称(需匹配配置中setCacheNames的缓存名):

@Service
public class CacheConfigService {

    // 注入业务依赖(如DAO)
    @Autowired
    private CacheConfigMapper cacheConfigMapper;

    /**
     * 查询充值金额档位(通用缓存场景)
     * @param type 充值类型
     * @return 档位列表
     */
    @Cacheable(
        value = CacheConstants.CACHE_PREFIX, // 匹配配置中setCacheNames的缓存名
        cacheManager = "caffeineCacheManager", // 指定通用缓存管理器Bean名
        key = "#type" // 缓存key:用充值类型作为唯一标识
    )
    public List<CacheGrade> queryCacheGrade(String type) {
        // 缓存未命中时执行:查询数据库/其他数据源
        return cacheConfigMapper.selectCacheGradeByType(type);
    }

    /**
     * 更新充值档位后,清除对应缓存(避免脏数据)
     * @param type 充值类型
     */
    @CacheEvict(
        value = CacheConstants.CACHE_PREFIX,
        cacheManager = "caffeineCacheManager",
        key = "#type"
    )
    public void updateCacheGrade(String type, CacheGrade grade) {
        cacheConfigMapper.updateCacheGrade(type, grade);
    }
}

2. 站点列表专用缓存(caffeineCacheManagerForStationList)的使用

同理,通过cacheManager指定专用 Bean 名,适配站点列表业务:

@Service
public class PointService {

    @Autowired
    private PointMapper PointMapper;

    /**
     * 查询站点列表(专用缓存场景)
     * @param city 城市
     * @return 站点列表
     */
    @Cacheable(
        value = CacheConstants.CACHE_PREFIX, // 若需彻底隔离,建议改为专属缓存名(如CacheConstants.Point_LIST_CACHE)
        cacheManager = "caffeineCacheManagerForPointList", // 指定站点专用缓存管理器
        key = "#city" // 缓存key:用城市作为唯一标识
    )
    public List<PointVO> queryPointList(String city) {
        // 缓存未命中时执行:查询数据库
        return PointMapper.selectPointListByCity(city);
    }

    /**
     * 刷新站点信息后,清除缓存
     * @param city 城市
     */
    @CacheEvict(
        value = CacheConstants.CACHE_PREFIX,
        cacheManager = "caffeineCacheManagerForPointList",
        key = "#city"
    )
    public void refreshPointList(String city) {
        // 业务逻辑:如同步最新站点数据
    }
}

注解核心参数说明

参数作用
value缓存名称(需匹配配置类中setCacheNames的值,实现缓存分区)
cacheManager指定使用的缓存管理器 Bean 名(区分通用 / 站点专用缓存)
key缓存的唯一标识(SpEL 表达式),如#city/#type,避免不同参数的缓存冲突
@Cacheable缓存查询结果:方法执行前先查缓存,命中则返回;未命中则执行方法并缓存结果
@CachePut更新缓存:执行方法后,将结果更新到缓存(适用于新增 / 修改场景)
@CacheEvict清除缓存:执行方法后,删除指定 key 的缓存(适用于数据更新 / 删除场景)

三、方式 2:编程式使用(手动操作缓存,灵活度更高)

若业务逻辑复杂(如需要手动控制缓存的读写 / 失效),可直接注入CacheManager,手动获取缓存实例操作:

1. 通用缓存的编程式使用

@Service
public class CacheConfigService {

    // 注入通用缓存管理器(通过Bean名称指定)
    @Autowired
    @Qualifier("caffeineCacheManager")
    private CacheManager caffeineCacheManager;

    public List<CacheGrade> queryCacheGrade(String type) {
        // 1. 获取指定名称的缓存实例
        Cache cache = caffeineCacheManager.getCache(CacheConstants.CACHE_PREFIX);
        // 2. 尝试从缓存中获取数据
        Cache.ValueWrapper wrapper = cache.get(type);
        if (wrapper != null) {
            // 缓存命中:直接返回
            return (List<CacheGrade>) wrapper.get();
        }
        // 3. 缓存未命中:查询数据库
        List<CacheGrade> gradeList = cacheConfigMapper.selectCacheGradeByType(type);
        // 4. 将结果放入缓存
        cache.put(type, gradeList);
        return gradeList;
    }

    // 手动清除缓存
    public void clearCacheGradeCache(String type) {
        Cache cache = caffeineCacheManager.getCache(CacheConstants.CACHE_PREFIX);
        cache.evict(type); // 清除指定key的缓存
        // cache.clear(); // 清除该缓存下所有数据(慎用)
    }
}

2. 打卡点列表专用缓存的编程式使用

@Service
public class PointService {

    // 注入打卡点专用缓存管理器
    @Autowired
    @Qualifier("caffeineCacheManagerForPointList")
    private CacheManager PointListCacheManager;

    public List<PointVO> queryPointList(String city) {
        Cache cache = PointListCacheManager.getCache(CacheConstants.CACHE_PREFIX);
        Cache.ValueWrapper wrapper = cache.get(city);
        if (wrapper != null) {
            return (List<PointVO>) wrapper.get();
        }
        List<PointVO> PointList = PointMapper.selectPointListByCity(city);
        cache.put(city, PointList);
        return PointList;
    }
}

四、关键注意事项

  1. 缓存名隔离优化:代码中两个CacheManager都设置了相同的CacheConstants.CACHE_PREFIX作为缓存名,建议改为专属缓存名(如通用缓存用Cache_CONFIG_CACHE,打卡点列表用Point_LIST_CACHE),避免不同缓存管理器操作同一批缓存数据,导致策略冲突(如过期时间不一致)。
  2. 避免缓存穿透:配置中setAllowNullValues(false)已禁止缓存 null 值,但若查询结果为空(如无对应城市的打卡点),需在业务代码中处理(如返回空列表,而非 null),避免每次查询都穿透到数据库。
  3. 缓存更新策略
    • 数据更新后必须手动清除 / 更新缓存(如@CacheEvict/cache.evict()),否则会出现 “缓存脏数据”;
    • 若数据更新频率低,可结合expireAfterWrite自动过期兜底;若更新频率高,建议主动清除缓存。
  4. 泛型类型处理:编程式获取缓存时,wrapper.get()返回Object,需强制类型转换,建议增加类型校验(如instanceof),避免类型转换异常。
  5. 多缓存管理器区分:注入时必须通过@Qualifier指定 Bean 名称(如@Qualifier("caffeineCacheManagerForPointList")),否则 Spring 会默认注入任意一个CacheManager,导致缓存策略错误。

五、两种使用方式的对比

使用方式优点缺点适用场景
注解式代码简洁、无侵入、自动管理缓存灵活度低,复杂逻辑难以适配简单查询 / 更新、标准化业务逻辑
编程式灵活度高,可手动控制缓存全流程代码冗余、需手动处理缓存逻辑复杂业务(如多条件缓存、批量操作)

总结

  • 配置好CacheManager后,核心是通过@Cacheable等注解(推荐)或手动注入CacheManager操作缓存,关键是通过cacheManager参数 /@Qualifier区分通用和打卡点专用缓存管理器,匹配不同业务的缓存策略(过期时间、容量等)。

  • 实际开发中优先使用注解式,仅在逻辑复杂时用编程式,同时注意缓存隔离、脏数据和穿透问题。

SpringBoot
JAVA-技能点
知识点
设计模式
Java注解