宁武皇仁光九年锦文轩刻本《异闻录》载: 扶桑画师浅溪,居泰安,喜绘鲤。院前一方荷塘,锦鲤游曳,溪常与嬉戏。 其时正武德之乱,潘镇割据,战事频仍,魑魅魍魉,肆逆于道。兵戈逼泰安,街邻皆逃亡,独溪不舍锦鲤,未去。 是夜,院室倏火。有人入火护溪,言其本鲤中妖,欲取溪命,却生情愫,遂不忍为之。翌日天明,火势渐歇,人已不见。 溪始觉如梦,奔塘边,但见池水干涸,莲叶皆枯,塘中鲤亦不知所踪。 自始至终,未辨眉目,只记襟上层迭莲华,其色魅惑,似血着泪。 后有青岩居士闻之,叹曰:魑祟动情,必作灰飞。犹蛾之投火耳,非愚,乃命数也。 ————《锦鲤抄》

【SpringBoot实现两级缓存】

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。

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

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

posted @ 2023-07-31 19:53  哒布溜  阅读(502)  评论(0编辑  收藏  举报