一、缓存机制的原理
- 一个系统在面向用户使用的时候,当用户的数量不断增多,那么请求次数也会不断增多,当请求次数增多的时候,就会造成请求压力,而我们当前的所有数据查询都是从数据库MySQL中直接查询的,那么就可能会产生如下问题
- 频繁访问数据库,数据库访问压力大,系统性能下降,用户体验差
- 解决问题的方法
- 要解决上述提到的问题,就可以使用前面学习的Redis技术,通过Redis实现缓存机制,从而降低数据库的访问压力;提高系统的访问性能,从而提升用户体验
- 加入Redis后,在进行数据查询的时候,就需要先查询缓存,如果缓存中有数据,直接返回;如果没有相对应的数据,那么就去查询数据库,再将数据库查询的结果,缓存在Redis中
二、缓存短信验证码
环境搭建
①、在项目pom.xml文件中导入spring-data-redis的maven坐标
<!--Spring data redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
②、在项目的配置文件中加入Redis相关配置(在Spring层级下)
redis: jedis: pool: max-idle: 5 #最大链接数,连接池中最多有10个 min-idle: 1 # 最大空闲数 max-wait: 1000ms #连接池最大阻塞等待时间 max-active: 10 #最大链接数 host: 127.0.0.1 port: 6379 database: 2 # password:
2.1、思路分析
- 前面实现的移动端手机验证登录功能,随机生成的验证码是保存在
HttpSession
当中的。但是实际的业务场景中,一般验证码都是需要设置过期时间的,如果存在HttpSession
中就无法设置过期时间,此时我们就需要对这一块的功能进行优化 - 可以将验证码缓存在Redis中,具体的实现思路如下
- ①、在服务端
UserController
中注入RedisTemplate
对象,用于操作Redis - ②、在服务端
UserController
的sendMsg
方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分组 - ③、在服务端
UserController
的login
方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
- ①、在服务端
2.2、代码改造
-
①、在
UserController
中注入RedisTemplate
对象,用于操作Redis-
@Autowired private RedisTemplate<String, String> redisTemplate;
-
-
②、在
UserController
的sendMsg
方法中,将生成的验证码保存到Redis中(为了测试方便,这里是直接生成了固定的验证码,没有调用真实的生成验证码的API)-
// 将登录账号的信息存储在redis中 // 获取字符串的客户端 ValueOperations<String, String> valueOperations = redisTemplate.opsForValue(); // 存储验证码,让验证码失效时间是一分钟 valueOperations.set("SMS_" + user.getPhone(), code, 1, TimeUnit.MINUTES);
-
-
③、在登录校验的代码中实现从Redis中取出数据
-
// 2. 获取正确的验证码 // String verifyCode = (String) session.getAttribute("SMS_" + inputPhone); // 从redis中获取正确的验证码 String verifyCode = redisTemplate.opsForValue().get("SMS_" + inputPhone);
-
2.3、功能测试
- ①、访问前端,获取验证码
- 通过控制台的日志,可以看到生成的代码
- ②、通过Redis的图形化界面工具查看Redis中的数据
- ③、在登录界面填写验证码登录完成后,查看Redis中的数据是否删除
三、缓存菜品信息
3.1、思路分析
-
之前项目中已经实现了移动端菜品查看的功能,对应的服务端方法为
DishController
的list
方法,此方法会根据前端提交的查询条件(categoryId
)进行数据库查询操作。 -
在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长;针对这个问题,可以对此方法进行缓存优化,提高系统的性能
-
那么应该缓存多少分数据呢?是所有的菜品缓存一份,还是需要根据分类的不同,缓存多份?
- 很显然,在前端点击一个分类的时候,展示的就是这个分类下的菜品,其他菜品无需展示
- 所以,这里面我们在缓存时,可以根据菜品的分类,缓存多分数据,页面在提交查询请求的时候,就查询该分类下的菜品缓存数据
-
具体实现思路
- ①、修改业务层的
list
方法,先从Redis中获取分类对应的菜品数据,如果有则直接返回,无需查询数据库;如果没有,则查询数据库,并将查询到的菜品数据存入Redis - ②、修改
DishController
的save
和update
方法,加入清理缓存的逻辑
- ①、修改业务层的
-
注意事项
- 在使用缓存的过程当中,要注意保证数据库中的数据和缓存中的数据保持一致
- 如果数据库中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况
3.2、代码改造
3.2.1、查询菜品缓存
在增加缓存之前,需要对存储进Redis中的数据进行一个简单的设计,如下所示
数据类型 key值 value值 String dish_菜品分类的id 菜品的List集合(List )
-
①、在
DishServiceImpl
中注入RedisTemplate
-
@Autowired private RedisTemplate<String, String> redisTemplate;
-
-
②、在
list
方法中,查询数据库之前,先查询缓存,如果缓存有数据,则直接返回-
// 根据分类id查询菜品列表数据 @Override public List<DishDto> selectByCategoryIdAndStatus(Long categoryId, Integer status) { // 0. 首先先判断Redis中是否存在缓存 // 获取redis操作字符串的客户端 ValueOperations<String, String> valueOperations = redisTemplate.opsForValue(); List<DishDto> dishDtoList = JSON.parseObject(valueOperations.get("dish_" + categoryId + "_" + status), List.class); // 如果redis中不存在这个缓存,则查询数据库,并且将查询到的结果存储到缓存中 if (dishDtoList == null) { // 1. 调用 dao 层对象执行sql语句查询数据 List<Dish> dishList = dishMapper.selectByCategoryIdAndStatus(categoryId, status); // 2. 遍历dishList,查询其相对应的口味数据表 dishDtoList = dishList.stream().map(dish -> { // 查询对应的口味表 List<DishFlavor> dishFlavorList = dishFlavorMapper.selectByDishId(dish.getId()); DishDto dishDto = new DishDto(); // 将数据封装到dishDto中 dishDto.setFlavors(dishFlavorList); // 将基本属性复制给dishDto BeanUtils.copyProperties(dish, dishDto); return dishDto; }).collect(Collectors.toList()); // 把查询到的数据,存储到Redis中 // 把dishDtoList对象转换为Json格式 String dishJson = JSON.toJSONString(dishDtoList); valueOperations.set("dish_" + categoryId, dishJson, 2, TimeUnit.DAYS); } // 返回数据 return dishDtoList; }
-
3.2.2、清理菜品缓存
为了保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据
所以,需要在菜品的增删改中清空缓存数据
-
清理菜品缓存的方式有两种
-
A、清理所有分类下的菜品缓存
-
//清理所有菜品的缓存数据 Set keys = redisTemplhate.keys("dish_*"); //获取所有以dish_xxx开头的key redisTemplate.delete(keys); //删除这些key
-
-
B、清理当前添加菜品分类下的缓存
-
//清理某个分类下面的菜品缓存数据 String key = "dish_" + dishDto.getCategoryId(); redisTemplate.delete(key);
-
-
两者的优劣(需要结合实际的业务场景考虑)
- 对于这次的修改操作,用户可以修改菜品的分类,如果用户修改了了菜品的分类,那么原来的分类下将少一个菜品,新的分类下将多一个菜品,这样的话,两个分类的菜品列表数据都发生了变化
- 即此时的情况不能只是删除某一个分类的菜品缓存
- 所以,在本次的系统中推荐使用第一种方法清理菜品缓存
-
-
这里清理缓存的操作比较简单,就不演示了,只需要在数据发生变更后的代码后面添加一个删除缓存的代码即可,如下所示
3.3、功能测试
- ①、访问移动端,根据分类查询菜品列表,然后再检查Redis的缓存数据是否存在
- ②、当对菜品进行增删改的时候,查询Redis中的缓存数据,是否被清除
四、Spring Cache
4.1、Spring Cache介绍
-
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码
-
Spring Cache只是提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口
-
针对不同的缓存技术需要实现不同的CacheManager,如下表所示
-
CacheManager 描述 EhCacheCacheManager 使用EhCache作为缓存技术 GuavaCacheManager 使用Google的GuavaCache作为缓存技术 RedisCacheManager 使用Redis作为缓存技术 spring 自己也搞了一套缓存技术,默认的缓存
spring缓存是缓存在Map集合中
-
4.2、Spring Cache注解
-
在SpringCache中提供了很多缓存操作的注解,常见的几个如下所示
-
注解 说明 @EnableCaching 开启缓存注解功能 @Cacheable 在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,则调用方法并将方法返回值放到缓存中 @CachePut 将方法的返回值或者参数放到缓存中 @CacheEvict 将一条或多条数据从缓存中删除
-
-
在SpringBoot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可
- 例如使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标,同时在配置文件中配置Redis的相关配置即可
4.3、Spring Cache入门案例
- 接下来,我们可以通过一个入门案例演示以下SpringCache的常见用法。上面提到,SpringCache可以集成不同的缓存技术,如Redis、Ehcache甚至我们可以使用Map来缓存数据,接下来我们在演示的时候,就先通过一个Map来缓存数据,最后我们再换成Redis来缓存
4.3.1、环境准备
-
①、数据库准备
-
/* SQLyog Ultimate v11.33 (64 bit) MySQL - 5.5.40 : Database - cache_demo ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`cache_demo` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `cache_demo`; /*Table structure for table `user` */ DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; /*Data for the table `user` */ LOCK TABLES `user` WRITE; UNLOCK TABLES; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-
-
②、导入基本工程
- 基本工程的创建,这里就不演示了,只是一个User表的增删改查操作
-
③、注入CacheManager
- 我们可以在
UserController
注入一个CacheManager
,在Debug时,我们可以通过CacheManger
跟踪缓存中数据的变化 - 我们可以进入CacheManger接口的源码中查看,默认的实现有几种,如下图所示
- 而在上述的几个实现中,默认使用的是
ConcurrentMapCacheManger
,稍后我们可以通过断点的形式跟踪缓存数据的变化
- 我们可以在
-
④、启动类加上@EnableCaching注解
- 在启动类加上该注解,就代表当前项目开启缓存注解功能
4.3.2、@CachePut注解
- @CachePut注解说明
- 作用
- 将方法返回值,放入缓存
- value
- 缓存的名称,每个缓存名称下面可以有很多key
- key
- 缓存的key,支持Spring的表达式语言SPEL语法
-
①、在save方法上加上注解@CachePut
-
当前
UserController
的save
方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save
方法上加上注解@CachePut
,如下所示 -
/** * CachePut:将方法返回值放入缓存 * value:缓存的名称,每个缓存名称下面可以有多个key * key:缓存的key */ @CachePut(value = "userCache", key = "#user.id") @PostMapping public User save(@RequestBody User user){ userService.save(user); return user; }
-
key的写法如下:
#user.id
#user
指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key
#user.name
#user
指的是方法形参的名称, name指的是user的name属性 ,也就是使用user的name属性作为key
#result.id
#result
代表方法返回值,该表达式 代表以返回对象的id属性作为key
#result.name
#result
代表方法返回值,该表达式 代表以返回对象的name属性作为key
-
-
②、使用Postman进行功能测试
- 启动服务,通过postman请求访问
UserController
的方法, 然后通过断点(Debug)的形式跟踪缓存数据 - 第一次访问时,缓存中的数据是空的,因为save方法执行完毕后才会缓存数据
- 第二次访问时,我们通过debug可以看到已经有一条数据了,就是上次保存的数据,已经缓存了,缓存的key就是用户的id
- 启动服务,通过postman请求访问
-
PS
- 上述的演示,最终的数据,实际上是缓存在ConcurrentHashMap中,那么当我们的服务器重启之后,缓存中的数据就会丢失。 后面使用了Redis来缓存就不存在这样的问题
4.3.3、@CacheEvict注解
- @CacheEvict注解说明
- 作用
- 清理指定缓存
- value
- 缓存的名称,每个缓存名称下面可以有多个key
- key
- 缓存的key,支持Spring的表达式语言SPEL语法
-
①、在delete方法上加@CacheEvict注解
-
当我们在删除数据库
user
表数据的时候,需要删除缓存中对应的数据,此时就可以使用@CacheEvict
注解,如下所示-
/** * CacheEvict:清理指定缓存 * value:缓存的名称,每个缓存名称下面可以有多个key * key:缓存的key */ @CacheEvict(value = "userCache", key = "#p0") //#p0 代表第一个参数 //@CacheEvict(value = "userCache",key = "#root.args[0]") //#root.args[0] 代表第一个参数 //@CacheEvict(value = "userCache",key = "#id") //#id 代表变量名为id的参数 @DeleteMapping("/{id}") public void delete(@PathVariable Long id){ userService.removeById(id); }
-
-
-
②、使用Postman进行功能测试
- 测试缓存的删除,先访问save方法任意次,保存n条数据到数据库的同时,也保存到缓存中,最终可以通过debug看到缓存中的数据信息,然后可以通过Postman方法delete方法,进行缓存的删除
- 删除数据的时候,通过debug可以看到已经缓存的4条数据
- 当执行完delete操作后,我们再保存一条数据,在保存的时候debug查看之前的缓存是否已经被删除
-
③、在update方法上加注解@CacheEvict
-
在更新数据之后,数据库的数据已经发生了变更,我们需要将缓存中对应的数据删除掉,避免出现数据库数据与缓存数据不一致的情况
-
//@CacheEvict(value = "userCache",key = "#p0.id") //第一个参数的id属性 //@CacheEvict(value = "userCache",key = "#user.id") //参数名为user参数的id属性 //@CacheEvict(value = "userCache",key = "#root.args[0].id") //第一个参数的id属性 @CacheEvict(value = "userCache",key = "#result.id") //返回值的id属性 @PutMapping public User update(@RequestBody User user){ userService.updateById(user); return user; }
-
-
加上注解之后,重启服务,然后使用Postman进行测试,测试步骤和方法跟上述①、②差不多,这里就不再演示
-
4.3.4、@Cacheable注解
- @Cacheable注解说明
- 作用
- 在方法执行前,Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放入到缓存中
- value
- 缓存的名称,每个缓存名称下面可以有多个key
- key
- 缓存的key,支持Spring的表达式语言SPEL语法
-
①、在getById方法上加@Cacheable注解
-
@Cacheable(value = "user",key = "#id") @GetMapping("/{id}") public User getById(@PathVariable Long id){ User user = userService.findById(id); return user; }
-
-
②、使用Postman进行功能测试
- 重启服务,然后通过debug断点跟踪程序执行;可以发现,第一次访问,会请求
Controller
的方法,查询数据库。后面再查询相同的id,就直接获取到数据库,不用再查询数据库了,就说明缓存已经生效- 第一次查询
- 第二次查询
- 第一次查询
- 在测试的时候,查询一个数据库中不存在的id值,第一次查询缓存中没有,也会查询数据库。第二次查询的时候,会发现,不再查询数据库了,而是直接返回,那也就是说如果根据id没有查询到数据,那么会自动缓存一个null值,可以通过debug进行验证一下
- 第一次查询
- 第二次查询
- 第一次查询
- 这时候就会出现一个问题,能不能查询到的值不为null值的时候再进行缓存,如果为null值,则不进行缓存呢?
- 重启服务,然后通过debug断点跟踪程序执行;可以发现,第一次访问,会请求
-
③、缓存非null值
-
在
@Cacheable
注解中,提供了两个属性分别为:condition
、unless
condition
- 表示满足什么条件,再进行缓存
unless
- 表示满足条件则不缓存,与上述的condition是反向的
-
具体实现方法如下所示
-
/** * 注意: @Cacheable把方法的返回值缓存起来, 即使方法返回值为null也会被缓存,如果需要改变这个结果: * condition : 符合指定条件则缓存,这个属性不建议使用,因为condition这个属性不能使用result。 * unless : 不符合指定条件则缓存 */ // @Cacheable 执行方法前先判断缓存是否存在指定id的user, // 如果存在不会执行方法,直接返回缓存中数据即可。如果不存在才会返回缓存数据 @Cacheable(value = "user",key = "#id",unless = "#result==null") @GetMapping("/{id}") public User getById(@PathVariable Long id){ User user = userService.findById(id); return user; }
-
-
这里这能使用
unless
,因为condition
属性无法获取到结果#result
-
4.4.、Spring Cache集成Redis
-
在使用上述默认的ConcurrentHashMap做缓存时,服务重启之后,之前缓存的数据就全部丢失了,操作起来并不友好。在项目中使用,我们会选择使用redis来做缓存,主要需要操作以下几步
-
①、添加依赖
-
<!--spring cache依赖导入--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--spring data redis 导入--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
-
②、配置application.yaml配置文件(Spring层级下)
-
redis: host: 127.0.0.1 database: 2 cache: redis: time-to-live: 1800000 #单位毫秒,设置缓存过期时间,可选
-
-
③、测试
-
五、缓存套餐数据
5.1、思路分析
- 前面已经实现了移动端套餐查看功能,对应的服务端方法为
SetmealController
的list方法,此方法会根据前端提交的查询条件进行数据库查询操作 - 在高并发的情况下,频繁地查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能
5.2、代码改造
-
①、导入SpringCache和Redis相关的maven坐标(spring data redis之前已经导入过了)
-
<!--spring cache依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
-
-
②、在application.yml配置文件中配置缓存数据的过期时间
-
cache: redis: time-to-live: 1800000 # 设置缓存数据过期时间
-
-
③、在启动类上加入@EnableCaching注解,开启缓存注解功能
-
package com.coolman; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.cache.annotation.EnableCaching; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @MapperScan(basePackages = "com.coolman.mapper") @ServletComponentScan(basePackages = "com.coolman.filters") @EnableTransactionManagement //开启对事务管理的支持 @EnableCaching public class ReggieApplication { public static void main(String[] args) { SpringApplication.run(ReggieApplication.class, args); } }
-
-
④、在
SetmealServiceImpl
的selectByCategoryIdAndStatus
方法上加入@Cacheable注解-
在进行套餐数据查询时,需要根据分类ID和套餐的状态进行查询,所以在缓存数据的时候,可以将套餐分类id和套餐状态组合起来作为key,如下所示
1627182182_1
(1627182182是分类id,1是状态)
-
@Override @Cacheable(value = "setmeal", key = "#categoryId + '_' + #status") public List<SetMeal> selectByCategoryIdAndStatus(Long categoryId, Integer status) { return setMealMapper.selectByCategoryIdAndStatus(categoryId, status); }
-
-
⑤、在
SetmealServiceImpl
的数据变更方法上加入@CacheEvict注解-
为了保证数据库中数据与缓存数据的一致性,在添加套餐或者删除套餐数据之后,需要清空当前套餐缓存的全部数据
-
那么@CacheEvict注解如何清除某一份缓存下所有的数据呢?这里可以指定@CacheEvict中的一个属性allEnties,将其设置为true即可,其含义为setmeal名称空间下面的所有key都删除
-
@CacheEvict(value = "setmeal", allEntries = true) // allEntries = true 代表了setmeal名称空间下面的所有key都删除 public void update(SetMealDto setMealDto) { // 补全数据,更新时间 setMealDto.setUpdateTime(LocalDateTime.now()); // 更新setmeal表的数据 setMealMapper.updateByIds(setMealDto, new Long[]{setMealDto.getId()}); // 更新setmeal_dish表的数据 // 先删除,再修改 // 原因: 前端返回的数据是一个集合,有多个id,同时其中的数据也不是固定不变的,且是一对多关系,处理起来非常麻烦 // 给setmeal_dish补全数据 List<SetMealDish> setmealDishes = setMealDto.getSetmealDishes(); for (SetMealDish setmealDish : setmealDishes) { // 设置创建人和创建时间 setmealDish.setCreateUser(setMealDto.getCreateUser()); setmealDish.setCreateTime(setMealDto.getCreateTime()); // 设置更新时间和更新人 setmealDish.setUpdateUser(setMealDto.getUpdateUser()); setmealDish.setUpdateTime(LocalDateTime.now()); // 设置setmeal_id setmealDish.setSetmealId(setMealDto.getId().toString()); // 设置sort setmealDish.setSort(0); } // 根据id批量删除数据 ArrayList<String> ids = new ArrayList<>(); ids.add(setMealDto.getId().toString()); setMealDishMapper.deleteByIds(ids); // 批量插入数据 setMealDishMapper.batchInsert(setmealDishes); }
-
上述代码只是修改功能的一个方法,其他有数据变更的操作,一般都要清理缓存,以保证数据库的数据和缓存的数据一致
-
5.3、功能测试
- 代码编写完成之后,重启工程,然后访问后台管理系统,对套餐数据进行新增以及删除, 然后通过Redis的图形化界面工具,查看Redis中的套餐缓存是否已经被删除
- ①、第一次查询套餐列表,查看Redis中是否存储相对应的数据
- ②、第二次查询套餐列表,查看服务端终端输出的是否有SQL语句,验证是否是从Redis中读取数据
- ③、执行数据变更操作(这里测试使用添加功能),查看Redis中的缓存数据是否删除
- 这里有部分dish值出现null字符的原因是之前在没单纯使用
RedisTemplate
对象的时候没有删除测试代码,所以不管传入的status值是否为null值,其都会存储到缓存中,不过在这里无伤大雅,仅作为测试,在实际应用场景中,非常不建议同时使用RedisTemplate
和Spring redis data
- 这里有部分dish值出现null字符的原因是之前在没单纯使用
- 到这里,项目的缓存优化结束!