Loading... ## 课程内容 - 环境搭建 - 缓存短信验证码 - 缓存菜品信息 - SpringCache - 缓存套餐数据 ## 前言 > 1). 当前系统存在的问题 之前我们已经实现了移动端菜品展示、点餐、购物车、下单等功能,但是由于移动端是面向所有的消费者的,请求压力相对比较大,而我们当前所有的数据查询都是从数据库 MySQL 中直接查询的,那么可能就存在如下问题: **频繁访问数据库,数据库访问压力大,系统性能下降,用户体验较差。** ![](https://blog.fivk.cn/usr/uploads/2023/09/219349644.png) > 2). 解决该问题的方法 要解决我们上述提到的问题,就可以使用我们前面学习的一个技术:Redis,通过 Redis 来做缓存,从而降低数据库的访问压力,提高系统的访问性能,从而提升用户体验。加入 Redis 做缓存之后,我们在进行数据查询时,就需要先查询缓存,如果缓存中有数据,直接返回,如果缓存中没有数据,则需要查询数据库,再将数据库查询的结果,缓存在 redis 中。 ## 1. 环境搭建 ### 1.1 版本控制 接下来,我们就需要对我们的功能进行优化,但是需要说明的是,我们不仅仅要对上述提到的缓存进行优化,还需要对我们程序的各个方面进行优化。我们本章节主要是针对于缓存进行优化,为了方便的对我们各个优化版本的代码进行管理,我们使用 Git 来控制代码版本。 那么此时我们就需要将我们之前开发完成的代码提交到 Git,并且推送到码云 Gitee 的远程仓库,执行步骤如下: **1). 创建 Gitee 远程仓库** ![](https://blog.fivk.cn/usr/uploads/2023/09/2184899233.png) **2). idea-创建本地仓库** ![](https://blog.fivk.cn/usr/uploads/2023/09/3656158233.png) **3). 准备忽略文件.gitignore** 在我们的项目中, 有一些文件是无需提交的到 git,比如: .idea,target/,\*.iml 等。我们可以直接将今天课程资料中提供的.gitignore 文件导入到我们的项目中。 ![](https://blog.fivk.cn/usr/uploads/2023/09/855584240.png) **4). idea-提交并推送本地代码** A. 添加项目文件进暂存区 ![](https://blog.fivk.cn/usr/uploads/2023/09/663136001.png) B. 提交代码 ![](https://blog.fivk.cn/usr/uploads/2023/09/2058551146.png) ![](https://blog.fivk.cn/usr/uploads/2023/09/3908172484.png) C. 推送代码到远程仓库 ![](https://blog.fivk.cn/usr/uploads/2023/09/3951005815.png) **5). 查看 gitee 远程仓库** ![](https://blog.fivk.cn/usr/uploads/2023/09/1345494553.png) **6). 创建分支** 目前默认 git 中只有一个主分支 master,我们接下来进行缓存的优化,就不在 master 分支来操作了,我们需要在 git 上创建一个单独的分支 v1.0,缓存的优化,我们就在该分支上进行操作。 ![](https://blog.fivk.cn/usr/uploads/2023/09/2973667656.png) 当前创建的 v1.0 分支,是基于 master 分支创建出来的,所以目前 master 分支的代码, 和 v1.0 分支的代码是完全一样的,接下来把 v1.0 的代码也推送至远程仓库。 **7). 推送分支代码到远程** ![](https://blog.fivk.cn/usr/uploads/2023/09/3961745734.png) ![](https://blog.fivk.cn/usr/uploads/2023/09/2466891583.png) ### 1.2 环境准备 **1). 在项目的 pom.xml 文件中导入 spring data redis 的 maven 坐标** ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` **2). 在项目的 application.yml 中加入 redis 相关配置** ```yml redis: host: 192.168.200.200 port: 6379 password: root@123456 database: 0 ``` <div class="tip inlineBlock error"> 注意: 引入上述依赖时,需要注意 yml 文件前面的缩进,上述配置应该配置在 spring 层级下面。 </div> **3). 编写 Redis 的配置类 RedisConfig,定义 RedisTemplate** ```java import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); //默认的Key序列化器为:JdkSerializationRedisSerializer redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } } ``` **解释说明:** 1). 在 SpringBoot 工程启动时, 会加载一个自动配置类 RedisAutoConfiguration, 在里面已经声明了 RedisTemplate 这个 bean ![](https://blog.fivk.cn/usr/uploads/2023/09/2761324185.png) 上述框架默认声明的 RedisTemplate 用的 key 和 value 的序列化方式是默认的 JdkSerializationRedisSerializer,如果 key 采用这种方式序列化,最终我们在测试时通过 redis 的图形化界面查询不是很方便,如下形式: ![](https://blog.fivk.cn/usr/uploads/2023/09/1808404707.png) 2). 如果使用我们自定义的 RedisTemplate, key 的序列化方式使用的是 StringRedisSerializer, 也就是字符串形式, 最终效果如下: ![](https://blog.fivk.cn/usr/uploads/2023/09/771856801.png) 3). 定义了两个 bean 会不会出现冲突呢? 答案是不会, 因为源码如下: ![](https://blog.fivk.cn/usr/uploads/2023/09/515331864.png) ## 2. 缓存短信验证码 ### 2.1 思路分析 前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在 HttpSession 中的。但是在我们实际的业务场景中,一般验证码都是需要设置过期时间的,如果存在 HttpSession 中就无法设置过期时间,此时我们就需要对这一块的功能进行优化。 现在需要改造为将验证码缓存在 Redis 中,具体的实现思路如下: 1). 在服务端 UserController 中注入 RedisTemplate 对象,用于操作 Redis; 2). 在服务端 UserController 的 sendMsg 方法中,将随机生成的验证码缓存到 Redis 中,并设置有效期为 5 分钟; 3). 在服务端 UserController 的 login 方法中,从 Redis 中获取缓存的验证码,如果登录成功则删除 Redis 中的验证码; ### 2.2 代码改造 1). 在 UserController 中注入 RedisTemplate 对象,用于操作 Redis ```java @Autowired private RedisTemplate redisTemplate; ``` 2). 在 UserController 的 sendMsg 方法中,将生成的验证码保存到 Redis ```java //需要将生成的验证码保存到Redis,设置过期时间 redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES); ``` ![](https://blog.fivk.cn/usr/uploads/2023/09/2672151281.png) 3). 在 UserController 的 login 方法中,从 Redis 中获取生成的验证码,如果登录成功则删除 Redis 中缓存的验证码 ```java //从Redis中获取缓存的验证码 Object codeInSession = redisTemplate.opsForValue().get(phone); ``` ```java //从Redis中删除缓存的验证码 redisTemplate.delete(phone); ``` ![](https://blog.fivk.cn/usr/uploads/2023/09/2447710962.png) ### 2.3 功能测试 代码编写完毕之后,重启服务。 **1). 访问前端工程,获取验证码** ![](https://blog.fivk.cn/usr/uploads/2023/09/172827538.png) 通过控制台的日志,我们可以看到生成的验证码: ![](https://blog.fivk.cn/usr/uploads/2023/09/1479507843.png) **2). 通过 Redis 的图形化界面工具查看 Redis 中的数据** ![](https://blog.fivk.cn/usr/uploads/2023/09/1915640537.png) **3). 在登录界面填写验证码登录完成后,查看 Redis 中的数据是否删除** ![](https://blog.fivk.cn/usr/uploads/2023/09/4155010582.png) ## 3. 缓存菜品信息 ### 3.1 实现思路 前面我们已经实现了移动端菜品查看功能,对应的服务端方法为 DishController 的 list 方法,此方法会根据前端提交的查询条件(categoryId)进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。 那么,我们又需要思考一个问题, 具体缓存几份数据呢, 所有的菜品缓存一份 , 还是说需要缓存多份呢? 我们可以看一下我们之前做的移动端效果: ![](https://blog.fivk.cn/usr/uploads/2023/09/1916987808.png) 我们点击哪一个分类,展示的就是该分类下的菜品, 其他菜品无需展示。所以,这里面我们在缓存时,可以根据菜品的分类,缓存多份数据,页面在查询时,点击的是哪个分类,我们就查询该分类下的菜品缓存数据。 **具体的实现思路如下:** 1). 改造 DishController 的 list 方法,先从 Redis 中获取分类对应的菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据存入 Redis。 2). 改造 DishController 的 save 和 update 方法,加入清理缓存的逻辑。 > 注意: > > 在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况。 ### 3.2 代码改造 需要改造的代码为: DishController #### 3.2.1 查询菜品缓存 | 改造的方法 | redis 的数据类型 | redis 缓存的 key | redis 缓存的 value | | ---------- | ---------------- | ---------------------------------------------- | ------------------ | | list | string | dish**分类 Id**状态 , 比如: dish_12323232323_1 | List | **1). 在 DishController 中注入 RedisTemplate** ```java @Autowired private RedisTemplate redisTemplate; ``` **2). 在 list 方法中,查询数据库之前,先查询缓存, 缓存中有数据, 直接返回** ```java List<DishDto> dishDtoList = null; //动态构造key String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//dish_1397844391040167938_1 //先从redis中获取缓存数据 dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key); if(dishDtoList != null){ //如果存在,直接返回,无需查询数据库 return R.success(dishDtoList); } ``` ![](https://blog.fivk.cn/usr/uploads/2023/09/3795890860.png) **3). 如果 redis 不存在,查询数据库,并将数据库查询结果,缓存在 redis,并设置过期时间** ```java //如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES); ``` ![](https://blog.fivk.cn/usr/uploads/2023/09/3772749748.png) #### 3.2.2 清理菜品缓存 为了保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。所以,我们需要在添加菜品、更新菜品时清空缓存数据。 **1). 保存菜品,清空缓存** 在保存菜品的方法 save 中,当菜品数据保存完毕之后,需要清空菜品的缓存。那么这里清理菜品缓存的方式存在两种: A. 清理所有分类下的菜品缓存 ```java //清理所有菜品的缓存数据 Set keys = redisTemplate.keys("dish_*"); //获取所有以dish_xxx开头的key redisTemplate.delete(keys); //删除这些key ``` B. 清理当前添加菜品分类下的缓存 ```java //清理某个分类下面的菜品缓存数据 String key = "dish_" + dishDto.getCategoryId() + "_1"; redisTemplate.delete(key); ``` 此处, 我们推荐使用第二种清理的方式, 只清理当前菜品关联的分类下的菜品数据。 ![](https://blog.fivk.cn/usr/uploads/2023/09/3624591833.png) **2). 更新菜品,清空缓存** 在更新菜品的方法 update 中,当菜品数据更新完毕之后,需要清空菜品的缓存。这里清理缓存的方式和上述基本一致。 A. 清理所有分类下的菜品缓存 ```java //清理所有菜品的缓存数据 Set keys = redisTemplate.keys("dish_*"); //获取所有以dish_xxx开头的key redisTemplate.delete(keys); //删除这些key ``` B. 清理当前添加菜品分类下的缓存 ```java //清理某个分类下面的菜品缓存数据 String key = "dish_" + dishDto.getCategoryId() + "_1"; redisTemplate.delete(key); ``` ![](https://blog.fivk.cn/usr/uploads/2023/09/334848676.png) <div class="tip inlineBlock error"> 注意: 在这里我们推荐使用第一种方式进行清理,这样逻辑更加严谨。 因为对于修改操作,用户是可以修改菜品的分类的,如果用户修改了菜品的分类,那么原来分类下将少一个菜品,新的分类下将多一个菜品,这样的话,两个分类下的菜品列表数据都发生了变化。 </div> ### 3.3 功能测试 代码编写完毕之后,重新启动服务。 1). 访问移动端,根据分类查询菜品列表,然后再检查 Redis 的缓存数据,是否可以正常缓存; ![](https://blog.fivk.cn/usr/uploads/2023/09/3495140950.png) 我们也可以在服务端,通过 debug 断点的形式一步一步的跟踪代码的执行。 2). 当我们在进行新增及修改菜品时, 查询 Redis 中的缓存数据, 是否被清除; ### 3.4 提交并推送代码 **1). 提交并推送代码** 在 v1.0 分支中, 将我们已经实现并且测试通过的使用 redis 缓存验证码和菜品信息的代码,提交并推送至 Gitee ![](https://blog.fivk.cn/usr/uploads/2023/09/133985707.png) ![](https://blog.fivk.cn/usr/uploads/2023/09/2086722361.png) **2). 合并代码到 master 分支** A. 将代码切换到 master 分支 ![](https://blog.fivk.cn/usr/uploads/2023/09/3265734611.png) B. 将 v1.0 分支的代码合并到当前 master 分支 ![](https://blog.fivk.cn/usr/uploads/2023/09/2498948335.png) C. 将 master 分支合并后代码推送到 Gitee ![](https://blog.fivk.cn/usr/uploads/2023/09/3206851516.png) ![](https://blog.fivk.cn/usr/uploads/2023/09/3633025926.png) ## 4. SpringCache ### 4.1 介绍 **Spring Cache**是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码。 Spring Cache 只是提供了一层抽象,底层可以切换不同的 cache 实现。具体就是通过**CacheManager**接口来统一不同的缓存技术。CacheManager 是 Spring 提供的各种缓存技术抽象接口。 针对不同的缓存技术需要实现不同的 CacheManager: | **CacheManager** | **描述** | | ------------------- | -------------------------------------- | | EhCacheCacheManager | 使用 EhCache 作为缓存技术 | | GuavaCacheManager | 使用 Google 的 GuavaCache 作为缓存技术 | | RedisCacheManager | 使用 Redis 作为缓存技术 | ### 4.2 注解 在 SpringCache 中提供了很多缓存操作的注解,常见的是以下的几个: | **注解** | **说明** | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | | @EnableCaching | 开启缓存注解功能 | | @Cacheable | 在方法执行前 spring 先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 | | @CachePut | 将方法的返回值放到缓存中 | | @CacheEvict | 将一条或多条数据从缓存中删除 | 在 spring boot 项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching 开启缓存支持即可。 例如,使用 Redis 作为缓存技术,只需要导入 Spring data Redis 的 maven 坐标即可。 ### 4.3 入门程序 接下来,我们将通过一个入门案例来演示一下 SpringCache 的常见用法。 上面我们提到,SpringCache 可以集成不同的缓存技术,如 Redis、Ehcache 甚至我们可以使用 Map 来缓存数据, 接下来我们在演示的时候,就先通过一个 Map 来缓存数据,最后我们再换成 Redis 来缓存。 #### 4.3.1 环境准备 **1). 数据库准备** 将今天资料中的 SQL 脚本直接导入数据库中。 ![](https://blog.fivk.cn/usr/uploads/2023/09/2549749835.png) **2). 导入基础工程** 基础环境的代码,在我们今天的资料中已经准备好了, 大家只需要将这个工程导入进来就可以了。导入进来的工程结构如下: ![](https://blog.fivk.cn/usr/uploads/2023/09/865198943.png) 由于 SpringCache 的基本功能是 Spring 核心(spring-context)中提供的,所以目前我们进行简单的 SpringCache 测试,是可以不用额外引入其他依赖的。 **3). 注入 CacheManager** 我们可以在 UserController 注入一个 CacheManager,在 Debug 时,我们可以通过 CacheManager 跟踪缓存中数据的变化。 ![](https://blog.fivk.cn/usr/uploads/2023/09/3135898984.png) 我们可以看到 CacheManager 是一个接口,默认的实现有以下几种 ; ![](https://blog.fivk.cn/usr/uploads/2023/09/3114893653.png) 而在上述的这几个实现中,默认使用的是 ConcurrentMapCacheManager。稍后我们可以通过断点的形式跟踪缓存数据的变化。 **4). 引导类上加@EnableCaching** 在引导类上加该注解,就代表当前项目开启缓存注解功能。 ![](https://blog.fivk.cn/usr/uploads/2023/09/2779952615.png) #### 4.3.2 @CachePut 注解 > @CachePut 说明: > > 作用: 将方法返回值,放入缓存 > > value: 缓存的名称, 每个缓存名称下面可以有很多 key > > key: 缓存的 key ----------> 支持 Spring 的表达式语言 SPEL 语法 **1). 在 save 方法上加注解@CachePut** 当前 UserController 的 save 方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在 save 方法上加上注解 @CachePut,用法如下: ```java /** * CachePut:将方法返回值放入缓存 * value:缓存的名称,每个缓存名称下面可以有多个key * key:缓存的key */ @CachePut(value = "userCache", key = "#user.id") @PostMapping public User save(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 ; **2). 测试** 启动服务,通过 postman 请求访问 UserController 的方法, 然后通过断点的形式跟踪缓存数据。 ![](https://blog.fivk.cn/usr/uploads/2023/09/4071958627.png) 第一次访问时,缓存中的数据是空的,因为 save 方法执行完毕后才会缓存数据。 ![](https://blog.fivk.cn/usr/uploads/2023/09/1519891028.png) 第二次访问时,我们通过 debug 可以看到已经有一条数据了,就是上次保存的数据,已经缓存了,缓存的 key 就是用户的 id。 ![](https://blog.fivk.cn/usr/uploads/2023/09/619573328.png) <div class="tip inlineBlock error"> 注意: 上述的演示,最终的数据,实际上是缓存在 ConcurrentHashMap 中,那么当我们的服务器重启之后,缓存中的数据就会丢失。 我们后面使用了 Redis 来缓存就不存在这样的问题了。 </div> #### 4.3.3 @CacheEvict 注解 > @CacheEvict 说明: > > 作用: 清理指定缓存 > > value: 缓存的名称,每个缓存名称下面可以有多个 key > > key: 缓存的 key ----------> 支持 Spring 的表达式语言 SPEL 语法 **1). 在 delete 方法上加注解@CacheEvict** 当我们在删除数据库 user 表的数据的时候,我们需要删除缓存中对应的数据,此时就可以使用@CacheEvict 注解, 具体的使用方式如下: ```java /** * 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); } ``` **2). 测试** 要测试缓存的删除,我们先访问 save 方法 4 次,保存 4 条数据到数据库的同时,也保存到缓存中,最终我们可以通过 debug 看到缓存中的数据信息。 然后我们在通过 postman 访问 delete 方法, 如下: ![](https://blog.fivk.cn/usr/uploads/2023/09/3665942600.png) 删除数据时,通过 debug 我们可以看到已经缓存的 4 条数据: ![](https://blog.fivk.cn/usr/uploads/2023/09/245508667.png) 当执行完 delete 操作之后,我们再次保存一条数据,在保存的时候 debug 查看一下删除的 ID 值是否已经被删除。 ![](https://blog.fivk.cn/usr/uploads/2023/09/891535910.png) **3). 在 update 方法上加注解@CacheEvict** 在更新数据之后,数据库的数据已经发生了变更,我们需要将缓存中对应的数据删除掉,避免出现数据库数据与缓存数据不一致的情况。 ```java //@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(User user){ userService.updateById(user); return user; } ``` 加上注解之后,我们可以重启服务,然后测试方式,基本和上述相同,先缓存数据,然后再更新某一条数据,通过 debug 的形式查询缓存数据的情况。 #### 4.3.4 @Cacheable 注解 > @Cacheable 说明: > > 作用: 在方法执行前,spring 先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 > > value: 缓存的名称,每个缓存名称下面可以有多个 key > > key: 缓存的 key ----------> 支持 Spring 的表达式语言 SPEL 语法 **1). 在 getById 上加注解@Cacheable** ```java /** * Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 * value:缓存的名称,每个缓存名称下面可以有多个key * key:缓存的key */ @Cacheable(value = "userCache",key = "#id") @GetMapping("/{id}") public User getById(@PathVariable Long id){ User user = userService.getById(id); return user; } ``` **2). 测试** 我们可以重启服务,然后通过 debug 断点跟踪程序执行。我们发现,第一次访问,会请求我们 controller 的方法,查询数据库。后面再查询相同的 id,就直接获取到数据库,不用再查询数据库了,就说明缓存生效了。 ![](https://blog.fivk.cn/usr/uploads/2023/09/1191850388.png) 当我们在测试时,查询一个数据库不存在的 id 值,第一次查询缓存中没有,也会查询数据库。而第二次再查询时,会发现,不再查询数据库了,而是直接返回,那也就是说如果根据 ID 没有查询到数据,那么会自动缓存一个 null 值。 我们可以通过 debug,验证一下: ![](https://blog.fivk.cn/usr/uploads/2023/09/1842400119.png) 我们能不能做到,当查询到的值不为 null 时,再进行缓存,如果为 null,则不缓存呢? 答案是可以的。 **3). 缓存非 null 值** 在@Cacheable 注解中,提供了两个属性分别为: condition, unless 。 > condition : 表示满足什么条件, 再进行缓存 ; > > unless : 表示满足条件则不缓存 ; 与上述的 condition 是反向的 ; 具体实现方式如下: ```java /** * Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 * value:缓存的名称,每个缓存名称下面可以有多个key * key:缓存的key * condition:条件,满足条件时才缓存数据 * unless:满足条件则不缓存 */ @Cacheable(value = "userCache",key = "#id", unless = "#result == null") @GetMapping("/{id}") public User getById(@PathVariable Long id){ User user = userService.getById(id); return user; } ``` <div class="tip inlineBlock error"> 注意: 此处,我们使用的时候只能够使用 unless, 因为在 condition 中,我们是无法获取到结果 #result 的。 </div> **4). 在 list 方法上加注解@Cacheable** 在 list 方法中进行查询时,有两个查询条件,如果传递了 id,根据 id 查询; 如果传递了 name, 根据 name 查询,那么我们缓存的 key 在设计的时候,就需要既包含 id,又包含 name。 具体的代码实现如下: ```java @Cacheable(value = "userCache",key = "#user.id + '_' + #user.name") @GetMapping("/list") public List<User> list(User user){ LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(user.getId() != null,User::getId,user.getId()); queryWrapper.eq(user.getName() != null,User::getName,user.getName()); List<User> list = userService.list(queryWrapper); return list; } ``` 然后再次重启服务,进行测试。 ![](https://blog.fivk.cn/usr/uploads/2023/09/3755696821.png) 第一次查询时,需要查询数据库,在后续的查询中,就直接查询了缓存,不再查询数据库了。 ### 4.4 集成 Redis 在使用上述默认的 ConcurrentHashMap 做缓存时,服务重启之后,之前缓存的数据就全部丢失了,操作起来并不友好。在项目中使用,我们会选择使用 redis 来做缓存,主要需要操作以下几步: 1). pom.xml ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 2). application.yml ```yml spring: redis: host: 192.168.200.200 port: 6379 password: root@123456 database: 0 cache: redis: time-to-live: 1800000 #设置缓存过期时间,可选 ``` 3). 测试 重新启动项目,通过 postman 发送根据 id 查询数据的请求,然后通过 redis 的图形化界面工具,查看 redis 中是否可以正常的缓存数据。 ![](https://blog.fivk.cn/usr/uploads/2023/09/2563977245.png) ![](https://blog.fivk.cn/usr/uploads/2023/09/86066016.png) ## 5. 缓存套餐数据 ### 5.1 实现思路 前面我们已经实现了移动端套餐查看功能,对应的服务端方法为 SetmealController 的 list 方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。 具体的实现思路如下: 1). 导入 Spring Cache 和 Redis 相关 maven 坐标 2). 在 application.yml 中配置缓存数据的过期时间 3). 在启动类上加入@EnableCaching 注解,开启缓存注解功能 4). 在 SetmealController 的 list 方法上加入@Cacheable 注解 5). 在 SetmealController 的 save 和 delete 方法上加入 CacheEvict 注解 ### 5.2 缓存套餐数据 #### 5.2.1 代码实现 1). pom.xml 中引入依赖 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> ``` <div class="tip inlineBlock error"> 备注: spring-boot-starter-data-redis 这个依赖前面已经引入了, 无需再次引入。 </div> 2). application.yml 中设置缓存过期时间 ```yml spring: cache: redis: time-to-live: 1800000 #设置缓存数据的过期时间 ``` 3). 启动类上加入@EnableCaching 注解 ![](https://blog.fivk.cn/usr/uploads/2023/09/3768275222.png) 4). SetmealController 的 list 方法上加入@Cacheable 注解 在进行套餐数据查询时,我们需要根据分类 ID 和套餐的状态进行查询,所以我们在缓存数据时,可以将套餐分类 ID 和套餐状态组合起来作为 key,如: 1627182182_1 (1627182182 为分类 ID,1 为状态)。 ```java /** * 根据条件查询套餐数据 * @param setmeal * @return */ @GetMapping("/list") @Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status") public R<List<Setmeal>> list(Setmeal setmeal){ LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId()); queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus()); queryWrapper.orderByDesc(Setmeal::getUpdateTime); List<Setmeal> list = setmealService.list(queryWrapper); return R.success(list); } ``` #### 5.2.2 测试 缓存数据的代码编写完毕之后,重新启动服务,访问移动端进行测试,我们登陆之后在点餐界面,点击某一个套餐分类,查询套餐列表数据时,服务端报错了,错误信息如下: ![](https://blog.fivk.cn/usr/uploads/2023/09/2546387417.png) ![](https://blog.fivk.cn/usr/uploads/2023/09/2384638227.png) <div class="tip inlineBlock warning"> 为什么会报出这个错误呢? </div> 因为 @Cacheable 会将方法的返回值 R 缓存在 Redis 中,而在 Redis 中存储对象,该对象是需要被序列化的,而对象要想被成功的序列化,就必须得实现 Serializable 接口。而当前我们定义的 R,并未实现 Serializable 接口。所以,要解决该异常,只需要让 R 实现 Serializable 接口即可。如下: ![](https://blog.fivk.cn/usr/uploads/2023/09/596210315.png) 修复完毕之后,再次重新测试,访问套餐分类下对应的套餐列表数据后,我们会看到 Redis 中确实可以缓存对应的套餐列表数据。 ![](https://blog.fivk.cn/usr/uploads/2023/09/75587975.png) ### 5.3 清理套餐数据 #### 5.3.1 代码实现 为了保证数据库中数据与缓存数据的一致性,在我们添加套餐或者删除套餐数据之后,需要清空当前套餐缓存的全部数据。那么@CacheEvict 注解如何清除某一份缓存下所有的数据呢,这里我们可以指定@CacheEvict 中的一个属性 allEnties,将其设置为 true 即可。 **1). 在 delete 方法上加注解@CacheEvict** ```java /** * 删除套餐 * @param ids * @return */ @DeleteMapping @CacheEvict(value = "setmealCache",allEntries = true) //清除setmealCache名称下,所有的缓存数据 public R<String> delete(@RequestParam List<Long> ids){ log.info("ids:{}",ids); setmealService.removeWithDish(ids); return R.success("套餐数据删除成功"); } ``` **2). 在 delete 方法上加注解@CacheEvict** ```java /** * 新增套餐 * @param setmealDto * @return */ @PostMapping @CacheEvict(value = "setmealCache",allEntries = true) //清除setmealCache名称下,所有的缓存数据 public R<String> save(@RequestBody SetmealDto setmealDto){ log.info("套餐信息:{}",setmealDto); setmealService.saveWithDish(setmealDto); return R.success("新增套餐成功"); } ``` #### 5.3.2 测试 代码编写完成之后,重启工程,然后访问后台管理系统,对套餐数据进行新增 以及 删除, 然后通过 Redis 的图形化界面工具,查看 Redis 中的套餐缓存是否已经被删除。 ### 5.4 提交推送代码 到目前为止,我们已经在 v1.0 这个分支中完成了套餐数据的缓存,接下来我们就需要将代码提交并推送到远程仓库。 ![](https://blog.fivk.cn/usr/uploads/2023/09/3562159890.png) 然后,在 idea 中切换到 master 分支,然后将 v1.0 分支的代码合并到 master。 ![](https://blog.fivk.cn/usr/uploads/2023/09/2633274582.png) 再将合并后的 master 分支的代码,推送到远程仓库。 ![](https://blog.fivk.cn/usr/uploads/2023/09/3708988923.png) 最后修改:2023 年 09 月 06 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏