入门 SpringBoot 3 Spring Cache Redis 实现对接口自定义缓存 home 编辑时间 2024/06/16 ![](/api/file/getImage?fileId=666e8a6fc0a657000b00153a) <br> ## 前言 (高能预警:写着一篇笔记的时候被气得不轻 ,前言全是废话,折腾里也有大量废话,不看废话只看代码,不影响最终结果) 很久以前我有一个需求 当用户请求一些不经常改变的数据时 如果我正常处理流程如下 1. 用户请求 2. 校验传入参数合法 2. 数据库多表联查读取 3. Java数据处理 4. 返回数据 每个人都来一遍非常浪费资源 我需要一个短时间缓存 流程改为 1. 用户请求 2. 查看缓存中是否存在完全同类名同方法名同参数的请求 3. 存在则立即返回缓存数据,不存在则计算后存如缓存 4. 返回数据 对比后可以发现,可以减少大量的重复的SQL读取和Java计算 只需要例如每5分钟处理一次即可 当年我是用AOP自己实现了这个需求 AOP拦截 在before中 拿到类名 方法名 全部参数 以及注解 通过判断注解 确认是否需要走缓存 再按上述流程进行处理 命中缓存的直接返回,跳过查询和计算 而最近我发现直接用 `spring-cache` 可以实现相同的效果,且更优雅,唯一的缺点是网上最新版本 Springboot3 搭配 redis `spring cache` 的资料太难找 自己摸索花了1天才完全跑通 具体代码可以参考第二章节的折腾 必须吐槽一下百度和KIMI 百度:搜到都是什么破玩意,CSDN占70%结果,包括通篇没代码全概念还是复制官网的,代码过时6年前的,代码自己都不懂就贴上来的,没头没尾中间给你贴2行其余全靠猜的,见面先要会员才能往下看的。剩下30%也不是什么好鸟,甚至有标题一本正经,内容完全和标题无关的。 可以想象到,我这篇文章写出来以后,与百度的结果有多格格不入,搜索SpringBoot Cache redis缓存,至少得排在2万名开外,原因是太不合群了。 KIMI:其实我知道他也尽力了,奈何他只会写原生语言,比如Java8实现分批读取大文本文件,他可以直接阅读国外官方文档,然后用脑子写代码,写出来95%都能开箱即用。而像同时用到了 `java` `springboot` `spring-cache` `redis` 加中文的关键字例如 `缓存`,他可能就无法直接独立完成代码了,他就去百度了,然后本来能力有90分脑子的KIMI,百度过后写出来的代码,那叫一个狗屁不通,贴进来还没运行,先一大堆红色,真无语。出淤泥而染进骨髓。 <br> ## 折腾 本身需要的依赖只有这两个 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> ``` 我在代码里还用到了这些依赖 你可以看情况选择是否用同样的方法实现 ```xml <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.51</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.17.0</version> </dependency> ``` 新建一个config类 `CacheConfig.java` ```java package com.zzzmh.config; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONWriter; import com.zzzmh.utils.Result; import org.apache.commons.codec.digest.DigestUtils; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.*; import java.nio.charset.Charset; import java.time.Duration; @Configuration @EnableCaching public class CacheConfig { /** * 自定义缓存key * 决定了什么样的请求会命中哪一个缓存 * 我这里用 类名 + 方法名 + 全参数JSONString 算MD5 * 可以命中 同类 同方法 完全同参数的请求命中缓存 */ @Bean public KeyGenerator keyGenerator() { return (target, method, params) -> { String string = target.getClass().getSimpleName() + "_" + method.getName() + "_" + JSONArray.of(params).toJSONString(JSONWriter.Feature.WriteMapNullValue); System.out.println(string); String md5Hex = DigestUtils.md5Hex(string); System.out.println(md5Hex); return md5Hex; }; } @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // 序列化存入redis里的key和value 其中JSON的部分用FASTJSON2实现 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Fastjson2RedisSerializer())) // 启用过期时间 .enableTimeToIdle() // 缓存过期时间 300秒 .entryTtl(Duration.ofSeconds(300l)) // 前缀 .prefixCacheNameWith("spring_cache_") // 禁用空返回值进缓存 .disableCachingNullValues(); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); } /** * 写个匿名内部类 * 实现最简单的序列化和反序列化 */ class Fastjson2RedisSerializer implements RedisSerializer { @Override public byte[] serialize(Object result) throws SerializationException { return result == null ? null : JSONObject.toJSONString(result).getBytes(Charset.forName("UTF-8")); } @Override public Result deserialize(byte[] bytes) throws SerializationException { return JSONObject.parseObject(new String(bytes, Charset.forName("UTF-8")), Result.class); } } } ``` 在接口上价格注解就可以用缓存了 ```java @Cacheable(value = "getData", keyGenerator = "keyGenerator") @PostMapping("getData") public Result getData(@RequestBody getDataForm form) { // 中间是啥代码不重要 省略了 } ``` 只需要在接口顶上配置@Cacheable即可 keyGenerator是对应前文中的 `CacheConfig.keyGenerator()` 方法 用于命中缓存的,返回哪一个缓存就靠这个判断,否则同一个方法不同的传入参数,返回了一样的缓存结果,就炸了 value只是用于redis的key的后缀,让你自己去删redis的时候方便辨认,前文中配置的前缀是 `spring_cache_`,这里后缀是getData 那redis里拼全后的就是 `spring_cache_getData::md5` <br><br> 这里每一行代码都是我自己试出来的,你能想象有多绝望吗 挑几个重点说道说道,不想看可以跳过这段啰嗦 <br><br> `KeyGenerator` 方法是用于判断命中规则 我这里用类名 + 方法名 + 多个参数转 JSONArray 再 toJSONString,得到一个字符串 再对字符串MD5 得到一段32位特征码,只有同类 同方法 同参数,特征码才会完全一致 测试后发现toJSONString有个坑 比如你的传入参数 有空值时 想象中应该是 [{"name":"张三","mobile":null}] 实际上得到的是 [{"name":"张三"}] 他偷懒偷到我头上来了 在toJSONString里加这段代码就可以显示所有参数了 `JSONWriter.Feature.WriteMapNullValue` 序列化value有这样一行代码 `.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Fastjson2RedisSerializer()))` 本身推荐的写法是 `.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))` 好处是不需要自己实现`Fastjson2RedisSerializer`方法 坏处是真的刺激 他的智障反序列化 需要你每一个返回值里所牵涉到的类 都要接 ` implements Serializable ` 并且你要么彻底不写构造函数 你要写就必须有个空参的构造函数 注意我说的是每一个涉及到的类 举个例子你就知道多奔溃了 假设我返回的代码长这样(不严谨 意思表达出来就行) `return Result.ok(PageUtils.format(List<User>))` 这里面一共3个类 `Result` `PageUtils` `User` 少一个他就序列化失败!必须按照要求接 `Serializable` + 空参的构造函数 本来就是为了优雅,用 `spring-cache`,现在优雅荡然无存 之所以这么麻烦,是以为他不知道你返回值的类型是什么类型 所以他序列化要提前兼容你的类型就会很蛋疼 据我所知 `fastjson2` 就没那么麻烦 如果是你要一步到位兼容所有的类 应该是这样写 ```java class Fastjson2RedisSerializer<T> implements RedisSerializer<T> { private final Class<T> clazz; private final Charset charset; public Fastjson2RedisSerializer(Class<T> clazz) { this.clazz = clazz; this.charset = Charset.forName("UTF-8"); } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } try { return JSON.toJSONString(t).getBytes(charset); } catch (Exception e) { throw new SerializationException("Could not serialize: " + t, e); } } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } try { return JSON.parseObject(new String(bytes, charset), clazz); } catch (Exception e) { throw new SerializationException("Could not deserialize: " + e.getMessage(), e); } } } ``` 用的地方这样用 ```java .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Fastjson2RedisSerializer<>(Object.class))) ``` 简单几行,就兼容所有类了 但是我这里的需求是,只用在controller请求的返回值上。 我只可能用到Result这一个我自己定义的返回类(只需要第一层) 所以我最终代码里,写的是Result,不重复写了,你看我给的最终代码即可。 经过测试 上述代码第一次请求 会在redis中生成一条记录 key spring_cache_getData::md5 value { ... } ttl 300秒 到期自动销毁 第二次请求 如果存在相同的md5缓存 直接用value转成Result返回 大功告成 累个半死 #END 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay Vue3 Vite 入门笔记 2024 解决腾讯云轻量云服务器内网互联云数据库MySQL内网地址连不上问题