一、系统问题&解决方法
1). 当前系统存在的问题
之前我们已经实现了移动端菜品展示、点餐、购物车、下单等功能,但是由于移动端是面向所有的消费者的,请求压力相对比较大,而我们当前所有的数据查询都是从数据库MySQL中直接查询的,那么可能就存在如下问题: ==频繁访问数据库,数据库访问压力大,系统性能下降,用户体验较差。==
2). 解决该问题的方法
要解决我们上述提到的问题,就可以通过Redis来做缓存,从而降低数据库的访问压力,提高系统的访问性能,从而提升用户体验。
加入Redis做缓存之后,我们在进行数据查询时,就需要先查询缓存,如果缓存中有数据,直接返回,如果缓存中没有数据,则需要查询数据库,再将数据库查询的结果,缓存在redis中。
二、 环境搭建
2.1 版本控制
接下来,我们就需要对我们的功能进行优化,但是需要说明的是,我们不仅仅要对上述提到的缓存进行优化,还需要对我们程序的各个方面进行优化。我们本章节主要是针对于缓存进行优化,为了方便的对我们各个优化版本的代码进行管理,我们使用Git来控制代码版本。 那么此时我们就需要将我们之前开发完成的代码提交到Git,并且推送到码云Gitee的远程仓库,执行步骤如下:
1). 创建Gitee远程仓库
2). idea-创建本地仓库
3). 准备忽略文件.gitignore
在我们的项目中, 有一些文件是无需提交的到git,比如: .idea,target/,*.iml等。
忽略文件代码
1 | .git |
4). idea-提交并推送本地代码
A. 添加项目文件进暂存区
B. 提交代码
C.将代码推送至远程仓库
5). 查看gitee远程仓库
6). 创建分支
目前默认git中只有一个主分支master,我们接下来进行缓存的优化,就不在master分支来操作了,我们需要在git上创建一个单独的分支v1.0,缓存的优化,我们就在该分支上进行操作。
当前创建的v1.0分支,是基于master分支创建出来的,所以目前master分支的代码, 和v1.0分支的代码是完全一样的,接下来把v1.0的代码也推送至远程仓库。
7). 推送分支代码到远程
2.2 环境准备
1). 在项目的pom.xml文件中导入spring data redis的maven坐标
1 | <dependency> |
2). 在项目的application.yml中加入redis相关配置
1 | redis: |
==注意: 引入上述依赖时,需要注意yml文件前面的缩进,上述配置应该配置在spring层级下面。==
这里如果redis没有设置密码的话直接将password注释就行了
3). 编写Redis的配置类RedisConfig,定义RedisTemplate
1 | import org.springframework.cache.annotation.CachingConfigurerSupport; |
解释说明:
1). 在SpringBoot工程启动时, 会加载一个自动配置类 RedisAutoConfiguration, 在里面已经声明了RedisTemplate这个bean
上述框架默认声明的RedisTemplate用的key和value的序列化方式是默认的 JdkSerializationRedisSerializer,如果key采用这种方式序列化,最终我们在测试时通过redis的图形化界面查询不是很方便,如下形式:
2). 如果使用我们自定义的RedisTemplate, key的序列化方式使用的是StringRedisSerializer, 也就是字符串形式, 最终效果如下:
3). 定义了两个bean会不会出现冲突呢? 答案是不会, 因为源码如下:
三、 缓存短信验证码
3.1 思路分析
前面移动端手机验证码登录随机生成的验证码是保存在HttpSession中的。
但是在实际的业务场景中,一般验证码都是需要设置过期时间的,如果存在HttpSession中就无法设置过期时间,此时就需要对这一块的功能进行优化。
现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:
1). 在服务端UserController中注入RedisTemplate对象,用于操作Redis;
2). 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟;
3). 在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码;
3.2 代码改造
1). 在UserController中注入RedisTemplate对象,用于操作Redis
1 |
|
2). 在UserController的sendMsg方法中,将生成的验证码保存到Redis
1 | //需要将生成的验证码保存到Redis,设置过期时间 |
3). 在UserController的login方法中,从Redis中获取生成的验证码,如果登录成功则删除Redis中缓存的验证码
1 | //从Redis中获取缓存的验证码 |
1 | //从Redis中删除缓存的验证码 |
3.3 功能测试
1). 访问前端工程,获取验证码
后端验证码已经发过来了
2).查看Redis中的数据
在Redis中查看发现可以查到
3). 在登录界面填写验证码登录完成后,查看Redis中的数据是否删除
登录成功后再查询就已经查不到了,数据已经正常删除了
四、 缓存菜品信息
4.1 实现思路
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件(categoryId)进行数据库查询操作。
在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。
那么,我们又需要思考一个问题, 具体缓存几份数据呢, 所有的菜品缓存一份 , 还是说需要缓存多份呢? 我们可以看一下我们之前做的移动端效果:
我们点击哪一个分类,展示的就是该分类下的菜品, 其他菜品无需展示。所以,这里面我们在缓存时,
可以根据菜品的分类,缓存多份数据,页面在查询时,点击的是哪个分类,我们就查询该分类下的菜品缓存数据。
具体的实现思路如下:
1). 改造DishController的list方法,先从Redis中获取分类对应的菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据存入Redis。
2). 改造DishController的save和update方法,加入清理缓存的逻辑。
注意:
在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况。
4.2 代码改造
需要改造的代码为: DishController
1. 查询菜品缓存
改造的方法 | redis的数据类型 | redis缓存的key | redis缓存的value |
---|---|---|---|
list | string | dish_分类Id_状态 , 比如: dish_12323232323_1 | List |
1). 在DishController中注入RedisTemplate
1 |
|
2). 在list方法中,查询数据库之前,先查询缓存, 缓存中有数据, 直接返回
1 | List<DishDto> dishDtoList = null; |
3). 如果redis不存在,查询数据库,并将数据库查询结果,缓存在redis,并设置过期时间
1 | //如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis |
测试
第二次访问页面时数据已经存入了Redis
2 清理菜品缓存
为了保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。所以,我们需要在添加菜品、更新菜品时清空缓存数据。
1). 保存菜品,清空缓存
在保存菜品的方法save中,当菜品数据保存完毕之后,需要清空菜品的缓存。那么这里清理菜品缓存的方式存在两种:
A. 清理所有分类下的菜品缓存
1 | //清理所有菜品的缓存数据 |
B. 清理当前添加菜品分类下的缓存
1 | //清理某个分类下面的菜品缓存数据 |
此处, 我们推荐使用第二种清理的方式, 只清理当前菜品关联的分类下的菜品数据。
2). 更新菜品,清空缓存
在更新菜品的方法update中,当菜品数据更新完毕之后,需要清空菜品的缓存。这里清理缓存的方式和上述基本一致。
A. 清理所有分类下的菜品缓存
1 | //清理所有菜品的缓存数据 |
B. 清理当前添加菜品分类下的缓存
1 | //清理某个分类下面的菜品缓存数据 |
测试:
修改菜品数据前
修改数据
修改数据后,少了一条数据
4.3 合并分支
切换回master分支,然后和v1.0合并
五、 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 注解详解
1.@EnableCaching
在引导类上加该注解,就代表当前项目开启缓存注解功能。
2.@CachePut注解
@CachePut 说明:
- 作用: 将方法返回值,放入缓存
- value: 缓存的名称, 每个缓存名称下面可以有很多key
- key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法
在save方法上加注解@CachePut
当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:
1 | /** |
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 ;
3. @CacheEvict注解
@CacheEvict 说明:
作用: 清理指定缓存
value: 缓存的名称,每个缓存名称下面可以有多个key
key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法
1). 在 delete 方法上加注解@CacheEvict
当我们在删除数据库user表的数据的时候,我们需要删除缓存中对应的数据,此时就可以使用@CacheEvict注解, 具体的使用方式如下:
1 | /** |
2). 在 update 方法上加注解@CacheEvict
在更新数据之后,数据库的数据已经发生了变更,我们需要将缓存中对应的数据删除掉,避免出现数据库数据与缓存数据不一致的情况。
1 | //@CacheEvict(value = "userCache",key = "#p0.id") //第一个参数的id属性 |
加上注解之后,我们可以重启服务,然后测试方式,基本和上述相同,先缓存数据,然后再更新某一条数据,通过debug的形式查询缓存数据的情况。
4. @Cacheable注解
@Cacheable 说明:
作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
value: 缓存的名称,每个缓存名称下面可以有多个key
key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法
1). 在getById上加注解@Cacheable
1 | /** |
2). 缓存非null值
在@Cacheable注解中,提供了两个属性分别为: condition, unless 。
condition : 表示满足什么条件, 再进行缓存 ;
unless : 表示满足条件则不缓存 ; 与上述的condition是反向的 ;
具体实现方式如下:
1 | /** |
==注意: 此处,我们使用的时候只能够使用 unless, 因为在condition中,我们是无法获取到结果 #result的。==
3). 在list方法上加注解@Cacheable
在list方法中进行查询时,有两个查询条件,如果传递了id,根据id查询; 如果传递了name, 根据name查询,那么我们缓存的key在设计的时候,就需要既包含id,又包含name。 具体的代码实现如下:
1 |
|
4.4 集成Redis
在使用上述默认的ConcurrentHashMap做缓存时,服务重启之后,之前缓存的数据就全部丢失了,操作起来并不友好。
在项目中使用,我们会选择使用redis来做缓存,主要需要操作以下几步:
1). pom.xml
1 | <dependency> |
2). application.yml
1 | spring: |
六、缓存套餐数据
前面实现了移动端套餐查看功能,对应的服务端方法为SetmealController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。
在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。
现在需要对此方法进行缓存优化,提高系统的性能。
具体的实现思路如下:
1). 导入Spring Cache和Redis相关maven坐标
2). 在application.yml中配置缓存数据的过期时间
3). 在启动类上加入@EnableCaching注解,开启缓存注解功能
4). 在SetmealController的list方法上加入@Cacheable注解
5). 在SetmealController的save和delete方法上加入CacheEvict注解
6.1 缓存套餐数据
代码实现
1). pom.xml中引入依赖
1 | <dependency> |
==备注: spring-boot-starter-data-redis 这个依赖前面已经引入了, 无需再次引入。==
2). application.yml中设置缓存过期时间
1 | spring: |
4). SetmealController的list方法上加入@Cacheable注解
在进行套餐数据查询时,我们需要根据分类ID和套餐的状态进行查询,所以我们在缓存数据时,可以将套餐分类ID和套餐状态组合起来作为key,如: 1627182182_1 (1627182182为分类ID,1为状态)。
1 | /** |
==注意:因为 @Cacheable 会将方法的返回值R缓存在Redis中,而在Redis中存储对象,该对象是需要被序列化的,而对象要想被成功的序列化,就必须得实现 Serializable 接口。而当前我们定义的R,并未实现 Serializable 接口。==
所以,要解决该异常,只需要让R实现 Serializable 接口即可。如下:
6.2 清理套餐数据
代码实现
为了保证数据库中数据与缓存数据的一致性,在我们添加套餐或者删除套餐数据之后,需要清空当前套餐缓存的全部数据。那么@CacheEvict注解如何清除某一份缓存下所有的数据呢,这里我们可以指定@CacheEvict中的一个属性 allEnties,将其设置为true即可。
1). 在delete方法上加注解@CacheEvict
1 | /** |
2). 在delete方法上加注解@CacheEvict
1 | /** |
6.3 提交推送代码
将代码提交并推送到远程仓库
在idea中切换到master分支
然后将v1.0分支的代码合并到master。
再将合并后的master分支的代码,推送到远程仓库。