瑞吉外卖优化01-缓存优化

本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集

问题:用户数量多,系统访问量大。频繁访问数据库,系统性能下降,用户体验差。解决方法:使用缓存。用户点链接后,服务端先看缓存中有没有相应数据。如果有数据,不用再查数据库。

1. 使用git管理代码

在gitee上新建仓库,名称和当前项目名相同,权限为私有仓库。

1.1 将本地代码推送到仓库

  • 复制远程仓库链接。

  • 打开IDEA,在菜单选中VCS,选择Import into version control,选择create git repository,选中项目。

  • .gitignore文件中,排除一些文件(除了IDEA自动生成的,还添加了以下的)

    1
    2
    3
    4
    5
    6
    7
    8
    .git
    logs
    rebel.xml
    target/
    !.mvn/wrapper/maven-wrapper.jar
    log.path_IS_UNDEFINED
    .DS_Store
    offline_user.md
  • 右键项目,点击Git,点击Add。

  • 点击IDEA上方工具的Commit,可以加入注释“加入redis,缓存验证码和菜品”,点击Commit and Push

  • push时,点击Define remote,将刚才复制的远程仓库链接粘到url中,配置远程仓库。

1.2 创建新的分支,把缓存相关优化都放在新的分支上

点击IDEA右下角的分支,新建分支命名为v1.0。

将v1.0分支也推送到远程仓库,当前代码没有改,直接点工具中的Push即可。

2. 环境搭建

2.1 maven坐标

在项目的pom. xml文件中,导入spring data redis的maven坐标:

1
2
3
4
5
<!--导入Redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 配置文件

在项目的application.yml中加入redis相关配置:

1
2
3
4
5
6
spring:
redis:
host: localhost # 本地ip或者虚拟机IP。此处使用了本地的
port: 6379
password:
database: 0 # 默认使用0号数据库

2.3 配置类

在config包下创建RedisConfig配置类,复制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
@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.setHashKeySerializer(new StringRedisSerializer());

redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

以上操作完成后,可以Add、Commit、push到远程仓库,附上Commit Message:“Redis环境配置”。

3. 缓存短信验证码

3.1 实现思路

前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的(有效期30分钟)。

现在需要改造为:将验证码缓存在Redis中(有效期设置为5分钟),具体的实现思路如下:

  1. 在服务端UserController中注入RedisTemplate对象,用于操作Redis
  2. 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
  3. 在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码

3.2 代码改造

1、在服务端UserController中注入RedisTemplate对象,用于操作Redis

1
2
@Autowired
private RedisTemplate redisTemplate;

2、在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟

1
2
// session.setAttribute(phone, code);
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

3、在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码

1
2
3
4
5
6
7
8
// 之前:从session中获取 刚才sendMsg()时session中保存的 该手机号对应的验证码
// Object codeInSession = session.getAttribute(phone);

// 缓存优化:从Redis中获取缓存的验证码
Object codeInSession = redisTemplate.opsForValue().get(phone);

// 如果登录成功则删除Redis中的验证码
redisTemplate.delete(phone);

4. 缓存菜品数据

4.1 实现思路

前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件(categoryId和status)进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。

具体的实现思路如下:

1、改造DishController的list方法,先从Redis中获取菜品数据。如果有,则直接返回,无需查询数据库;如果没有,则查询数据库,并将查询到的菜品数据放入Redis。

2、改造DishController的save和update方法,加入清理缓存的逻辑。后台系统新增菜品或更新菜品信息后,清理缓存,用户点击这个分类后,能看到更新后的菜品。

3、(补充功能)由于我们前面自己补充了启售/停售功能和删除菜品功能,所以此处也需要改造DishController的switchStatus和delete方法。在商家后台启售/停售或者删除菜品后,也要清理缓存。

注意事项:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。

4.2 代码改造

1、改造DishController的list方法,先从Redis中获取菜品数据。需要按照分类,分别缓存。

如果有,则直接返回,无需查询数据库;

如果没有,则查询数据库,并将查询到的菜品数据放入Redis。

先在DishController中注入RedisTemplate对象,用于操作Redis:

1
2
@Autowired
private RedisTemplate redisTemplate;

list方法中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<DishDto> dishDtoList = null; // 希望从redis缓存获取到的是List<DishDto>类型的菜品数据
// 动态地构造key
String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus(); // key为dish_1397844263642378242_1

// 先从Redis中获取菜品缓存数据
dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);

//如果存在,则直接返回,无需查询数据库
if (dishDtoList == null) {
return R.success(dishDtoList); // 直接将从redis中拿到的菜品数据返回给前端
}
//如果不存在,执行后续代码,查询数据库

// 后续代码...

//如果Redis中不存在菜品缓存数据dishDtoList,还需要将查询到的菜品数据缓存到Redis,缓存60分钟
redisTemplate.opsForValue().set(key, dishDtoList, 60, TimeUnit.MINUTES);

2、改造DishController的save和update方法。后台系统新增菜品或更新菜品信息后,清理缓存,用户点击这个分类后,能看到更新后的菜品。

在DishController的save和update方法都添加:

1
2
3
4
5
6
7
//方法一:清理所有菜品缓存数据
//Set keys = redisTemplate.keys("dish_*");
//redisTemplate.delete(keys);

//方法二:清理某个分类下的菜品缓存数据
String key = "dish_" + dishDto.getCategoryId() + "_" + dishDto.getStatus();
redisTemplate.delete(key);

3、由于我们前面自己补充了启售/停售功能和删除菜品功能,所以此处也需要改造DishController的switchStatus和delete方法。在商家后台启售/停售或者删除菜品后,也要清理缓存。

DishController的switchStatus方法添加清理缓存的功能,该方法完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 修改启售/停售状态,支持批量修改和单独修改
* 处理请求/dish/status/1?ids=1413384757047271425,1413385247889891330
* @param status
* @param ids 多个dish_id
* @return
*/
@PostMapping("/status/{status}")
public R<String> switchStatus(@PathVariable int status, String[] ids){
for(String id : ids){
Dish dish = dishService.getById(id);//通过遍历到的这个当前id,获取dish表中的一条记录(一个dish对象)
dish.setStatus(status);//将该记录的status改为url路径参数中的status
dishService.updateById(dish);

//清理所有菜品缓存数据
Set keys = redisTemplate.keys("dish_*");
redisTemplate.delete(keys);
}
return R.success("状态修改成功");
}

DishController的delete方法添加清理缓存的功能,该方法完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 删除菜品
* 请求/dish?ids=1630924768242917378
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(String[] ids){
for (String id : ids){
dishService.removeById(id);

//清理所有菜品缓存数据
Set keys = redisTemplate.keys("dish_*");
redisTemplate.delete(keys);
}
return R.success("菜品删除成功");
}

4.3 提交仓库

最后,将代码提交到Git本地仓库并推送到远程仓库。

图片

并且经过测试没有问题之后,可以将v1.0分支合并至主分支。

图片

再切回v1.0分支,继续完成其他代码。

图片

5. Spring Cache

5.1 Spring Cache 介绍

Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。

CacheManager是Spring提供的各种缓存技术抽象接口。

针对不同的缓存技术需要实现不同的CacheManager:

图片

5.2 Spring Cache 常用注解

图片

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

5.3 Spring Cache 使用方式-使用map

下面是使用map为例演示这几个注解的使用:

1、准备工作:

  • 启动类上加@EnableCaching开启缓存注解功能:

图片

  • 使用前还需要在Controller类中注入CacheManager对象:

图片

2、@CachePut将方法的返回值放到缓存中:

图片

3、@CacheEvict将一条或名条数据从缓存中删除:

图片

图片

4、@Cacheable在方法执行前Spring先查看缓存中是否有数据,如果有数据,则(不经过方法)直接返回缓存中的数据;若没有数据,再调用方法并将方法返回值放到缓存中。:

图片

5.4 Spring Cache 使用方式-使用Redis作为缓存产品

在Spring Boot项目中使用Spring Cache的操作步骤(使用redis缓存技术);

1、导入maven坐标

spring-boot-starter-data-redis和spring-boot-starter-cache:

1
2
3
4
5
6
7
8
9
<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>

2、配置application.yml

1
2
3
4
spring:
cache:
redis:
time-to-live: 1800000 # 设置缓存有效期,非必需,如果没设置就是永久有效

3、在启动类上加@EnableCaching注解,开启缓存注解功能

4、在Controller的方法上加@Cacheable、@CacheEvict等注解,进行缓存操作(同5.3中注解的使用方法)

6. 缓存套餐数据

6.1 实现思路

前面我们已经实现了移动端套餐查看功能,对应的服务端方法为SetmealController的list方法。此方法会根据前端提交的查询条件,进行数据库查询操作。

在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。

具体的实现思路如下:

1、导入Spring Cache和Redis相关maven坐标

2、在application.yml中配置缓存数据的过期时间

3、在启动类上加入@EnableCaching注解,开启缓存注解功能

4、在SetmealController的list方法上加@Cacheable注解。

@Cacheable注解可以实现:在方法执行前Spring先查看缓存中是否有数据,如果有数据,则(不经过方法)直接返回缓存中的数据;若没有数据,再调用方法并将方法返回值放到缓存中。

5、在SetmealController的save和delete方法上加@CacheEvict注解

@CacheEvict注解可以实现:将一条或名条数据从缓存中删除。

6.2 代码改造

1、在pom.xml文件中导入maven坐标:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2、在application.yml中配置缓存数据过期时间:

1
2
3
4
spring:
cache:
redis:
time-to-live: 1800000 # 设置缓存有效期,非必需,如果没设置就是永久有效

3、在启动类上添加@EnableCaching注解,开启缓存注解功能

1
2
3
4
5
6
7
8
9
10
11
@ServletComponentScan   //  加上这个注解后,才会扫描@WebFilter注解,使得过滤器生效
@Slf4j //加上这个注解后,可以在主程序中使用log.xxxx方法,便于打印日志
@SpringBootApplication
@EnableTransactionManagement //和@Transactional一起,开启事务
@EnableCaching //开启缓存注解功能
public class TimingFoodDeliveryApplication {
public static void main(String[] args) {
SpringApplication.run(TimingFoodDeliveryApplication.class, args);
log.info("项目启动成功");
}
}

4、在SetmealController的list方法上加@Cacheable注解,向redis中添加缓存:

1
@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status") //根据查询条件,拼接出key

5、在SetmealController的save、delete、update、switchStatus等方法上加@CacheEvict注解,清除缓存:

1
@CacheEvict(value = "setmealCache", allEntries = true)

allEntries = true,则会删除value(setmealCache)对应的所有缓存数据。

注意:要让R实现Serializable接口(序列化),注解才能生效。

1
2
3
4
@Data
public class R<T> implements Serializable {
// R中原本的代码
}

6.3 提交仓库

提交:将代码提交到Git本地仓库并推送到远程仓库。

图片

合并:经过测试没有问题之后,可以先切换(checkout)回master分支,再将v1.0分支合并至主分支。

图片