SpringBoot 自定义注解实现Redis缓存功能

news/2024/11/27 2:11:56/

背景

最近小A的公司要做一个大屏可视化平台,主要是给领导看的,领导说这个项目要给领导演示,效果好不好直接关系到能不能拿下这个项目,领导还补了一句“这项目至少是百万级的,大伙要全力以赴”,早上小A还想着“最近这么闲,难道还能接着摸鱼?”,想什么怕是来什么,要摸代码了。

小A打起精神,认真研究了功能,几乎是统计、数据展示、地图的一些功能。

下面是小A前后优化的代码


@Service
public class TestServiceImpl implements ITestService {@Autowiredprivate TestMapper testMapper;@Overridepublic List<AbcVo> sczy(String type) {Map<String, AbcVo> map = Stream.of("统计A", "统计B", "统计C", "统计D").map(str->new AbcVo(str, "0", "天")).collect(Collectors.toMap(AbcVo::getType, AbcVo -> AbcVo));List<AbcVo> list = testMapper.sczy(type);for(int i=0; i<list.size(); i++) {AbcVo obj = map.get(list.get(i).getType());if(obj != null) {obj.setValue(new BigDecimal(list.get(i).getValue()).setScale(2, BigDecimal.ROUND_HALF_UP).toString());}}List<AbcVo> resList = new ArrayList<AbcVo>(map.values());return resList;}
}

由于大屏可视化平台每个页面有十几个统计模块,差不多十几个接口,考虑到给领导们展示的时候页面丝滑不卡顿,小A对代码做了优化,加入了缓存,如下:

@Service
public class TestServiceImpl implements ITestService {@Autowiredprivate TestMapper testMapper;@Autowiredprivate RedisCache redisCache;@Overridepublic List<AbcVo> sczy(String qydm) {List<AbcVo> resList = null;//先从缓存取Object cacheObj = redisCache.getCacheObject(CacheConstants.YPTTJ_KEY, qydm);if (StringUtils.isNotNull(cacheObj)) {resList = StringUtils.cast(cacheObj);return resList;}//缓存没有执行下面逻辑,从数据库取值统计if(CollectionUtils.isEmpty(resList)) {Map<String, AbcVo> map = Stream.of("统计A", "统计B", "统计C", "统计D").map(str->new AbcVo(str, "0", "天")).collect(Collectors.toMap(AbcVo::getType, AbcVo -> AbcVo));List<AbcVo> list = testMapper.sczy(qydm);for(int i=0; i<list.size(); i++) {AbcVo obj = map.get(list.get(i).getType());if(obj != null) {obj.setValue(list.get(i).getValue());}}resList = new ArrayList<AbcVo>(map.values());//将结果保存到缓存redisCache.setCacheObject(CacheConstants.YPTTJ_KEY, qydm, resList);}return resList;}

可是一个页面十几个接口,N个页面那不是N倍十几个接口,每个接口都这样加缓存,是不是改动量太大了,而且很多代码都是重复的。于是小A灵机一动,能不能写个注解,在方法上加个缓存注解,代码逻辑不变,就像这样:

@Cache(key = CacheConstants.YPTTJ_KEY)


@Service
public class TestServiceImpl implements ITestService {@Autowiredprivate TestMapper testMapper;@Override@Cache(key = CacheConstants.YPTTJ_KEY)public List<AbcVo> sczy(String type) {Map<String, AbcVo> map = Stream.of("统计A", "统计B", "统计C", "统计D").map(str->new AbcVo(str, "0", "天")).collect(Collectors.toMap(AbcVo::getType, AbcVo -> AbcVo));List<AbcVo> list = testMapper.sczy(type);for(int i=0; i<list.size(); i++) {AbcVo obj = map.get(list.get(i).getType());if(obj != null) {obj.setValue(new BigDecimal(list.get(i).getValue()).setScale(2, BigDecimal.ROUND_HALF_UP).toString());}}List<AbcVo> resList = new ArrayList<AbcVo>(map.values());return resList;}
}

使用自定义缓存的注解时,可以根据缓存key判断缓存中是否存在要的数据,有就直接返回,不再执行方法体中代码;如果缓存中没有,则执行方法体代码,并将方法的返回值缓存到redis中。

小A信心满满,觉得这样写代码才能匹配自己做事的态度。

实现

POM依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId>
</dependency>

项目中Redis配置

# redis 配置
redis:# 地址host: localhost# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms

Redis配置

Redis序列化器

/*** Redis使用FastJson序列化*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {@SuppressWarnings("unused")private ObjectMapper objectMapper = new ObjectMapper();public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static {ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJson2JsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}public void setObjectMapper(ObjectMapper objectMapper) {Assert.notNull(objectMapper, "'objectMapper' must not be null");this.objectMapper = objectMapper;}protected JavaType getJavaType(Class<?> clazz) {return TypeFactory.defaultInstance().constructType(clazz);}
}

redis配置

/*** redis配置*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);serializer.setObjectMapper(mapper);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流脚本*/private String limitScriptText() {return "local key = KEYS[1]\n" + "local count = tonumber(ARGV[1])\n" + "local time = tonumber(ARGV[2])\n"+ "local current = redis.call('get', key);\n" + "if current and tonumber(current) > count then\n"+ "    return tonumber(current);\n" + "end\n" + "current = redis.call('incr', key)\n"+ "if tonumber(current) == 1 then\n" + "    redis.call('expire', key, time)\n" + "end\n"+ "return tonumber(current);";}
}

redis操作工具类:将redis的相关操作封装成类,以组件的形式注入到spring容器中。

 

创建自定义注解

/*** redis 缓存注解*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {/**缓存的key**/String key() default "";/**缓存时长,默认-1表示永久有效**/int time() default -1;/**缓存时长单位,默认单位秒**/TimeUnit type() default TimeUnit.SECONDS;}

自定义缓存注解的逻辑实现

/*** 自定义缓存处理*/
@Aspect
@Slf4j
@Component
public class RedisCacheAspect {@Pointcut("@annotation(com.tencet.common.annotation.Cache)")public void annotationPoint() throws Throwable {}@Autowiredpublic RedisCache redisService;@SneakyThrows@Around(value = "annotationPoint() && @annotation(redisCache)",argNames = "proceedingJoinPoint,redisCache")public Object around(ProceedingJoinPoint proceedingJoinPoint, Cache redisCache){//判断是否开启缓存String hckg = redisService.getCacheObject(CacheConstants.YPT_HCKG); //缓存开关if(StringUtils.isNotEmpty(hckg) && "1".equals(hckg)) { //开启String redisKey = createKeyName(proceedingJoinPoint, redisCache);if(StringUtils.isNotEmpty(redisKey)) { //缓存key不为空System.out.println("key=>"+redisKey);//根据缓存key从缓存中取数据Object cacheObject = redisService.getCacheObject(redisKey);if (Objects.isNull(cacheObject)){ //不存在缓存中//继续执行方法内的代码逻辑, 得到方法的返回值Object methodResult = proceedingJoinPoint.proceed();//方法的返回值存入缓存中redisService.setCacheObject(redisKey, methodResult, redisCache.time(), redisCache.type());log.info("不存在缓存中,存入缓存:"+methodResult.toString());return methodResult; }else{ //存在缓存中log.info("存在缓存中,缓存取出:"+cacheObject.toString());// 缓存命中后,直接返回缓存中的数据。并不执行方法内的逻辑。return cacheObject;}}}//缓存未开启或缓存key为空//忽略缓存,继续执行方法内的代码逻辑, 得到方法的返回值Object methodResult = proceedingJoinPoint.proceed();return methodResult;}/**** * @MethodName: createKeyName * @Description: 生成缓存的 key规则* @Date 2023年5月5日* @param proceedingJoinPoint* @param redisCache* @return*/private String createKeyName(ProceedingJoinPoint proceedingJoinPoint, Cache redisCache){StringBuffer resultKey = new StringBuffer();String key = redisCache.key();if (StringUtils.isNotEmpty(key)){//前缀resultKey.append(key);//后缀String keySuffix = getKeySuffix(proceedingJoinPoint);if (StringUtils.isNotEmpty(keySuffix)){resultKey.append(keySuffix);}}else {return null;}return resultKey.toString();}/*** * @MethodName: getKeySuffix * @Description: 获取方法参数生成缓存key的后缀* @Date 2023年5月5日* @param proceedingJoinPoint* @return*/public String getKeySuffix(ProceedingJoinPoint proceedingJoinPoint) {StringBuffer keySuffix = new StringBuffer();Object[] values = proceedingJoinPoint.getArgs();for(Object o : values) {if(o == null) {keySuffix.append("all");}else {keySuffix.append(o.toString());}}return keySuffix.toString();}/*** * @MethodName: getParam * @Description: 获取参数名和参数值* @Date 2023年5月5日* @param proceedingJoinPoint* @return*/public String getParam(ProceedingJoinPoint proceedingJoinPoint) {Map<String, Object> map = new HashMap<String, Object>();Object[] values = proceedingJoinPoint.getArgs();CodeSignature cs = (CodeSignature) proceedingJoinPoint.getSignature();String[] names = cs.getParameterNames();for (int i = 0; i < names.length; i++) {map.put(names[i], values[i]);}return JSONObject.toJSONString(map);}
}

在方法上使用自定义注解

/*** spring redis 工具类*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache {@Autowiredpublic RedisTemplate redisTemplate;/*** 查看key是否存在*/public boolean exists(String key) {return redisTemplate.hasKey(key);}/*** 缓存基本的对象,Integer、String、实体类等** @param key   缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value) {redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key   缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String keyt, String keyw, final T value) {if(StringUtils.isEmpty(keyw)) {keyw = "all";}String key = keyt + keyw;redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key      缓存的键值* @param value    缓存的值* @param timeout  时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {if(timeout == -1){//永久有效redisTemplate.opsForValue().set(key, value);}else{redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}}/*** 设置有效时间** @param key     Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout) {return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key     Redis键* @param timeout 超时时间* @param unit    时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit) {return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key) {ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String keyt, String keyw) {ValueOperations<String, T> operation = redisTemplate.opsForValue();if(StringUtils.isEmpty(keyw)) {keyw = "all";}String key = keyt + keyw;return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key) {return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** * @MethodName: deleteKey * @Description: 删除多个缓存key * @Date 2023年5月3日* @param arrs* @return*/public long deleteKey(String[] arrs) {Set<String> keys = new HashSet<String>();for(String key : arrs) {//模糊匹配所有以keyword:开头的所有key值keys.addAll(redisTemplate.keys(key+"*"));}return redisTemplate.delete(keys);}/*** 缓存List数据** @param key      缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList) {Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key) {return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key     缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()) {setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key) {return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key) {return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key   Redis键* @param hKey  Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value) {redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key  Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey) {HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据* * @param key* @param hKey*/public void delCacheMapValue(final String key, final String hKey) {HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hKey);}/*** 获取多个Hash中的数据** @param key   Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern) {return redisTemplate.keys(pattern);}
}

在方法上使用自定义注解

@Cache(key = CacheConstants.YPTTJ_KEY)
public Object testMethod(String param) {.........
}//添加过期时间的缓存
@Cache(key = CacheConstants.YPTTJ_KEY, time = 1, type = TimeUnit.HOURS)
public testMethod(String param) {.........
}

运行结果

缓存中无数据,执行方法体代码,并将结果保存到缓存,控制台输出如下:

缓存中有数据,不执行方法体代码,将缓存中数据返回,控制台输出如下:

 缓存如下 


http://www.ppmy.cn/news/61866.html

相关文章

js高级知识汇总一

目录 1.怎么理解闭包&#xff1f; 2.闭包的作用&#xff1f; 3.闭包可能引起的问题&#xff1f; 4.变量提升 5.函数动态参数 6.剩余参数 ...&#xff08;实际开发中提倡使用&#xff09; 7.展开运算符 8.箭头函数 9.解构赋值&#xff08;数组、对象&#xff09; 10 创…

道德经-第十六章

致虚极&#xff0c;守静笃。 万物并作&#xff0c;吾以观复。 夫物芸芸&#xff0c;各复归其根。 归根曰静&#xff0c;是谓复命。 复命曰常&#xff0c;知常曰明。 不知常&#xff0c;妄作&#xff0c;凶。 知常容&#xff0c;容乃公。 公乃王&#xff0c;王乃天。 天乃道&…

云原生时代崛起的编程语言Go并发编程实战

文章目录 概述基础理论并发原语协程-Goroutine通道-Channel多路复用-Select通道使用超时-Timeout非阻塞通道操作关闭通道通道迭代 定时器-TimerAndTicker工作池-Worker Pools等待组-WaitGroup原子操作-Atomic互斥锁-Mutex读写互斥锁-RWMutex有状态协程单执行-Once条件-Cond上下…

使用prometheus时发现mongodb exporter的/metrics数据展示很慢,延迟高

项目场景&#xff1a; 使用prometheusgrafana搭建对mongoDB集群的监控。 问题描述 使用prometheus时发现mongodb exporter的/metrics数据展示接口很慢&#xff0c;延迟高。 看了一下大概是10s 原因分析&#xff1a; 由于是在云服务器上进行搭建的。 经过尝试之后发现创建mo…

Visual Studio Code 1.78 发布

VS Code 1.78 已发布&#xff0c;此版本一些主要亮点包括&#xff1a; 辅助功能改进 - 更好的屏幕阅读器支持、新的音频提示。新的颜色主题 - “Modern” 浅色和深色主题默认设置。 配置文件模板 - Python、Java、数据科学等的内置模板。 新版本提供了配置文件模板&#xff0…

第7章链接:引言

链接&#xff08;linking&#xff09;是将各种代码和数据部分收集起来并组成称为一个单一文件的过程&#xff0c;这个文件可被加载&#xff08;或拷贝&#xff09;到存储器并执行。 链接可在如下三个阶段执行&#xff1a; 编译时&#xff08;complile time&#xff09;&#…

Java实现二叉树

一、树型结构 1、概念 树是一种 非线性 的数据结构&#xff0c;它是由 n &#xff08; n>0 &#xff09;个有限结点组成一个具有层次关系的集合。 把它叫做树是因为它看 起来像一棵倒挂的树&#xff0c; 也就是说它是根朝上&#xff0c;而叶朝下的 。它具有以下的特点&am…

中介者模式

中介者模式 中介者模式定义:使用场景1、房地产中介&#xff1a;房东和租客之间需要通过中介公司来联系&#xff0c;中介公司负责协调双方之间的交流和协商&#xff0c;从而促成房屋出租的交易。2、航空管制中心&#xff1a;在航空交通中&#xff0c;机场、飞机、航空管制中心之…