Spring Cache 框架的注解 @Cacheable: 「声明式缓存」


Spring Cache 框架的注解 @Cacheable: 「声明式缓存」

@Cacheable(cacheNames = ChargeConstants.CACHE_PREFIX, cacheManager = "hourCacheManager",
        key = "'" + ChargeConstants.ORDER_CACHE_PREFIX + "'+ #root.methodName + ':' + #orderDetailId",
        unless = "#result == null")
public ChargeOrderDetailBase queryOrderChargeDetailCache(Long orderDetailId) {
    //缓存失效时,查询主库,防止主从不同步。最多5次保护
    String orderQueryMasterLimitKey = ChargeConstants.CACHE_PREFIX + "::" + ChargeConstants.ORDER_CACHE_PREFIX + "orderDetailId_MasterLimit:" +orderDetailId;
    Integer value = redisTempleUtils.incr(orderQueryMasterLimitKey, 24, TimeUnit.HOURS);
    if (value != null && value <= 5) {
        return queryOrderDetailFromMasterByDetailId(orderDetailId);
    }
    RpcResp<ChargeOrderDetailBase> rpcResp = queryOrderChargeDetailWithRpc("", orderDetailId);
    return RpcUtil.getData(rpcResp);
}

一、核心功能复述

你这段代码是 Spring Cache 框架中 @Cacheable 注解的典型使用,核心目的是 —— 对标注该注解的方法返回结果做「声明式缓存」,方法执行前先从指定缓存(xx_chahe)中查询:

  • 若缓存命中则直接返回缓存值(不执行方法体);
  • 若缓存未命中则执行方法体,仅当方法返回值非 null 时,将结果存入缓存,后续相同条件的调用可直接复用缓存,提升接口性能。

二、@Cacheable 注解属性逐行解析

@Cacheable 是 Spring Cache 的核心注解(声明式缓存,无需手动操作 Redis / 内存缓存),以下拆解每个属性的含义和作用:

属性名代码值含义与实现细节
cacheNames"xx_cache"缓存名称(缓存分组 / 命名空间)
1. 必须指定,Spring 会根据该名称定位对应的缓存区域;
2. 作用:对缓存做逻辑分组(如「用户缓存」「商品缓存」),方便后续批量清理 / 管理;
3. 若底层用 Redis 作为缓存,该名称会作为 Redis Key 的前缀一部分(如 xx_chahe::user_prefix:methodName:123)。
cacheManager"hourCacheManager"指定缓存管理器(自定义的「小时级缓存管理器」)
1. Spring Cache 支持多缓存管理器(默认 / 自定义),该属性指定使用名为 hourCacheManager 的 Bean 作为缓存管理器;
2. 自定义的 hourCacheManager 通常配置了:
- 缓存介质(如 Redis,而非默认内存缓存);
- 缓存过期时间(小时级,如 1 小时 / 24 小时,避免缓存永久有效导致脏数据);
- 缓存序列化规则(如 JSON 序列化)。
key"'"+Constants.USER_CACHE_PREFIX+"' + #root.methodName + ':' + #userDetailId"缓存键(Key)的生成规则(SpEL 表达式),保证缓存键的唯一性,避免不同请求的缓存冲突:
1. 拆解 SpEL 表达式:- '"+Constants.USER_CACHE_PREFIX+"':拼接常量前缀(如 Constants.USER_CACHE_PREFIX = "user:detail:"),单引号包裹表示字符串常量;
- #root.methodName:SpEL 内置变量,指当前方法名(如方法名是 getUserDetail,则该部分为 getUserDetail);
- #userDetailId:指方法的入参(参数名必须为 userDetailId),如入参值为 123,则该部分为 123
2. 最终生成的 Key 示例:user:detail:getUserDetail:123(唯一标识「getUserDetail 方法查询 userDetailId=123 的结果」)。
unless"#result == null"缓存排除条件(「除非」满足该条件,否则缓存):
1. #result:SpEL 内置变量,指方法的返回值
2. 逻辑:仅当方法返回值 != null 时,才将结果存入缓存;若返回值为 null,则不缓存(避免缓存 null 值,导致后续请求直接返回 null,而非重新查询);
3. 对比:与 condition 属性相反(condition="#result != null" 等价于 unless="#result == null"),unless 是「排除缓存」,condition 是「条件缓存」。

三、整体实现逻辑(执行流程)

Spring Cache 对标注 @Cacheable 的方法执行时,会按以下步骤处理(核心是「先查缓存,再执行业务,最后缓存结果」):

graph TD
    A[调用标注@Cacheable的方法] --> B[根据cacheManager找到hourCacheManager]
    B --> C[根据cacheNames=xx_chahe定位缓存区域]
    C --> D[解析key表达式生成唯一缓存Key]
    D --> E{缓存中是否存在该Key的有效值?}
    E -->|是(缓存命中)| F[直接返回缓存值,不执行方法体]
    E -->|否(缓存未命中)| G[执行方法体(如查询DB/远程接口)]
    G --> H{方法返回值#result是否为null?}
    H -->|是(#result=null)| I[不存入缓存,直接返回null]
    H -->|否(#result≠null)| J[将返回值存入缓存(Key=解析后的唯一Key)]
    J --> K[返回方法执行结果]

关键细节补充:

  1. 缓存命中的核心价值:若缓存中已有该 Key 的值,方法体(如数据库查询 select * from user where id=123)不会执行,直接返回缓存值,大幅减少数据库 / 远程调用开销,提升接口响应速度。

  2. 缓存管理器的作用:hourCacheManager是自定义的 Bean,通常在配置类中声明,示例如下(以 Redis 缓存为例):

    @Bean("hourCacheManager")
    public RedisCacheManager hourCacheManager(RedisConnectionFactory factory) {
        // 配置缓存过期时间为1小时
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)) // 小时级过期,对应hourCacheManager
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return RedisCacheManager.builder(factory).cacheDefaults(config).build();
    }
    
  3. Key 唯一性保障常量前缀 + 方法名 + 参数 的组合,避免不同方法、不同参数的缓存冲突(如 getUserDetail(123)getUserDetail(456) 生成不同 Key,getUserInfo(123) 因方法名不同也生成不同 Key)。

  4. unless 的防御性设计:若方法查询结果为 null(如用户不存在),不缓存 null 值,否则后续查询该不存在的用户时,会一直返回缓存的 null,而非重新查询(避免「缓存穿透」的基础防护)。

四、使用场景与核心设计思路

1. 典型使用场景

该注解适用于「读多写少」的查询场景,比如:

  • 根据 userDetailId 查询用户详情(如 getUserDetail(Long userDetailId));
  • 高频查询但数据变更频率低(如用户基础信息,1 小时更新一次即可);
  • 希望通过缓存减少数据库压力,提升接口响应速度。

2. 核心设计思路

  • 声明式缓存:无需手动编写「查询缓存→缓存未命中查 DB→存入缓存」的代码,通过注解一键实现,简化开发;
  • 缓存隔离cacheNames 分组 + 自定义 cacheManager,实现不同业务缓存的过期策略、存储介质隔离(如用户缓存 1 小时过期,商品缓存 10 分钟过期);
  • 避免无效缓存unless="#result == null" 过滤 null 值,避免缓存无效数据,同时减少缓存空间占用。

五、注意事项(避坑点)

  1. Key 表达式写法:SpEL 中字符串常量必须用单引号包裹(如 'user:detail:'),否则会被解析为变量名,导致 Key 生成失败;
  2. 参数名匹配#userDetailId 必须与方法入参名完全一致(若编译时丢失参数名,需用 #arg0 代替,如 #arg0 表示第一个入参);
  3. 缓存管理器命名cacheManager = "hourCacheManager" 必须与配置类中 @Bean("hourCacheManager") 的名称一致,否则会找不到缓存管理器,使用默认管理器;
  4. 缓存更新 / 失效:若用户数据更新(如修改昵称),需手动清理缓存(如 @CacheEvict 注解),否则缓存会返回旧数据(脏数据);
  5. 笔误修正cacheNames = "xx_chahe" 应为 xx_cache(拼写错误),不影响功能,但建议修正以符合命名规范。

总结

  1. 核心逻辑:通过 @Cacheable 实现声明式缓存,优先从 hourCacheManager 管理的 xx_chahe 缓存中查询,缓存未命中则执行方法体,仅当返回值非 null 时存入缓存;
  2. 关键设计:
    • 缓存 Key 由「常量前缀 + 方法名 + 参数」组成,保证唯一性;
    • 自定义 hourCacheManager 控制缓存过期时间(小时级),避免脏数据;
    • unless 过滤 null 值,避免无效缓存;
  3. 核心价值:无侵入式提升查询性能,减少数据库 / 远程调用开销,简化缓存代码开发。
SpringBoot
JAVA-技能点
中间件
Java注解