使用SpringAOP+Caffeine+Redis实现本地缓存与多级缓存

文章目录

  • 一、背景
  • 二、实现本地缓存
    • 1、定义注解
    • 2、切面
    • 3、缓存工具类
    • 4、测试
  • 三、实现多级缓存

一、背景

公司想对一些不经常变动的数据做一些本地缓存,我们使用AOP+Caffeine来实现。

但是只使用本地缓存,如果缓存失效,高并发下在重新加载缓存时,用户会出现时不时的“卡顿”现象。
如果使用非过期的缓存,还需要额外维护缓存一致性的问题。

本案例中采用二级缓存。L1缓存失效时间短,L2缓存失效时间长。请求优先从L1缓存获取数据,如果未命中,则加锁,保证只有一个线程去数据库中读取数据然后再更新到L1和L2中。然后其他线程依然在L2缓存获取数据。
这样可以保证,用户请求的数据都是来自于缓存,不会出现“卡顿”的现象。

多级缓存通常用于不怎么经常修改的数据、经常访问的数据比如说网站首页、商品详情页等。

我们一级缓存使用Caffeine,二级缓存使用Redis。

二、实现本地缓存

1、定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 本地缓存
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalCacheable {

    // 过期时间 默认10分钟
    long expired() default 600;

    // key创建器
    String keyGenerator() default "org.springframework.cache.interceptor.KeyGenerator";
}

2、切面


import com.google.gson.internal.LinkedTreeMap;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 本地缓存
 */
@Aspect
@Component
public class LocalCacheAspect {

    private static final String separator = ":";

    @Around("@annotation(com.framework.localcache.LocalCacheable)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        if (AopUtils.isAopProxy(point.getTarget())) {
            return point.proceed();
        }

        Method method = getMethodSignature(point).getMethod();
        if (method == null) {
            return point.proceed();
        }

        LocalCacheable annotation = method.getAnnotation(LocalCacheable.class);
        if (annotation == null) {
            return point.proceed();
        }

        // 生成key
        String key = generateKey(point);
//         System.out.println("生成的key:" + key);

        long expired = annotation.expired();

        Throwable[] throwable = new Throwable[1];
        Object proceed = LocalCache.cacheData(key, () -> {
            try {
                return point.proceed();
            } catch (Throwable e) {
                throwable[0] = e;
            }
            return null;
        }, expired);

        if (throwable[0] != null) {
            throw throwable[0];
        }

        return proceed;
    }

    /**
     * 获取方法
     */
    private MethodSignature getMethodSignature(ProceedingJoinPoint point) {
        Signature signature = point.getSignature();
        if (signature instanceof MethodSignature) {
            return ((MethodSignature) signature);
        }
        return null;
    }

    /**
     * 获取key
     */
    private String generateKey(ProceedingJoinPoint point) {

        // 目标类、方法、参数等
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(point.getTarget());
        Method method = getMethodSignature(point).getMethod();
        String[] parameterNames = getMethodSignature(point).getParameterNames();
        Object[] args = point.getArgs();

        // 解析参数,生成key
        LinkedTreeMap<String, Object> paramResolveResult = new LinkedTreeMap<>();
        if (ArrayUtils.isNotEmpty(args)) {
            for (int i = 0; i < args.length; i++) {
                resolveParam(args[i], paramResolveResult, parameterNames[i]);
            }
        }

        StringBuilder key = new StringBuilder(targetClass.getName() + separator + method.getName() + separator);
        paramResolveResult.forEach((k, v) -> {
            if (v != null) {
                key.append(k + "," + v + separator);
            }
        });

        // 根据方法名和参数生成唯一标识
        return key.toString();
    }


    private void resolveParam(Object param, Map<String, Object> paramResolveResult, String prefix) {
        if (param == null) {
            return;
        }
        Class<?> type = param.getClass();
        if (type == List.class) {
            List<Object> param0 = (List) param;
            for (int i = 0; i < param0.size(); i++) {
                resolveParam(param0.get(i), paramResolveResult, prefix + "[" + i + "]");
            }
        } else if (type == Map.class) {
            Map<Object, Object> param0 = (Map) param;
            param0.forEach((k, v) -> {
                resolveParam(v, paramResolveResult, prefix + "." + k);
            });
        } else if (type.isArray()) {
            Object[] param0 = (Object[]) param;
            for (int i = 0; i < param0.length; i++) {
                resolveParam(param0[i], paramResolveResult, prefix + "[" + i + "]");
            }
        } else if (type == Byte.class
                || type == Short.class
                || type == Integer.class
                || type == Long.class
                || type == Float.class
                || type == Double.class
                || type == Boolean.class
                || type == Character.class
                || type == String.class) {
            paramResolveResult.put(prefix, param);
        } else if (type.getName().startsWith("java.")) {

        } else {
            // 复杂类型
            Map<String, Object> fieldMap = new HashMap<>();
            // CGLIB代理
            if (Enhancer.isEnhanced(type)) {
                getAllFieldsAndValue(param, type.getSuperclass(), fieldMap);
            } else {
                getAllFieldsAndValue(param, type, fieldMap);
            }

            fieldMap.forEach((k, v) -> {
                if (v == null) {
                    return;
                }
                resolveParam(v, paramResolveResult, prefix + "." + k);
            });
        }
    }

    /**
     * 获取所有字段和值
     */
    private void getAllFieldsAndValue(Object o, Class type, Map<String, Object> fieldMap) {

        for (Method method : type.getMethods()) {
            if (method.getName().startsWith("get") && method.getParameterCount() == 0) {
                try {
                    Object value = method.invoke(o);
                    if (value != null) {
                        fieldMap.put(method.getName().substring(3), value);
                    }
                } catch (Exception e) {}
            }
        }
    }


}

3、缓存工具类

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 本地缓存
 */
public class LocalCache {

    private static final Map<Long, Cache<Object, Object>> cacheMap = new ConcurrentHashMap<>();

    /**
     * 创建本地缓存
     * @param seconds 过期时间:秒
     */
    private static Cache<Object, Object> createCache(long seconds) {
        return Caffeine.newBuilder()
                .expireAfterWrite(seconds, TimeUnit.SECONDS)
                .build();
    }

    /**
     * 创建本地缓存
     * @param seconds 过期时间:秒
     * @param loader 缓存方法
     */
    private Cache<Object, Object> createLoadingCache(long seconds, CacheLoader<Object, Object> loader) {
        return Caffeine.newBuilder()
                .expireAfterWrite(seconds, TimeUnit.SECONDS)
                .build(loader);
    }


    /**
     * 获取一个缓存组
     * @param seconds 缓存过期时间
     */
    private static Cache<Object, Object> getAndLoad(long seconds) {
        if (cacheMap.containsKey(seconds)) {
            return cacheMap.get(seconds);
        }

        Cache<Object, Object> cache = createCache(seconds);
        cacheMap.put(seconds, cache);

        return cache;
    }

    /**
     * 缓存数据,过期时间默认10分钟
     * @param key key
     * @param supplier 数据来源的方法
     */
    public static Object cacheData(Object key, Supplier<Object> supplier) {
        return cacheData(key, supplier, 600);
    }


    /**
     * 缓存数据
     * @param key key
     * @param supplier 数据来源的方法
     * @param seconds 过期时间:秒
     */
    public static Object cacheData(Object key, Supplier<Object> supplier, long seconds) {
        Assert.state(seconds > 0, "过期时间必须大于0秒");
        Cache<Object, Object> cache = getAndLoad(seconds);
        return cache.get(key, k -> supplier.get());
    }
}

4、测试

    @LocalCacheable
    @GetMapping("test1")
    public String test1() {
        System.out.println("执行了");
        return "success";
    }

    @LocalCacheable
    @GetMapping("test2")
    public String test2(String a) {
        System.out.println("执行了" + a);
        return "success";
    }

    @LocalCacheable
    @GetMapping("test3")
    public String test3(String a, int b, String c) {
        System.out.println("执行了" + a + b + c);
        return "success";
    }

    @LocalCacheable
    @GetMapping("test4")
    public String test4(UserInfo user) {
        System.out.println("执行了" + user);
        return "success";
    }


    @LocalCacheable
    @GetMapping("test5")
    public String test5(UserInfo[] users) {
        System.out.println("执行了" + users);
        return "success";
    }


    @LocalCacheable
    @GetMapping("test6")
    public String test6(List<UserInfo> users) {
        System.out.println("执行了" + users);
        return "success";
    }


    @LocalCacheable
    @GetMapping("test7")
    public String test7(UserInfo user) {
        System.out.println("执行了" + user.getMap());
        return "success";
    }

三、实现多级缓存

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 本地缓存
 *
 * @author cuixiangfei
 * @since 2024-03-18
 */
@Component
public class CaffeineCache {

    private static final Map<Long, Cache<String, String>> cacheMap = new ConcurrentHashMap<>();

    /**
     * 创建本地缓存
     * @param seconds 过期时间:秒
     */
    private static Cache<String, String> createCache(long seconds) {
        return Caffeine.newBuilder()
                .expireAfterWrite(seconds, TimeUnit.SECONDS)
                .build();
    }


    /**
     * 获取一个缓存组
     * @param seconds 缓存过期时间
     */
    private static Cache<String, String> getAndLoad(long seconds) {
        if (cacheMap.containsKey(seconds)) {
            return cacheMap.get(seconds);
        }

        Cache<String, String> cache = createCache(seconds);
        cacheMap.put(seconds, cache);

        return cache;
    }

    /**
     * 缓存数据,过期时间默认10分钟
     * @param key key
     * @param supplier 数据来源的方法
     */
    public String cacheData(String key, Supplier<String> supplier) {
        return cacheData(key, supplier, 600);
    }


    /**
     * 缓存数据
     * @param key key
     * @param supplier 数据来源的方法
     * @param seconds 过期时间:秒
     */
    public String cacheData(String key, Supplier<String> supplier, long seconds) {
        Assert.state(seconds > 0, "过期时间必须大于0秒");
        Cache<String, String> cache = getAndLoad(seconds);
        // 自动解决缓存击穿问题
        return cache.get(key, k -> supplier.get());
    }
}

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 缓存管理
 */
@Component
public class CacheManager {

    private final StringRedisTemplate redisTemplate;

    private final CaffeineCache caffeineCache;

    // 用于刷新缓存的线程池
    private final ThreadPoolTaskExecutor refreshCacheExecutor;

    @Autowired
    public CacheManager(StringRedisTemplate redisTemplate, CaffeineCache caffeineCache) {
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
        // 用于刷新缓存的线程池
        this.refreshCacheExecutor = new ThreadPoolTaskExecutor();
        this.refreshCacheExecutor.setCorePoolSize(10);
        this.refreshCacheExecutor.setMaxPoolSize(10);
        this.refreshCacheExecutor.setKeepAliveSeconds(60);
        this.refreshCacheExecutor.setThreadNamePrefix("refreshCache-");
        this.refreshCacheExecutor.setWaitForTasksToCompleteOnShutdown(true);
        this.refreshCacheExecutor.initialize();
    }

    /**
     * 多级缓存
     * @param key key
     * @param supplier 查询数据库的结果
     * @param localSeconds 本地缓存秒数
     * @param redisSeconds redis过期的秒数
     */
    public String multiCache(String key, Supplier supplier, long localSeconds, long redisSeconds) {
        return caffeineCache.cacheData(key, () -> redisCache(key, supplier, redisSeconds), localSeconds);
    }

    /**
     * 获取redis的缓存
     * @param key key
     * @param supplier 查询数据库的结果
     * @param seconds redis过期的秒数
     */
    private String redisCache(String key, Supplier supplier, long seconds) {
        String value = redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(value)) {
            System.out.println("获取到redis缓存" + key);
            // 异步查库,更新redis
            refreshCacheExecutor.execute(() -> {
                Object finalValue = supplier.get();
                if (finalValue != null) {
                    String jsonFinalValue = JSON.toJson(finalValue);
                    redisTemplate.opsForValue().set(key, jsonFinalValue, seconds, TimeUnit.SECONDS);
                } else {
                    // 解决缓存穿透问题
                    redisTemplate.opsForValue().set(key, "", seconds, TimeUnit.SECONDS);
                }
            });
            return value;
        }
        // 缓存不存在,需要查库(兜底方案)
        Object finalValue = supplier.get();
        System.out.println("redis不存在,查库");
        if (finalValue != null) {
            String jsonFinalValue = JSON.toJson(finalValue);
            redisTemplate.opsForValue().set(key, jsonFinalValue, seconds, TimeUnit.SECONDS);
            return jsonFinalValue;
        } else {
            // 解决缓存穿透问题
            redisTemplate.opsForValue().set(key, "", seconds, TimeUnit.SECONDS);
            return "";
        }
    }
}

@GetMapping("test1")
public String test1() {
    return cacheManager.multiCache("key1", () -> data.get("key1"), 5, 10);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/495786.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux:环境变量的特性及获取

目录 一、环境变量基本概念 1.1命令行参数 1.2常见环境变量 二、环境变量相关指令 创建本地变量 三、环境变量通常是具有全局属性的 一、环境变量基本概念 环境变量(environment variables)不是一个而是一堆&#xff0c;彼此之间其实没有关系。本质上是为了解决不同场景下…

【Java程序设计】【C00402】基于(JavaWeb)Springboot的新冠物资管理(含论文)

基于&#xff08;JavaWeb&#xff09;Springboot的新冠物资管理&#xff08;含论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千…

由浅到深认识Java语言(33):多线程

该文章Github地址&#xff1a;https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.c…

记一次 .NET某防伪验证系统 崩溃分析

一&#xff1a;背景 1. 讲故事 昨晚给训练营里面的一位朋友分析了一个程序崩溃的故障&#xff0c;因为看小伙子昨天在群里问了一天也没搞定&#xff0c;干脆自己亲自上阵吧&#xff0c;抓取的dump也是我极力推荐的用 procdump 注册 AEDebug 的方式&#xff0c;省去了很多沟通…

【Python BUG】CondaHTTPError解决记录

问题描述 CondaHTTPError: HTTP 429 TOO MANY REQUESTS for url https://mirrors.ustc.edu.cn/anaconda/pkgs/free/win-64/current_repodata.json Elapsed: 00:26.513315 解决方案 找到用户路径下的 .condarc文件&#xff0c;建议用这个方法前和我一样做个备份&#xff0c;方…

蛋糕店怎么弄一个微信小程序_开启蛋糕店新篇章

微信小程序&#xff0c;开启蛋糕店新篇章——甜蜜触手可及 在这个数字化、智能化的时代&#xff0c;微信小程序以其便捷、高效的特点&#xff0c;成为了众多商家与消费者之间的桥梁。对于蛋糕店而言&#xff0c;拥有一个专属的微信小程序&#xff0c;不仅可以提升品牌形象&…

家用超声波清洗机高端品牌推荐!4款值得入手的热门超声波清洗机

急着洗眼镜的朋友先不要慌&#xff0c;虽然洗眼镜是日常生活中最常见的操作&#xff0c;但是在清洗眼镜方面也是有讲究的&#xff0c;不是随随便便把眼镜擦一下就算清洁干净了&#xff01;因为我们拿眼镜布擦眼镜的时候&#xff0c;布料粗糙的微粒就会跟砂纸一样打磨着镜片&…

YOLOv5-小知识记录(一)

0. 写在前面 这篇博文主要是为了记录一下yolov5中的小的记忆点&#xff0c;方便自己查看和理解。 1. 完整过程 &#xff08;1&#xff09;Input阶段&#xff0c;图片需要经过数据增强Mosaic&#xff0c;并且初始化一组anchor预设&#xff1b; &#xff08;2&#xff09;特征提…

快递鸟物流轨迹地图API接口,包裹行程尽在掌握!

在快节奏的现代生活中&#xff0c;物流行业作为连接生产者与消费者的桥梁&#xff0c;其重要性不言而喻。随着电子商务的飞速发展&#xff0c;人们对物流信息的实时性和准确性要求越来越高。为了满足这一需求&#xff0c;快递鸟物流轨迹地图API应运而生&#xff0c;为广大用户提…

jsp将一个文本输入框改成下拉单选框,选项为字典表配置,通过后端查询

一&#xff0c;业务场景&#xff1a; 一个人员信息管理页面&#xff0c;原来有个最高学历是文本输入框&#xff0c;可以随意填写&#xff0c;现在业务想改成下拉单选框进行规范化&#xff0c;在专科及以下、本科、研究生三个选项中选择&#xff1b; 二&#xff0c;需要解决问…

职场中人如何做好时间管理提高工作效率?高效时间管理软件

在职场中&#xff0c;时间就是金钱&#xff0c;效率就是生命。面对繁杂的工作任务和紧迫的时间限制&#xff0c;做好时间管理显得尤为重要。只有合理规划时间&#xff0c;才能提高工作效率&#xff0c;从而在激烈的职场竞争中脱颖而出。 那么&#xff0c;职场中人如何做好时间…

mysql80-DBA数据库学习1-数据库安装

掌握能力 核心技能 核心技能 mysql部署 官网地址www.mysql.com 或者www.oracle.com https://dev.mysql.com/downloads/repo/yum/ Install the RPM you downloaded for your system, for example: yum install mysql80-community-release-{platform}-{version-number}.noarch…

大唐电信AC管理平台弱口令登录及信息泄露

大唐电信AC简介 大唐电信科技股份有限公司是电信科学技术研究院&#xff08;大唐电信科技产业集团&#xff09;控股的的高科技企业&#xff0c;大唐电信已形成集成电路设计、软件与应用、终端设计、移动互联网四大产业板块。 大唐电信AC集中管理平台存在弱口令及敏感信息泄漏漏…

如何在Windows通过eXtplorer结合cpolar搭建个人文件服务器并实现无公网ip远程访问

文章目录 1. 前言2. eXtplorer网站搭建2.1 eXtplorer下载和安装2.2 eXtplorer网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1. 前言 通过互联网传输文件&#xff0c;是互联网最重要的应用之一&#xff0c;无论是…

推特Twitter有直播功能吗?如何用Twitter直播?

现在各大直播平台已经成为社交媒体营销的一种重要渠道&#xff0c;它让品牌能够即时地与全球受众进行互动。据统计&#xff0c;直播市场正在迅速增长&#xff0c;预计到2028年将达到2230亿美元的规模。在这个不断扩张的市场中&#xff0c;许多社交媒体平台如YouTube、Facebook、…

【OS探秘】【虚拟化】【软件开发】【网络安全】在Windows11上安装Kali Linux虚拟机

一、所需原料 Windows 11主机、Oracle VM VirtualBox虚拟化平台、Kali Linux镜像文件 二、安装步骤 1、 在VBox管理器中&#xff0c;点击“新建”&#xff0c;进入向导模式&#xff0c;指定各个字段的值&#xff1a; 2、 安装完成&#xff0c;启动虚拟机&#xff1a; 3、 选择…

[linux初阶][vim-gcc-gdb] OneCharter: vim编辑器

一.vim编辑器基础 目录 一.vim编辑器基础 ①.vim的语法 ②vim的三种模式 ③三种模式的基本切换 ④各个模式下的一些操作 二.配置vim环境 ①手动配置(不推荐) ②自动配置(推荐) vim是vi的升级版,包含了更加丰富的功能. ①.vim的语法 vim [文件名] ②vim的三种模式 命令…

慧天[HTWATER]:采用CUDA框架实现耦合模型并行求解

慧天[HTWATER]软件简介 针对城市排水系统基础设施数据管理的需求&#xff0c;以及水文、水力及水质模拟对数据的需求&#xff0c;实现了以数据库方式对相应数据的存储。可以对分流制排水系统及合流制排水系统进行地表水文、管网水力、水质过程的模拟计算。可以对城市低影响开发…

CV论文--2024.3.28

1、Efficient Video Object Segmentation via Modulated Cross-Attention Memory 中文标题&#xff1a;通过调制交叉注意力记忆进行高效视频对象分割 简介&#xff1a;最近&#xff0c;基于Transformer的方法在半监督视频对象分割方面取得了出色的结果。然而&#xff0c;由于这…

【C++】手撕哈希表的闭散列和开散列

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;手撕哈希表的闭散列和开散列 > 毒鸡汤&#xff1a;谁不是一边受伤&#xff0c;一边学会坚强。 > 专栏选自&#xff1a;C嘎嘎进阶 > 望小伙伴们…