问题

上一篇Spring Boot Cache + redis 设置有效时间和自动刷新缓存,时间支持在配置文件中配置,说了一种时间方式,直接扩展注解的Value值,如:


@Override
@Cacheable(value = "people#${select.cache.timeout:1800}#${select.cache.refresh:600}", key = "#person.id", sync = true)
public Person findOne(Person person, String a, String[] b, List<Long> c) {Person p = personRepository.findOne(person.getId());System.out.println("为id、key为:" + p.getId() + "数据做了缓存");System.out.println(redisTemplate);return p;
}

但是这种方式有一个弊端就是破坏了原有Spring Cache架构,导致如果后期想换缓存就会去改很多代码。

解决思路

RedisCacheManager可以在配置CacheManager的Bean的时候指定过期时间,如:


@Bean
public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);// 开启使用缓存名称最为key前缀redisCacheManager.setUsePrefix(true);//这里可以设置一个默认的过期时间 单位是秒redisCacheManager.setDefaultExpiration(redisDefaultExpiration);// 设置缓存的过期时间Map<String, Long> expires = new HashMap<>();expires.put("people", 1000);redisCacheManager.setExpires(expires);return redisCacheManager;
}

我们借鉴一下redisCacheManager.setExpires(expires)思路,进行扩展。直接新建一个CacheTime类,来存过期时间和自动刷新时间。

在RedisCacheManager调用getCache(name)获取缓存的时候,当没有找到缓存的时候会调用getMissingCache(String cacheName)来新建缓存。在新建缓存的时候我们可以在扩展的Map<String, CacheTime> cacheTimes里面根据key获取CacheTime进而拿到有效时间和自动刷新时间。

具体实现

我们先新建CacheTime类

CacheTime:


 /*** @author yuhao.wang*/
public class CacheTime {public CacheTime(long preloadSecondTime, long expirationSecondTime) {this.preloadSecondTime = preloadSecondTime;this.expirationSecondTime = expirationSecondTime;}/*** 缓存主动在失效前强制刷新缓存的时间* 单位:秒*/private long preloadSecondTime = 0;/*** 缓存有效时间*/private long expirationSecondTime;public long getPreloadSecondTime() {return preloadSecondTime;}public long getExpirationSecondTime() {return expirationSecondTime;}
}

扩展一下RedisCache类

和上一篇的CustomizedRedisCache类一样,主要解决:

  • 获取缓存的在大并发下的一个bug,详情
  • 在获取缓存的时候判断一下缓存的过期时间和自动刷新时间,根据这个值去刷新缓存

CustomizedRedisCache:


/*** 自定义的redis缓存** @author yuhao.wang*/
public class CustomizedRedisCache extends RedisCache {private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCache.class);private CacheSupport getCacheSupport() {return SpringContextUtils.getBean(CacheSupport.class);}private final RedisOperations redisOperations;private final byte[] prefix;/*** 缓存主动在失效前强制刷新缓存的时间* 单位:秒*/private long preloadSecondTime = 0;/*** 缓存有效时间*/private long expirationSecondTime;public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, long preloadSecondTime) {super(name, prefix, redisOperations, expiration);this.redisOperations = redisOperations;// 指定有效时间this.expirationSecondTime = expiration;// 指定自动刷新时间this.preloadSecondTime = preloadSecondTime;this.prefix = prefix;}public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, long preloadSecondTime, boolean allowNullValues) {super(name, prefix, redisOperations, expiration, allowNullValues);this.redisOperations = redisOperations;// 指定有效时间this.expirationSecondTime = expiration;// 指定自动刷新时间this.preloadSecondTime = preloadSecondTime;this.prefix = prefix;}/*** 重写get方法,获取到缓存后再次取缓存剩余的时间,如果时间小余我们配置的刷新时间就手动刷新缓存。* 为了不影响get的性能,启用后台线程去完成缓存的刷。* 并且只放一个线程去刷新数据。** @param key* @return*/@Overridepublic ValueWrapper get(final Object key) {RedisCacheKey cacheKey = getRedisCacheKey(key);String cacheKeyStr = getCacheKey(key);// 调用重写后的get方法ValueWrapper valueWrapper = this.get(cacheKey);if (null != valueWrapper) {// 刷新缓存数据refreshCache(key, cacheKeyStr);}return valueWrapper;}/*** 重写父类的get函数。* 父类的get方法,是先使用exists判断key是否存在,不存在返回null,存在再到redis缓存中去取值。这样会导致并发问题,* 假如有一个请求调用了exists函数判断key存在,但是在下一时刻这个缓存过期了,或者被删掉了。* 这时候再去缓存中获取值的时候返回的就是null了。* 可以先获取缓存的值,再去判断key是否存在。** @param cacheKey* @return*/@Overridepublic RedisCacheElement get(final RedisCacheKey cacheKey) {Assert.notNull(cacheKey, "CacheKey must not be null!");// 根据key获取缓存值RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));// 判断key是否存在Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection connection) throws DataAccessException {return connection.exists(cacheKey.getKeyBytes());}});if (!exists.booleanValue()) {return null;}return redisCacheElement;}/*** 刷新缓存数据*/private void refreshCache(Object key, String cacheKeyStr) {Long ttl = this.redisOperations.getExpire(cacheKeyStr);if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {// 尽量少的去开启线程,因为线程池是有限的ThreadTaskUtils.run(new Runnable() {@Overridepublic void run() {// 加一个分布式锁,只放一个请求去刷新缓存RedisLock redisLock = new RedisLock((RedisTemplate) redisOperations, cacheKeyStr + "_lock");try {if (redisLock.lock()) {// 获取锁之后再判断一下过期时间,看是否需要加载数据Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {// 通过获取代理方法信息重新加载缓存数据CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(), cacheKeyStr);}}} catch (Exception e) {logger.info(e.getMessage(), e);} finally {redisLock.unlock();}}});}}public long getExpirationSecondTime() {return expirationSecondTime;}/*** 获取RedisCacheKey** @param key* @return*/public RedisCacheKey getRedisCacheKey(Object key) {return new RedisCacheKey(key).usePrefix(this.prefix).withKeySerializer(redisOperations.getKeySerializer());}/*** 获取RedisCacheKey** @param key* @return*/public String getCacheKey(Object key) {return new String(getRedisCacheKey(key).getKeyBytes());}
}

扩展RedisCacheManager

主要扩展通过getCache(String name)方法获取缓存的时候,当没有找到缓存回去调用getMissingCache(String cacheName)来新建缓存。

CustomizedRedisCacheManager:


/*** 自定义的redis缓存管理器* 支持方法上配置过期时间* 支持热加载缓存:缓存即将过期时主动刷新缓存** @author yuhao.wang*/
public class CustomizedRedisCacheManager extends RedisCacheManager {private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class);/*** 父类dynamic字段*/private static final String SUPER_FIELD_DYNAMIC = "dynamic";/*** 父类cacheNullValues字段*/private static final String SUPER_FIELD_CACHENULLVALUES = "cacheNullValues";RedisCacheManager redisCacheManager = null;// 0 - never expireprivate long defaultExpiration = 0;private Map<String, CacheTime> cacheTimes = null;public CustomizedRedisCacheManager(RedisOperations redisOperations) {super(redisOperations);}public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {super(redisOperations, cacheNames);}public RedisCacheManager getInstance() {if (redisCacheManager == null) {redisCacheManager = SpringContextUtils.getBean(RedisCacheManager.class);}return redisCacheManager;}/*** 获取过期时间** @return*/public long getExpirationSecondTime(String name) {if (StringUtils.isEmpty(name)) {return 0;}CacheTime cacheTime = null;if (!CollectionUtils.isEmpty(cacheTimes)) {cacheTime = cacheTimes.get(name);}Long expiration = cacheTime != null ? cacheTime.getExpirationSecondTime() : defaultExpiration;return expiration < 0 ? 0 : expiration;}/*** 获取自动刷新时间** @return*/private long getPreloadSecondTime(String name) {// 自动刷新时间,默认是0CacheTime cacheTime = null;if (!CollectionUtils.isEmpty(cacheTimes)) {cacheTime = cacheTimes.get(name);}Long preloadSecondTime = cacheTime != null ? cacheTime.getPreloadSecondTime() : 0;return preloadSecondTime < 0 ? 0 : preloadSecondTime;}/*** 创建缓存** @param cacheName 缓存名称* @return*/public CustomizedRedisCache getMissingCache(String cacheName) {// 有效时间,初始化获取默认的有效时间Long expirationSecondTime = getExpirationSecondTime(cacheName);// 自动刷新时间,默认是0Long preloadSecondTime = getPreloadSecondTime(cacheName);logger.info("缓存 cacheName:{},过期时间:{}, 自动刷新时间:{}", cacheName, expirationSecondTime, preloadSecondTime);// 是否在运行时创建CacheBoolean dynamic = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_DYNAMIC);// 是否允许存放NULLBoolean cacheNullValues = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_CACHENULLVALUES);return dynamic ? new CustomizedRedisCache(cacheName, (this.isUsePrefix() ? this.getCachePrefix().prefix(cacheName) : null),this.getRedisOperations(), expirationSecondTime, preloadSecondTime, cacheNullValues) : null;}/*** 根据缓存名称设置缓存的有效时间和刷新时间,单位秒** @param cacheTimes*/public void setCacheTimess(Map<String, CacheTime> cacheTimes) {this.cacheTimes = (cacheTimes != null ? new ConcurrentHashMap<String, CacheTime>(cacheTimes) : null);}/*** 设置默认的过去时间, 单位:秒** @param defaultExpireTime*/@Overridepublic void setDefaultExpiration(long defaultExpireTime) {super.setDefaultExpiration(defaultExpireTime);this.defaultExpiration = defaultExpireTime;}@Deprecated@Overridepublic void setExpires(Map<String, Long> expires) {}
}

在Config中配置RedisCacheManager


@Bean
public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);// 开启使用缓存名称最为key前缀redisCacheManager.setUsePrefix(true);//这里可以设置一个默认的过期时间 单位是秒redisCacheManager.setDefaultExpiration(redisDefaultExpiration);// 设置缓存的过期时间和自动刷新时间Map<String, CacheTime> cacheTimes = new HashMap<>();cacheTimes.put("people", new CacheTime(selectCacheTimeout, selectCacheRefresh));cacheTimes.put("people1", new CacheTime(120, 115));cacheTimes.put("people2", new CacheTime(120, 115));redisCacheManager.setCacheTimess(cacheTimes);return redisCacheManager;
}

剩余的创建切面来缓存方法信息请看上篇

源码地址:
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-cache-redis-2 工程

为监控而生的多级缓存框架 layering-cache这是我开源的一个多级缓存框架的实现,如果有兴趣可以看一下。

Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存-2相关推荐

  1. Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存

    时间方式,直接扩展注解的Value值,如: @Override @Cacheable(value = "people#${select.cache.timeout:1800}#${selec ...

  2. Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存,时间支持在配置文件中配置

    问题描述 Spring Cache提供的@Cacheable注解不支持配置过期时间,还有缓存的自动刷新. 我们可以通过配置CacheManneg来配置默认的过期时间和针对每个缓存容器(value)单独 ...

  3. Spring Boot之基于Redis实现MyBatis查询缓存解决方案

    转载自 Spring Boot之基于Redis实现MyBatis查询缓存解决方案 1. 前言 MyBatis是Java中常用的数据层ORM框架,笔者目前在实际的开发中,也在使用MyBatis.本文主要 ...

  4. STS创建Spring Boot项目实战(Rest接口、数据库、用户认证、分布式Token JWT、Redis操作、日志和统一异常处理)

    STS创建Spring Boot项目实战(Rest接口.数据库.用户认证.分布式Token JWT.Redis操作.日志和统一异常处理) 1.项目创建 1.新建工程 2.选择打包方式,这边可以选择为打 ...

  5. Vue + Spring Boot 项目实战(二十一):缓存的应用

    重要链接: 「系列文章目录」 「项目源码(GitHub)」 本篇目录 前言 一.缓存:工程思想的产物 二.Web 中的缓存 1.缓存的工作模式 2.缓存的常见问题 三.缓存应用实战 1.Redis 与 ...

  6. Spring Boot Serverless 实战系列“架构篇” | 光速入门函数计算

    作者 |:西流(阿里云函数计算专家) Spring Boot 是基于 Java Spring 框架的套件,它预装了 Spring 一系列的组件,开发者只需要很少的配置即可创建独立运行的应用程序. 在云 ...

  7. Spring Boot Serverless 实战系列“架构篇” 首发 | 光速入门函数计算

    简介:如何以 Serverless 的方式运行 Spring Boot 应用? 作者 | 西流(阿里云函数计算专家) Spring Boot 是基于 Java Spring 框架的套件,它预装了 Sp ...

  8. Spring Boot Serverless 实战系列“架构篇”首发 | 光速入门函数计算

    作者 | 西流(阿里云函数计算专家) Spring Boot 是基于 Java Spring 框架的套件,它预装了 Spring 一系列的组件,开发者只需要很少的配置即可创建独立运行的应用程序. ​ ...

  9. Spring Boot中使用Redis数据库

    Spring Boot中除了对常用的关系型数据库提供了优秀的自动化支持之外,对于很多NoSQL数据库一样提供了自动化配置的支持,包括:Redis, MongoDB, Elasticsearch, So ...

最新文章

  1. 二叉树两个结点的最低公共父结点 【微软面试100题 第七十五题】
  2. Windows7 支付宝证书安装方法
  3. CSS选择器学习笔记
  4. 九九乘法表Python+Java
  5. super在python中有什么用
  6. 三星Galaxy Note10系列带壳渲染图曝光:将取消3.5mm耳机孔
  7. c#泛型的使用性能测试
  8. 前端 视频标签 video的一些特殊属性详解
  9. UDP传输 TCP传输
  10. c语言输入一个数判断是否是同构数,c语言:编写函数判断x是否同构数
  11. hdu5336XYZ and Drops
  12. Developer Test-Jquery-常用技巧
  13. java中的保护(protected)修饰符的理解
  14. 软件测试-18个功能测试点总结
  15. 2022换工作面经--开课吧
  16. 往服务器上传文件的软件,上传云服务器文件的软件
  17. 图片水平垂直居中的几种方法总结
  18. day95-容器编排-kubernetes介绍与安装部署
  19. Intellij IDEA常用快捷键一览(windows)
  20. 惠普服务器型号详解,ZOL为您把脉 惠普塔式服务器型号详解

热门文章

  1. oracle pq distribute,SQL调优(SQL TUNING)并行查询提示(Hints)之pq_distribute的使用
  2. 买香奈儿鞋在这个夏天
  3. 一款超牛逼的神仙级接私活软件!吊到不行
  4. 启盈社:我体验开源世界的这几年
  5. 谷歌开发者大会,拳打苹果脚踢微软
  6. [COCI2008-2009#2] PERKET
  7. CUMT2022算法设计与分析A上机考试
  8. unity 使用socket制作局域网项目--激流勇进
  9. Github pages个人域名添加SSL
  10. 西部矿业(601168):整合湖北铅锌资源