首页 > 其他分享 >【SpringBoot实现两级缓存】

【SpringBoot实现两级缓存】

时间:2023-07-31 20:01:00浏览次数:65  
标签:缓存 SpringBoot 两级 redis WarningResult key warningResult public

spring boot中使用Caffeine + Redis实现二级缓存
1.依赖准备

首先确认Caffeine和redis这两者的依赖已导入(springboot版本为2.4.0):

<!-- redis与caffeine结合使用构成多级缓存 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
</dependency>
2.配置文件(application.yml部分)
server:
  port: 8087
spring:
  main:
    allow-bean-definition-overriding: true
  datasource:
    url: jdbc:mysql://123.45.67.890:3306/arknights?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    username: root
    password: yourpassword
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: 123.45.67.890
    port: ${SPRING_REDIS_PORT:6379}
    password: ${SPRING_REDIS_PASSWORD:saria}
    database: ${SPRING_REDIS_DATABASE:2}
    jedis:
      pool:
        max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
        max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
        max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
3.代码层

下面假设有个查询接口是对资产风险预警结果表的查询,然后我们逐步看:

本地缓存配置类:CacheManager.java, 这里面初始化cache管理器(有10s,60s,600s过期三种cache配置),设置过期时间和单位。


/**
 * 缓存管理配置
 * @author zoe
 */
@Slf4j
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager localCacheManager() {
        log.info("缓存实例化:no Primary");
        return initCacheManager();
    }

    private CacheManager initCacheManager() {
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        // 把各个cache注册到cacheManager中,Caffeine实现了org.springframework.cache.Cache接口
        simpleCacheManager.setCaches(Arrays.asList(
                // CacheBuilder构建多个cache
                new CaffeineCache(
                        // 定义cache名称:@Cacheable的cacheNames(等价value)属性要和此对应
                        "TIMEOUT_10S",
                        Caffeine.newBuilder()
                                // 设置缓存大小上限
                                .maximumSize(20000)
                                //参数:过期时长、单位
                                .expireAfterWrite(Duration.ofSeconds(10))
                                .build()
                ),
                new CaffeineCache(
                        "TIMEOUT_60S",
                        Caffeine.newBuilder()
                                .maximumSize(20000)
                                .expireAfterWrite(Duration.ofMinutes(1))
                                .build()
                ),
                new CaffeineCache(
                        "TIMEOUT_600S",
                        Caffeine.newBuilder()
                                .maximumSize(20000)
                                .expireAfterWrite(Duration.ofMinutes(10))
                                .build()
                )
        ));
        return simpleCacheManager;
    }
}

接下来是业务方法对应的接口:

public interface WarningResultService {

    /**
     * 查询预警结果
     *
     * @param warningResult
     * @return
     */
    WarningResult queryWarningResult(WarningResult warningResult);

    /**
     * 保存
     *
     * @param warningResult
     * @param key
     */
    void saveWarningResult(WarningResult warningResult, String key);

}

然后是业务接口实现类:

@Service
public class WarningResultServiceImpl implements WarningResultService {

    public static final Logger LOGGER = LoggerFactory.getLogger(WarningResultServiceImpl.class);
    private static final String cacheKey = "zoe:asset:";

    @Autowired
    private StringRedisTemplate redisTemplate;


    @Cacheable(cacheNames = "TIMEOUT_60S", cacheManager = "localCacheManager", key = "'zoe:asset:' + #warningResult.getWhichYear()+ #warningResult.getWhichMonth()")
    @Override
    public WarningResult queryWarningResult(WarningResult warningResult) {
        //判断redis中是否存在key为asset:result的数据
        int whichYear = warningResult.getWhichYear();
        int whichMonth = warningResult.getWhichMonth();
        String key = cacheKey + whichYear + whichMonth;
        // hasKey(key):判断是否有key所对应的值,有则返回true,没有则返回false
        if (!redisTemplate.hasKey(key)) {
            // 缓存中没有key则新增
            saveWarningResult(warningResult, key);
        } else {
            LOGGER.info("redis缓存命中");
        }
        String s = redisTemplate.opsForValue().get(key);
        LOGGER.info("数据返回给客户端");
        return JSONObject.parseObject(s, WarningResult.class);
    }

    @Override
    public void saveWarningResult(WarningResult warningResult, String key) {
        WarningResult returnDto = new WarningResult();
        int whichMonth = warningResult.getWhichMonth();
        if (!(whichMonth >= 0 && whichMonth <= 12)) {
            LOGGER.error("当前输入查询月份有误!");
            return;
        }
        LOGGER.info("redis未命中,查询mysql");
        // 模拟从数据库里获取数据封装DTO
        // 本月零金额或小额资产数
        returnDto.setThisMonthSmallCount(3000);
        // 本月新增资产数
        returnDto.setThisMonthSumCount(500);

        // 保存数据到缓存中
        // 调用fastJSON工具包进行序列化为JSONString格式
        String result = JSONObject.toJSONString(returnDto);
        // 设置360秒过期
        redisTemplate.opsForValue().set(key, result, 360, TimeUnit.SECONDS);
        LOGGER.info("更新二级缓存redis");
    }

}

controller方法:

    @ApiOperation(value = "查询预警结果(利用二级缓存)")
    @GetMapping("/warning-result-test")
    public WarningResult getWarningResultByMultiCache(WarningResult warningResult) {
        return assetsWarningResultService.queryWarningResult(warningResult);
    }

其实到这里就已经有二级缓存了,不过还是添加接口耗时打印看起来直观一些:利用AOP思想获取时间戳并相减可以获得时长。

耗时获取方法参考:https://www.cnblogs.com/architectforest/p/13357072.html

@Component
@Aspect
@Order(1)
public class CostTimeAspect {

    ThreadLocal<Long> startTime = new ThreadLocal<>();

    // 切点为刚刚的controller路径
    @Pointcut("execution(public * com.arknights.bot.api.controller.*.*(..))")
    private void pointcut() {}

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable{
        startTime.set(System.currentTimeMillis());
    }

    @AfterReturning(returning = "result" , pointcut = "pointcut()")
    public void doAfterReturning(Object result){
        System.out.println("aop前后方法耗时:毫秒数:"+ (System.currentTimeMillis() - startTime.get()));
    }

}
4.执行请求
http://localhost:8087/v1/risk-report/warning-result-test?whichMonth=6&whichYear=2023

返回结果(单次请求):
{
    "whichMonth": 0,
    "whichYear": 0,
    "thisMonthSmallCount": 3000,
    "thisMonthSumCount": 500
}
5.控制台打印
2023-07-31 12:57:32.419  INFO 24900 --- [nio-8087-exec-2] a.b.a.s.i.AssetsWarningResultServiceImpl : redis未命中,查询mysql
2023-07-31 12:57:32.490  INFO 24900 --- [nio-8087-exec-2] a.b.a.s.i.AssetsWarningResultServiceImpl : 更新二级缓存redis
2023-07-31 12:57:32.523  INFO 24900 --- [nio-8087-exec-2] a.b.a.s.i.AssetsWarningResultServiceImpl : 数据返回给客户端
aop前后方法耗时:毫秒数:1203
aop前后方法耗时:毫秒数:1
2023-07-31 12:59:05.264  INFO 24900 --- [nio-8087-exec-5] a.b.a.s.i.AssetsWarningResultServiceImpl : redis缓存命中
2023-07-31 12:59:05.296  INFO 24900 --- [nio-8087-exec-5] a.b.a.s.i.AssetsWarningResultServiceImpl : 数据返回给客户端
aop前后方法耗时:毫秒数:67

6.小结

​ 根据打印信息可以看到一共有三次请求,第一次请求调用没有走缓存,走的数据库查询,耗时1203ms;第二次直接走本地缓存,耗时1ms;根据之前设置,本地缓存60秒过期,redis360秒过期,所以一分钟后我进行第三次请求,则一级缓存未命中,走redis,然后返回,并更新一级缓存,耗时67ms。

​ 总结为:当用户获取数据时,先从一级缓存中获取数据,如果一级缓存有数据则返回数据,否则从二级缓存中获取数据。如果二级缓存中有数据则更新一级缓存,然后将数据返回客户端。如果二级缓存没有数据则去数据库查询数据,然后更新二级缓存,接着再更新一级缓存,最后将数据返回给客户端。

​ 思考:缓存的存在是为了优化程序响应时长,并不是用的越多越好,要根据实际业务需求取舍。

标签:缓存,SpringBoot,两级,redis,WarningResult,key,warningResult,public
From: https://www.cnblogs.com/dabuliu/p/17594146.html

相关文章

  • vscode创建springboot项目
    1、安装ExtensionPackforjava2、安装jdk11环境sudoaptinstallopenjdk-11-jdk-headless3、在项目目录下创建src/main/java/com/example/app.javaimportorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplicat......
  • springboot前后端分离
    1、可以分两个软件来写springboot跟vue吗2、springboot可以前后端分离吗3、springboot国际化(前后端分离情况)4、jsp和springboot属于前后端不分离吗可以分两个软件来写springboot跟vue吗简述:Springboot+Vue前后端分离项目部署springboot前后端分离,主要采用docker容器部......
  • [SpringBoot] Not registered via @EnableConfigurationPropertise or marked as Spri
    问题描述在Springboot进行参数绑定时,出现解决添加以下依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </depende......
  • SpringBoot SpringSecurity(四)短信验证码登录
    SpringSecurity默认只提供了账号密码的登录认证逻辑,所以要实现手机短信验证码登录认证功能,我们需要模仿SpringSecurity账号密码登录逻辑代码来实现一套自己的认证逻辑。通过下面步骤来完成短信验证码登录:1、短信验证码生成2、改造登录页3、添加短信验证码认证3.1定义SmsAu......
  • idea中SpringBoot项目module有红色下划线处理
    问题描述:项目打开时有的module会有红色下划线,点进去文件后又消失不见。项目启动时报错,无法启动。 解决方法:方法1.file->InvalidateCaches然后选择InvalidateandRestart(如果不管用的话可以√上ClearfilesystemcacheandLocalHistory再试一下,李粤说这个hisory挺......
  • 【SpringBoot】整合
    (知识目录)一、整合Junit使用@SpringBootTest注解可以定义测试类,要保证测试类在启动类在同一个包下,或者在启动类所处的类的子包下;如果不符合,要使用@SpringBootTest(classes=Demo3Application.class)下面通过service层和实现类模拟测试publicinterfaceUserService{......
  • Java面试题 P17:Redis篇:Redis使用场景-缓存-缓存穿透
    什么是缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把数据库压垮。  互斥锁代码:1privatefinalReadWriteLockreadWriteLock=newReentrantReadWriteLock();2privatefinalLock......
  • day02_springboot综合案例
    day02_springboot综合案例订单操作查询所有订单查询所有订单流程查询订单,要把订单对应的产品也要查出来Orders实体类@DatapublicclassOrders{privateStringid;privateStringorderNum;@DateTimeFormat(pattern="yyyy-MM-ddHH:mm")privateDateorderTi......
  • day01_springboot综合案例
    springboot项目课程目标1.【掌握】SSM整合2.【掌握】使用SSM完成查询3.【理解】AdminLTE4.【理解】理解SSM综合案例表的结构springboot环境搭建搭建工程pom.xml<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="htt......
  • 【微服务】springboot 多模块打包使用详解
    目录一、前言1.1为什么需要掌握多模块打包二、工程模块概述2.1前后端不分离2.2部署方式多样化2.3单模块向多模块演进三、单模块打包构建3.1环境准备3.1.1创建测试用的单模块工程3.1.2多环境配置文件3.1.3新增测试接口3.2pom配置详解3.2.1添加基础依赖3.2.2多环境配置3.2......