CoAlbum:多级缓存与性能对比

作者:RealmElysia日期:2025/10/29

目录

Target

1.多级缓存生效注解

2.缓存上下文

3.责任链

Hander接口

责任链初始化

5.切面Aop

Coalbum项目

使用缓存

性能对比

Jmeter tips


Target

Caffeine+Redis构建多级缓存,采用责任链模式,使用aop+注解的方式增强目标方法。若缓存命中,返回命中值。若没命中,执行目标方法,将目标方法返回值依次存入缓存链。

1.多级缓存生效注解

1/**
2 * 多级缓存生效注解
3 */
4@Target(ElementType.METHOD)
5@Retention(RetentionPolicy.RUNTIME)
6public @interface CacheableMultiLevel {
7
8    String keyPrefix() default "";// key 前缀
9    String key() default "";  // 缓存key,空key则计算hash
10    int localExpireSeconds() default 60; // 本地缓存过期时间(秒)
11    int redisExpireSeconds() default 300; // redis缓存过期时间(秒)
12    
13}

2.缓存上下文

1/**
2 * 通用缓存上下文:CacheContext
3 */
4@Data
5public class CacheContext {
6
7    private final String cacheKey;  // 当前查询的缓存 key
8    private String cachedValue;     // 当前 handler 返回的缓存值,命中则填充
9    private boolean hit = false;    // 是否命中缓存
10
11    public void setCachedValue(String cachedValue) {
12        this.cachedValue = cachedValue;
13        this.hit = true;
14    }
15}

3.责任链

Hander接口

Caffeine忽略expireSeconds,无法指定各key的过期时间

RedisCacheHandler 设置expireSeconds+ RandomUtil.randomInt(0, 300) 防止缓存雪崩

put方法中 应链式调用 下级缓存的 put 方法

1/**
2 * 责任链接口:CacheHandler
3 */
4public interface CacheHandler {
5    /**
6     * 尝试从当前缓存层获取数据
7     * @param context 缓存上下文,包含 key 和结果
8     * @return 如果命中缓存,设置 context.cachedValue 并返回 true;否则返回 false
9     */
10    boolean handle(CacheContext context);
11
12    void setNext(CacheHandler next);
13
14    void put(String key, String jsonValue, int expireSeconds);
15}
16

责任链初始化

1@Component
2public class CacheHandlerChain {
3
4    @Autowired(required = false)
5    private LocalCacheHandler localCacheHandler;
6
7    @Autowired(required = false)
8    private RedisCacheHandler redisCacheHandler;
9
10    @Autowired(required = false)
11    private DefaultCacheHandler defaultCacheHandler;
12
13    @Getter
14    private CacheHandler chain;
15
16    @PostConstruct
17    public void init() {
18        List<CacheHandler> handlers = new ArrayList<>();
19        if (localCacheHandler != null) handlers.add(localCacheHandler);
20        if (redisCacheHandler != null) handlers.add(redisCacheHandler);
21        if (defaultCacheHandler != null) handlers.add(defaultCacheHandler);
22
23        for (int i = 0; i < handlers.size() - 1; i++) {
24            handlers.get(i).setNext(handlers.get(i + 1));
25        }
26        this.chain = handlers.isEmpty() ? defaultCacheHandler : handlers.get(0);
27    }
28
29}

5.切面Aop

优先使用注解中指定的key,为指定则计算方法参数的hash 作为key

  1. 命中,将json字符串反序列化为returnType类型的对象
  2. 未命中,目标方法执行后,将返回对象转为json字符串存入链中的各级缓存

TODO:解决缓存击穿,避免缓存突然失效导致 ES 压力飙升

1@Aspect
2@Component
3public class CacheableAspect {
4
5    @Autowired
6    private CacheHandlerChain cacheHandlerChain;
7
8    @Around("@annotation(cacheableMultiLevel)")
9    public Object around(ProceedingJoinPoint joinPoint, CacheableMultiLevel cacheableMultiLevel) throws Throwable {
10        String key = cacheableMultiLevel.key();
11        if(StrUtil.isBlank(key)){
12            //未指定key,使用hash值作为key
13            Object[] args = joinPoint.getArgs();
14            String argJson = JSON.toJSONString(args);
15            key = DigestUtils.md5DigestAsHex(argJson.getBytes());
16        }
17        String cacheKey = cacheableMultiLevel.keyPrefix()+key;
18
19        CacheContext context = new CacheContext(cacheKey);
20        CacheHandler chain = cacheHandlerChain.getChain();
21
22        boolean hit = chain.handle(context);
23        // 获取方法签名
24        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
25
26        // 获取目标方法对象
27        java.lang.reflect.Method method = signature.getMethod(); // 或 getDeclaredMethod()
28        Class<?> returnType = method.getReturnType();
29
30        if (hit) {
31            System.out.println("[AOP] 缓存命中,直接返回");
32            return JSON.parseObject(context.getCachedValue(),returnType); // 命中则直接返回
33        }
34
35        // 未命中,放行目标方法,由方法自己查数据源并写入缓存
36        System.out.println("[AOP] 缓存未命中,放行目标方法");
37        Object res = joinPoint.proceed();
38        String json = JSON.toJSONString(res);
39        chain.put(cacheKey,json,cacheableMultiLevel.redisExpireSeconds());
40        return res;
41    }
42
43}

Coalbum项目

使用缓存

主页图片分页查询

1    /**
2     * ES作为数据源
3     */
4    @PostMapping("/list/page/vo/search")
5    @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
6    @CacheableMultiLevel(keyPrefix = "coalbum:listPictureVOByPage:",redisExpireSeconds =300)
7    public BaseResponse<Page<PictureVO>> listPictureVOByPageFromES(@RequestBody PictureQueryRequest pictureQueryRequest) {
8
9        try {
10            //TODO 权限校验
11            //TODO 解决缓存击穿,避免缓存突然失效导致 ES 压力飙升
12            return ResultUtils.success(pictureSearchService.searchPicture(pictureQueryRequest));
13        } catch (IOException e) {
14            throw new BusinessException(ErrorCode.SYSTEM_ERROR);
15        }
16    }

性能对比

线程数:50 ;second:1

ES直查:平均响应时间 30 ms

使用多级缓存:平均接口响应时间 14 ms

性能提升50%

Jmeter tips

请求头 携带cookies


CoAlbum:多级缓存与性能对比》 是转载文章,点击查看原文


相关推荐


为超过10亿条记录的订单表新增字段
Sam_Deep_Thinking2025/10/26

如何为 10 亿行 MySQL 大表安全新增字段? 之前在一家餐饮公司待过几年。由于这家公司已经营业了很多年,MySQL 5.6 的订单表数据早已超过 10 亿行。这就带来一个经典难题: 如何为这张大表新增字段? 当时的处理方式 假设业务需求的上线时间是 2025 年 10 月 23 日,我们会安排组内同事在 10 月 22 日或 23 日凌晨 提交数据库变更: ALTER TABLE order_items ADD COLUMN new_flag TINYINT DEFAULT 0; 变


C 语言标准库头文件 time.h
hubenchang05152025/10/23

#C 语言标准库头文件 time.h 请查看 C 语言标准库头文件列表 了解更多相关 API。 这个头文件提供 线程 相关的功能。例如线程控制、互斥量、条件变量、线程局部存储等。 #常量 宏标准说明CLOCKS_PER_SECC89处理器的每秒时钟数,POSIX 上定义为 1000000 而非实际的时钟数TIME_UTCC11表示时间基准为 UTC 时间 #类型 类型标准说明time_tC89日历时间的时间戳类型(自 1970 年 1 月 1 日 00:00:00.000 UTC 以来的秒数


Redis(77)Redis缓存的优点和缺点是什么?
Victor3562025/10/22

Redis作为一个高性能的分布式缓存系统,具有许多优点,但也有一些缺点。以下详细讲解Redis缓存的优点和缺点,并结合代码说明如何尽量发挥其优点和规避其缺点。 优点 1. 高性能 Redis以其卓越的读写性能著称,能够支持高吞吐量的读写操作。 示例代码: import redis.clients.jedis.Jedis; public class RedisPerformanceExample { public static void main(String[] args) {


FFmpeg 基本数据结构 AVFormatConext 分析
给大佬递杯卡布奇诺2025/10/21

1、FFmpeg 主要数据结构分层设计 1.1 IO抽象层 协议层与 I/O 抽象层 (Protocol & I/O Abstraction),这一层负责从最广泛的数据源读取或写入数据。 核心数据结构:AVIOContext 功能: 抽象了底层的 I/O 操作。通过它,FFmpeg 可以用统一的接口处理文件、网络流(HTTP, RTMP, TCP)、内存缓冲区等。 关键点: 它使得上层的格式层(解复用)无需关心数据是从哪里来的。这对于播放网络直播流或处理内存中的媒体数据至关重


SpringCloud微服务项目实战——系统实现篇
thginWalker2025/10/20

06 服务多不易管理如何破——服务注册与发现 经过上一篇系统性的介绍 Spring Cloud 及 Spring Cloud Alibaba 项目,相信你已经对这两个项目有个整体直观的感受,本篇开始正式进入本课程的第二部分,一起进入业务的开发阶段。 服务调用问题 在分析业务需求时,其中有个简单的功能点:会员可以开通月卡,开通月卡的同时,需要增加相应的积分。开通月卡功能在会员服务模块维护,但增加积分功能在积分服务模块维护,这就涉及到两个模块间的服务调用问题。 单实例情况:可以采用点对点的


linux系统jdk&&mysql配置
阑梦清川2025/10/18

使用ubuntu进行举例说明: 更新软件包 sudo apt update 安装JDK sudo apt install openjdk-17-jdk 关于mysql, #查找安装包 apt list |grep "mysql-server" #安装mysql sudo apt install mysql-server 查看数据库的状态: sudo systemctl status mysql 如果遇到问题,参考下面的这个: 这个主要是刷新权限表,然后修改我们的密码,否则会报错


【机器学习入门】8.1 降维的概念和意义:一文读懂降维的概念与意义 —— 从 “维度灾难” 到低维嵌入
做科研的周师兄2025/10/17

对于刚入门机器学习的同学来说,“高维数据” 是很容易遇到的痛点 —— 比如处理包含几十甚至上百个特征的数据集时,不仅训练速度变慢,模型还可能因为 “维度太多” 出现泛化能力下降的问题。而 “降维” 正是解决高维数据困境的核心技术。今天我们就从基础概念出发,拆解 “维度灾难” 的危害、降维的本质,以及经典的低维嵌入方法,帮你彻底理解降维为什么重要、到底在做什么。 一、先搞懂:什么是 “维度”?为什么会有 “维度灾难”? 在学习降维前,我们需要先明确 “维度” 的定义,以及高维数据会带来的核心问


【鸿蒙生态共建】鸿蒙6适配-API变化与兼容(2.UI交互与基础能力篇)--《精通HarmonyOS NEXT :鸿蒙App开发入门与项目化实战》读者福利
俩毛豆2025/10/15

HarmonyOS开发者版本6.0.0(API 20)Release(鸿蒙6) 在2005年9月25日正式发布。相对于HarmonyOS 5.0版本增加了多项新能力及对已有能力的优化。同时也有部分API进行了变更。 本篇内容是《精通HarmonyOS NEXT :鸿蒙App开发入门与项目化实战》这本书整体内容的延续,是咱这本书读者的福利,内容以本书的示例作为基础,适配鸿蒙6过程中,所需要完成的调整。并将这些调整记录分享给大家,本篇内容主要介绍与UI交互与基础能力的鸿蒙6兼容适配。 打


2025年下半年加密货币市场展望:机遇与挑战并存
终端域名2025/10/14

一、核心机遇:技术融合与市场结构升级 现实世界资产(RWA)代币化加速 市场规模爆发:截至2025年6月,链上RWA总价值已超238亿美元(不含稳定币),涵盖私人信贷、美国国债、大宗商品等领域。贝莱德预测,到2030年RWA市场规模将飙升至16万亿美元,成为DeFi与传统金融融合的关键桥梁。生态主体壮大:发行机构、托管银行、投资者、区块链平台等主体快速聚集,推动资产运营、跨境支付等领域投资机遇。例如,高盛推出RWA平台,某欧洲奢侈品牌通过NFT分割限量版手表所有权,拓宽市场覆盖面。技术支撑


Redis(59)Redis的主从复制是如何实现的?
Victor3562025/10/13

Redis 的主从复制(Master-Slave Replication)是一种数据复制机制,通过它可以将一个 Redis 实例的数据复制到一个或多个从节点。这种机制可以用于数据备份、读写分离、灾难恢复等场景。以下是 Redis 主从复制的详细实现过程及其代码示例。 主从复制原理 主节点(Master):负责处理写操作(SET、DEL 等),并将数据变化同步到从节点。 从节点(Slave):负责处理读操作(GET 等),从主节点接收数据变化。 同步过程: 初次同步:从节点连接到主节点,主节点

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0