深入解析 Spring @Cacheable 注解:从声明式缓存 到多维替代方案

深入解析 Spring @Cacheable 注解:从声明式缓存到多维替代方案

本文以一个真实生产项目(yu-ai-code-mother)中的精选应用分页接口为切入点,系统解析 Spring Cache 的 @Cacheable 注解。面向中级 Java 开发者,兼顾理论深度与可运行的实践示例。


0. 引言:从一段真实代码说起

AppController 的「精选应用分页」接口中,存在如下声明式缓存配置:

java 复制代码
@PostMapping("/good/list/page/vo")
@Cacheable(
        value = "good_app_page",
        key = "T(com.yupi.yuaicodemother.utils.CacheKeyUtils).generateKey(#appQueryRequest)",
        condition = "#appQueryRequest.pageNum <= 10"
)
public BaseResponse<Page<AppVO>> listGoodAppVOByPage(@RequestBody AppQueryRequest appQueryRequest) {
    // ... 限制每页 20 条、设置精选优先级、构建 QueryWrapper、分页查询、封装 VO
    return ResultUtils.success(appVOPage);
}

这段代码只有 5 行注解,却涵盖了 缓存命名空间SpEL 动态 Key条件缓存 三大核心能力,配合 RedisCacheManagerConfig 中针对 good_app_page 设置的 5 分钟 TTL,构成了一个完整的「热点列表 + 短时缓存」方案。

本文将围绕它展开:先讲透注解本身,再给出三种替代实现并横向对比,最后给出选型决策。


1. 注解的基本语法和参数配置说明

@Cacheable 来自 org.springframework.cache.annotation,是 Spring Cache 抽象的核心注解之一,语义为「方法执行前先查缓存,命中则直接返回;未命中则执行方法并把结果写入缓存」。

1.1 完整属性表

属性 类型 默认值 说明
value / cacheNames String[] {} 缓存命名空间(可理解为 cache 的"表名"),必填之一
key String "" SpEL 表达式,用于计算缓存 key;不写则用 SimpleKeyGenerator
keyGenerator String "" 自定义 KeyGenerator Bean 名;与 key 互斥
cacheManager String "" 指定使用的 CacheManager
cacheResolver String "" 动态解析缓存,与 cacheManager 互斥
condition String "" SpEL,方法执行前后均判断 ,为 false 则不读写缓存
unless String "" SpEL,仅方法执行后 判断,为 true 则不写入缓存(仍会读)
sync boolean false 同步加锁,避免缓存击穿(底层依赖 Cache#get(key, valueLoader)

1.2 关键属性详解

value ------ 缓存命名空间

底层会被拼到最终 Redis key 上。本项目 RedisCacheManagerConfig 中:

java 复制代码
.withCacheConfiguration("good_app_page",
        defaultConfig.entryTtl(Duration.ofMinutes(5)))

表示 good_app_page 这个命名空间单独享受 5 分钟过期,其余走默认 30 分钟。这就是「按命名空间精细控制 TTL」的典型用法。

key ------ SpEL 表达式

SpEL 是 @Cacheable 的灵魂。可用元数据:

表达式 含义
#param 方法参数(参数名需 -parameters 编译或使用 #p0
#root.methodName 方法名
#root.target 目标对象
#root.args 参数数组
#result 方法返回值(仅 unless 可用)
T(全限定类名) 引用静态方法/字段

本项目使用 T(...).generateKey(#appQueryRequest),调用静态工具把整个请求对象压缩成 MD5:

java 复制代码
public static String generateKey(Object obj) {
    if (obj == null) {
        return DigestUtil.md5Hex("null");
    }
    String jsonStr = JSONUtil.toJsonStr(obj);
    return DigestUtil.md5Hex(jsonStr);
}

为什么用 JSON + MD5?

  • 对象字段多,直接拼 key 又长又易错;
  • MD5 固定 32 位,控制 Redis key 长度(Redis 大 key 险情);
  • 不同字段组合也能被同一算法覆盖,避免手写 #a + '_' + #b

⚠️ 隐患:对象字段顺序、Null 字段策略、transient 字段都会影响 JSON 字符串,进而影响命中率。Hutool 的 JSONUtil.toJsonStr 默认忽略 null 值,需团队知悉。

condition ------ 条件缓存

condition = "#appQueryRequest.pageNum <= 10" 表示只缓存前 10 页。这是非常经典的高并发列表缓存策略:

  • 前 10 页是绝大多数用户行为(长尾效应),缓存收益最高;
  • 第 11 页之后多为"翻到底"的少数请求,缓存命中率低,反而浪费空间。

注意 condition 在「读」和「写」两个阶段都生效:条件不满足时既不查缓存也不写缓存,方法每次都会真正执行。

unless vs condition

维度 condition unless
时机 执行前 + 执行后判断 仅执行后判断
语义 不满足则完全绕过缓存 不满足则不写缓存(但已读)
能否用 #result

典型场景:unless = "#result == null" 防缓存穿透写 null;unless = "#result.total < 1" 不缓存空结果。

sync ------ 防击穿

sync = true 时 Spring 会用 Cache#get(key, valueLoader),底层(如 Redis)加分布式锁,保证同一 key 并发只穿透一次。代价是不能与 unless 同时使用。

1.3 底层工作原理(一图胜千言)

复制代码
请求进入
   │
   ▼
Spring CachingInterceptor (AOP 代理)
   │
   ├─ 解析 cacheNames / key / condition
   │
   ├─ condition 为 false? ── 是 ──► 直接执行方法,返回(不碰缓存)
   │              │
   │              否
   │              ▼
   ├─ Cache.get(key)
   │       │
   │       命中 ──► 反序列化返回(方法不执行)
   │       │
   │       未命中
   │       ▼
   ├─ 执行目标方法
   │       │
   │       ▼
   ├─ unless 为 true? ── 是 ──► 返回结果(不写缓存)
   │              │
   │              否
   │              ▼
   └─ Cache.put(key, result) ──► 返回结果

核心实现类:CacheAspectSupportCacheInterceptor,通过 @EnableCaching 开启,底层由 AnnotationCacheOperationSource 解析注解元数据。


2. 实际项目中的具体应用(代码示例)

2.1 项目原貌

java 复制代码
@RestController
@RequestMapping("/app")
public class AppController {

    @Resource
    private AppService appService;

    @PostMapping("/good/list/page/vo")
    @Cacheable(
            value = "good_app_page",
            key = "T(com.yupi.yuaicodemother.utils.CacheKeyUtils).generateKey(#appQueryRequest)",
            condition = "#appQueryRequest.pageNum <= 10"
    )
    public BaseResponse<Page<AppVO>> listGoodAppVOByPage(
            @RequestBody AppQueryRequest appQueryRequest) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);
        long pageSize = appQueryRequest.getPageSize();
        ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");
        long pageNum = appQueryRequest.getPageNum();
        appQueryRequest.setPriority(AppConstant.GOOD_APP_PRIORITY);
        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        appVOPage.setRecords(appVOList);
        return ResultUtils.success(appVOPage);
    }
}

2.2 配套的缓存基础设施

java 复制代码
@Configuration
public class RedisCacheManagerConfig {

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public CacheManager cacheManager() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());

        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultConfig)
                // 精选应用列表单独 5 分钟
                .withCacheConfiguration("good_app_page",
                        defaultConfig.entryTtl(Duration.ofMinutes(5)))
                .build();
    }
}

启动类需开启缓存:

java 复制代码
@SpringBootApplication
@EnableCaching
public class YuAiCodeMotherApplication { ... }

2.3 缓存 Key 实际形态

最终落到 Redis 的 key 形如:

复制代码
good_app_page::d41d8cd98f00b204e9800998ecf8427e
           └─ 前缀(value + '::')      └─ MD5(请求体JSON)

TTL = 5 分钟,序列化为 JSON(取决于是否打开 value 序列化器配置)。


3. 优点与潜在缺点分析

3.1 优点

优点 说明
声明式、低侵入 业务方法保持纯净,缓存与业务解耦,一行注解搞定读写
SpEL 表达力强 #paramT()#result、逻辑运算符组合,能应对绝大多数 key/condition 场景
统一抽象 切换 Caffeine/Redis/Ehcache 只换 CacheManager,注解零改动
细粒度 TTL withCacheConfiguration 按命名空间配 TTL,精准控制生命周期
生态成熟 与 Spring Boot 自动配置、Actuator 指标、@CacheEvict/@CachePut 协同完整

3.2 潜在缺点

缺点 说明与示例
this 调用失效 同类内部方法互调不走代理,注解失效------AOP 老问题
Key 生成隐式 T(...).generateKey 隐藏在字符串里,重构类名时编译期不报错,运行时才发现 key 漂移
复杂条件难表达 多参数联合判断、依赖外部 Bean 的条件,SpEL 写起来很丑且难调试
缓存一致性弱 写操作未配 @CacheEvict 时会读到脏数据;注解本身无强一致性保证
无法细控异常 缓存层异常(Redis 挂)默认会向上抛,需 CacheErrorHandler 兜底,否则缓存故障拖垮业务
调试困难 命中与否、key 是什么,都"看不见",排查需连 Redis 手查
sync 限制 unless 互斥;某些实现(如早期版本)对 sync 支持不完整
序列化陷阱 本项目注释掉了 value 序列化器,意味着默认用 JDK 序列化------Page<AppVO> 必须 Serializable,否则报错
缓存穿透/击穿需额外处理 @Cacheable 默认不防穿透(null 会被 disableCachingNullValues 拒写)、sync 才防击穿

4. 三种可替代方案总览

为完整对比,我们用三种不同思路实现「与前 10 页缓存等价」的能力:

方案 核心思想 控制粒度 侵入性
方案 A 编程式缓存RedisTemplate 手动 get/put 最细 高(业务代码耦合)
方案 B AOP 自定义切面 :自定义注解 + @Around 低(注解 + 切面)
方案 C 多级缓存 + 装饰器模式:Caffeine(本地) + Redis(分布式) 中(封装层)

5. 替代方案的完整实现与对比

方案 A:编程式缓存(RedisTemplate)

思路:彻底放弃注解,在业务方法内显式 get/put,所有逻辑可视、可控。

java 复制代码
@Service
public class AppCacheService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private AppService appService;

    private static final String CACHE_PREFIX = "good_app_page::";
    private static final Duration TTL = Duration.ofMinutes(5);
    private static final long MAX_CACHE_PAGE = 10;

    @SuppressWarnings("unchecked")
    public BaseResponse<Page<AppVO>> listGoodAppVOByPage(AppQueryRequest req) {
        ThrowUtils.throwIf(req == null, ErrorCode.PARAMS_ERROR);
        ThrowUtils.throwIf(req.getPageSize() > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");

        // 1. 条件判断:仅前 10 页缓存
        boolean cacheable = req.getPageNum() <= MAX_CACHE_PAGE;
        String key = CACHE_PREFIX + CacheKeyUtils.generateKey(req);

        // 2. 查缓存(仅缓存范围内)
        if (cacheable) {
            Object cached = redisTemplate.opsForValue().get(key);
            if (cached != null) {
                return (BaseResponse<Page<AppVO>>) cached;
            }
        }

        // 3. 执行业务
        BaseResponse<Page<AppVO>> result = doQuery(req);

        // 4. 写缓存(空结果不写,防穿透可改写空占位)
        if (cacheable && result != null && result.getData() != null
                && result.getData().getTotalRow() > 0) {
            redisTemplate.opsForValue().set(key, result, TTL);
        }
        return result;
    }

    private BaseResponse<Page<AppVO>> doQuery(AppQueryRequest req) {
        long pageNum = req.getPageNum();
        req.setPriority(AppConstant.GOOD_APP_PRIORITY);
        QueryWrapper qw = appService.getQueryWrapper(req);
        Page<App> appPage = appService.page(Page.of(pageNum, req.getPageSize()), qw);
        Page<AppVO> voPage = new Page<>(pageNum, req.getPageSize(), appPage.getTotalRow());
        voPage.setRecords(appService.getAppVOList(appPage.getRecords()));
        return ResultUtils.success(voPage);
    }
}

为防 Redis 故障拖垮业务,加兜底:

java 复制代码
@Configuration
public class SafeRedisCacheErrorHandler extends CachingConfigurerSupport {
    @Override
    public CacheErrorHandler errorHandler() {
        return new CacheErrorHandler() {
            @Override public void handleCacheGetError(RuntimeException e, Cache c, Object k) { /* log */ }
            @Override public void handleCachePutError(RuntimeException e, Cache c, Object k, Object v) {}
            @Override public void handleCacheEvictError(RuntimeException e, Cache c, Object k) {}
            @Override public void handleCacheClearError(RuntimeException e, Cache c) {}
        };
    }
}

方案 B:AOP 自定义切面(自定义注解)

思路 :仿照 @Cacheable 写一个轻量注解 @SmartCache,用 @Around 统一处理,把"前 N 页才缓存"等规则参数化。

① 自定义注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SmartCache {
    String namespace();                         // 缓存命名空间
    String keyExpr();                           // SpEL key 表达式
    String conditionExpr() default "";          // SpEL 条件
    String unlessExpr() default "";             // SpEL unless
    long ttlSeconds() default 300;              // TTL
    boolean sync() default false;               // 是否同步
}
② 切面实现
java 复制代码
@Aspect
@Component
public class SmartCacheAspect {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private CacheExpressionEvaluator evaluator; // 封装 SpEL 解析

    @Around("@annotation(smartCache)")
    public Object around(ProceedingJoinPoint pjp, SmartCache smartCache) throws Throwable {
        MethodSignature sig = (MethodSignature) pjp.getSignature();
        Method method = sig.getMethod();
        Object[] args = pjp.getArgs();

        // 1. 解析 key 与 condition
        String key = smartCache.namespace() + "::"
                + evaluator.eval(smartCache.keyExpr(), method, args, String.class);
        boolean condition = smartCache.conditionExpr().isEmpty()
                || evaluator.eval(smartCache.conditionExpr(), method, args, Boolean.class);

        // 2. 条件不满足直接放行
        if (!condition) {
            return pjp.proceed();
        }

        // 3. 查缓存
        Object cached;
        try {
            cached = redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            // 缓存故障降级:直接执行业务,不向上抛
            return pjp.proceed();
        }
        if (cached != null) {
            return cached;
        }

        // 4. 执行业务(sync 时可在此处加 Redis 分布式锁)
        Object result = pjp.proceed();

        // 5. unless 判断
        boolean unless = !smartCache.unlessExpr().isEmpty()
                && evaluator.eval(smartCache.unlessExpr(), method, args, Boolean.class,
                        new HashMap<>(Map.of("#result", result)));
        if (!unless && result != null) {
            try {
                redisTemplate.opsForValue().set(key, result,
                        Duration.ofSeconds(smartCache.ttlSeconds()));
            } catch (Exception ignore) { /* 降级 */ }
        }
        return result;
    }
}
③ 使用方式
java 复制代码
@PostMapping("/good/list/page/vo")
@SmartCache(
    namespace = "good_app_page",
    keyExpr = "#appQueryRequest",
    conditionExpr = "#appQueryRequest.pageNum <= 10",
    unlessExpr = "#result.data.totalRow < 1",
    ttlSeconds = 300
)
public BaseResponse<Page<AppVO>> listGoodAppVOByPage(
        @RequestBody AppQueryRequest appQueryRequest) { ... }
④ SpEL 求值器(核心片段)
java 复制代码
@Component
public class CacheExpressionEvaluator {

    private final ExpressionParser parser = new SpelExpressionParser();
    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();

    @SuppressWarnings("unchecked")
    public <T> T eval(String expr, Method method, Object[] args, Class<T> retType) {
        return eval(expr, method, args, retType, Collections.emptyMap());
    }

    @SuppressWarnings("unchecked")
    public <T> T eval(String expr, Method method, Object[] args, Class<T> retType,
                      Map<String, Object> extraVars) {
        EvaluationContext ctx = new StandardEvaluationContext();
        String[] names = pnd.getParameterNames(method);
        if (names != null) {
            for (int i = 0; i < names.length; i++) {
                ctx.setVariable(names[i], args[i]);
            }
        }
        extraVars.forEach(ctx::setVariable);
        return parser.parseExpression(expr).getValue(ctx, retType);
    }
}

此方案本质是把 Spring Cache 抽象「再发明」一遍,但可定制任意规则(如基于用户角色动态 TTL、灰度缓存等)。


方案 C:多级缓存 + 装饰器模式(Caffeine + Redis)

思路:L1 用 Caffeine 进程内缓存(亚微秒),L2 用 Redis(跨实例共享),通过装饰器模式组合,兼具低延迟与一致性。

① 抽象缓存接口
java 复制代码
public interface MultiLevelCache {
    <T> T get(String key, Class<T> type);
    void put(String key, Object value, Duration ttl);
    void evict(String key);
}
② L1 本地缓存(Caffeine)
java 复制代码
@Component
public class CaffeineLevelCache implements MultiLevelCache {

    private final com.github.benmanes.caffeine.cache.Cache<String, CacheEntry> cache =
            Caffeine.newBuilder()
                    .maximumSize(10_000)
                    .expireAfterWrite(Duration.ofMinutes(1))  // 本地更短,保证尽快感知 Redis 更新
                    .recordStats()
                    .build();

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(String key, Class<T> type) {
        CacheEntry e = cache.getIfPresent(key);
        if (e == null || e.isExpired()) return null;
        return (T) e.value;
    }

    @Override
    public void put(String key, Object value, Duration ttl) {
        cache.put(key, new CacheEntry(value, System.currentTimeMillis() + ttl.toMillis()));
    }

    @Override
    public void evict(String key) { cache.invalidate(key); }

    private record CacheEntry(Object value, long expireAt) {
        boolean isExpired() { return System.currentTimeMillis() > expireAt; }
    }
}
③ L2 分布式缓存(Redis)+ 装饰器组合
java 复制代码
@Component
public class RedisLevelCache implements MultiLevelCache {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override @SuppressWarnings("unchecked")
    public <T> T get(String key, Class<T> type) {
        return (T) redisTemplate.opsForValue().get(key);
    }

    @Override
    public void put(String key, Object value, Duration ttl) {
        redisTemplate.opsForValue().set(key, value, ttl);
    }

    @Override
    public void evict(String key) { redisTemplate.delete(key); }
}
④ 装饰器:两级联动
java 复制代码
@Component
public class TwoLevelCache implements MultiLevelCache {

    @Resource private CaffeineLevelCache l1;
    @Resource private RedisLevelCache l2;

    @Override
    public <T> T get(String key, Class<T> type) {
        // 1. L1
        T v = l1.get(key, type);
        if (v != null) return v;
        // 2. L2
        v = l2.get(key, type);
        if (v != null) {
            l1.put(key, v, Duration.ofMinutes(1)); // 回填 L1
        }
        return v;
    }

    @Override
    public void put(String key, Object value, Duration ttl) {
        l2.put(key, value, ttl);
        l1.put(key, value, Duration.ofMinutes(1));
    }

    @Override
    public void evict(String key) {
        // 一致性:Redis 发布失效消息,各节点订阅后清 L1(此处略)
        l2.evict(key);
        l1.evict(key);
    }
}
⑤ 业务调用
java 复制代码
@Service
public class GoodAppQueryService {

    @Resource private TwoLevelCache cache;
    @Resource private AppService appService;

    public BaseResponse<Page<AppVO>> list(AppQueryRequest req) {
        if (req.getPageNum() > 10) return doQuery(req);
        String key = "good_app_page::" + CacheKeyUtils.generateKey(req);
        BaseResponse<Page<AppVO>> cached = cache.get(key, BaseResponse.class);
        if (cached != null) return cached;

        BaseResponse<Page<AppVO>> result = doQuery(req);
        if (result != null && result.getData() != null && result.getData().getTotalRow() > 0) {
            cache.put(key, result, Duration.ofMinutes(5));
        }
        return result;
    }
    // doQuery(...) 同方案 A
}

一致性保证:写操作触发 cache.evict(key),并通过 Redis Pub/Sub 广播让所有节点失效本地 L1,避免脏读。


5.x 横向对比

维度 @Cacheable(原方案) A. 编程式 B. AOP 切面 C. 多级缓存
读延迟(命中) Redis 一次 RTT(~1ms) 同左 同左 L1 命中 ~1μs,未命中同左
开发效率 ★★★★★ 一行注解 ★★ 显式繁琐 ★★★ 一次开发多次复用 ★★ 需基础设施
可读性 ★★★★ 业务纯净 ★★ 缓存与业务耦合 ★★★★ 注解清晰 ★★★ 需理解层级
可维护性 ★★★ 字符串 SpEL 易漂移 ★★★ 显式但散落 ★★★★ 集中在切面 ★★★★ 集中在缓存层
扩展性 ★★ 受注解能力上限 ★★★ 任意逻辑 ★★★★★ 可加灰度/降级/限流 ★★★★★ 可加更多层
缓存击穿防护 sync=true(受实现限制) 自行加锁,可控 切面统一加锁,可控 单飞模式(singleflight)可控
缓存穿透防护 unless + 占位 直接写空占位 切面统一 切面统一 + 布隆过滤器
缓存一致性 需配 @CacheEvict 手动 evict 切面统一 evict 多级 evict + Pub/Sub
故障降级 CacheErrorHandler try-catch 直接写 切面统一降级 各级 try-catch
跨实例共享 ✅ Redis ✅ Redis ✅ Redis ✅ Redis + 本地加速
本地命中性能 ✅ Caffeine
调试难度 ★★ 隐式 ★★★ 显式可断点 ★★★ 切面可断点 ★★★ 分层可观测
适合规模 中小项目 单点/特殊接口 中大型可定制项目 高并发大流量

6. 选型决策建议

不同业务场景的推荐路线:

场景 1:CRUD 后台管理系统、QPS < 1k

推荐:@Cacheable 原生注解

  • 理由:开发效率压倒一切,注解一行搞定;TTL 用 withCacheConfiguration 精控即可;
  • 注意:配 @CacheEvict 保证写后清缓存;配 CacheErrorHandler 防 Redis 故障传染。

场景 2:热点接口需精细控制(条件复杂、依赖外部 Bean、需灰度)

推荐:方案 B(AOP 自定义切面)

  • 理由:SpEL 写不下的复杂条件、动态 TTL、灰度缓存(如仅对部分用户缓存)都可参数化;
  • 适合:电商首页、推荐流、运营位等"规则多变"场景。

场景 3:超高并发读、单接口 QPS > 10k、可容忍秒级不一致

推荐:方案 C(多级缓存 Caffeine + Redis)

  • 理由:Redis RTT 在万级 QPS 下也是成本,本地缓存把延迟压到微秒;
  • 必须:配 Pub/Sub 或 Canal 订阅做 L1 失效,否则多节点脏读;
  • 适合:排行榜、热搜、精选列表(如本文 good_app_page 流量爆发时)。

场景 4:特殊接口、缓存逻辑与业务深度耦合(如依赖用户鉴权结果)

推荐:方案 A(编程式缓存)

  • 理由:逻辑全部显式,断点可调,最直观;
  • 代价:缓存代码散落,复用性差,仅用于个别接口。

通用决策树

复制代码
是否需要本地缓存加速? ── 是 ──► 方案 C 多级缓存
        │否
        ▼
缓存规则是否超过 @Cacheable 表达力?
        │是 ──► 方案 B AOP 切面
        │否
        ▼
是否需要缓存逻辑与业务强耦合/可调试?
        │是 ──► 方案 A 编程式
        │否 ──► @Cacheable 原生注解(首选)

通用工程实践补充

  1. Key 规范 :统一前缀 业务:实体:维度,避免命名空间冲突;本项目 good_app_page::md5 可读性略差,建议 app:good:page:md5
  2. TTL 加随机抖动TTL + random(0, 60s) 防止缓存雪崩同时失效。
  3. 空值占位:穿透高发接口对 null 写短 TTL(30s)的空标记。
  4. 监控 :Caffeine recordStats() + Redis INFO 接入 Prometheus,命中率低于 50% 就要反思 key 策略。
  5. 序列化 :生产环境推荐打开 GenericJackson2JsonRedisSerializer 并启用默认类型,比 JDK 序列化可读、跨语言友好(本项目注释掉了,是已知改进点)。
  6. this 调用陷阱 :注解失效首排查项;必要时用 AopContext.currentProxy() 或拆分 Service。
  7. 写后读一致性:先写 DB 再 evict 缓存(延迟双删更稳:evict → sleep → evict)。
相关推荐
开发者联盟league1 小时前
maven核心插件介绍
java·maven
编程令我快乐1 小时前
maven中deploy 的流程
java·maven
jaysee-sjc1 小时前
【JavaWeb】Tlias智能学习辅助系统|后端Web实战(登录认证)
java·开发语言·前端·学习·mybatis
南城以南溫暖如初1472 小时前
社区健身小程序开发流程详解:技术选型与实施经验分享
java·经验分享·spring boot·mysql·vue·apache
万维易源2 小时前
免费药品信息查询:用API 读懂常用药
java·前端·数据库·药品信息·药品查询·药品查询api
wear工程师2 小时前
Java 类初始化到底何时触发?别把加载、连接和 static 执行混在一起
java·jvm
hard_hot2 小时前
RabbitMQ异步下单架构设计
java·rabbitmq·java-rabbitmq
郑州光合科技余经理2 小时前
餐饮预定系统架构拆解:订单链路、权限组织与私有化源码交付
java·开发语言·前端·数据库·人工智能·系统架构·php
vx-程序开发2 小时前
django汽车租赁系统---附源码25360
java·javascript·spring boot·python·eclipse·django·php