08 项目优化——缓存优化

本贴最后更新于 629 天前,其中的信息可能已经物是人非

image

环境搭建

使用 git 管理代码

先创建本地仓库:

image

修改.gitignore 文件:

.git logs rebel.xml target/ !.mvn/wrapper/maven-wrapper.jar log.path_IS_UNDEFINED .DS_Store offline_user.md ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ build/ nbbuild/ dist/ nbdist/ .nb-gradle/ generatorConfig.xml ### nacos ### third-party/nacos/derby.log third-party/nacos/data/ third-party/nacos/work/ file/

加入到暂存区:

image

提交文件:

image

image

定义远程仓库:

imageimage

最后点击 push 推送分支。

创建一个新的分支,不影响主分支进度:

imageimage

推送分支:

image

maven 坐标

image

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

配置文件

image

image​​

缓存短信验证码

实现思路

image

代码实现(修改 UserController)

package com.itheima.reggie.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.itheima.reggie.common.BaseContext; import com.itheima.reggie.common.R; import com.itheima.reggie.entity.User; import com.itheima.reggie.service.UserService; import com.itheima.reggie.utils.SMSUtils; import com.itheima.reggie.utils.ValidateCodeUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpSession; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @author shkstart * @create 2023-06-29 20:02 */ @RestController @Slf4j @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Autowired private RedisTemplate redisTemplate; /** * 发送手机短信验证码。因为前端发送的是json,所以要加上注解。 * @param user * @return */ @PostMapping("/sendMsg") public R<String> sendMsg(@RequestBody User user, HttpSession session){ //获取手机号 String phone = user.getPhone(); if(!StringUtils.isEmpty(phone)){ //生成随机的的4位验证码 String code= ValidateCodeUtils.generateValidateCode(4).toString(); log.info("code={}",code); //调用阿里云提供的短信API服务完成发送短信 // SMSUtils.sendMessage("瑞吉外卖","SMS_461670293",phone,code); //需要将生成的验证码保存到session // session.setAttribute(phone,code); //将生成的验证码缓存到redis中,并且设置有效期为5分钟 redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES); return R.success("手机短信验证码发送成功"); } return R.error("短信发送失败"); } /** * 移动端用户登录 * @param map 用来对应数据 * @return */ @PostMapping("/login") public R<String> login(@RequestBody Map map, HttpSession session){ log.info(map.toString()); //获取手机号 String phone=map.get("phone").toString(); //获取验证码 String code=map.get("code").toString(); //从Session中获取保存的验证码 // Object codeInSession =session.getAttribute(phone); //从redis当中获取缓存的验证码 Object codeInSession =redisTemplate.opsForValue().get(phone); //进行验证码比对(页面提交的验证码和session中保存的验证码比对) if(codeInSession!=null&&codeInSession.equals(code)){ //如果能比对成功,说明登录成功 LambdaQueryWrapper<User> queryWrapper=new LambdaQueryWrapper<>(); queryWrapper.eq(User::getPhone,phone);//根据手机号查对应的值 User user = userService.getOne(queryWrapper); if(user==null){ //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册 user=new User(); user.setPhone(phone); user.setStatus(1); userService.save(user); } //存user就好进入移动端界面 session.setAttribute("user",user.getId()); //如果用户登录成功,删除redis中缓存的验证码 redisTemplate.delete(phone); //不知道为啥传入不了user return R.success("登录成功"); } return R.error("登录失败"); } }

application.yml​文件下:(从 windows 下启动的 redis 没有设置启动密码,所以这里注释掉)

server: port: 8080 spring: application: name: reggie_take_out datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: root password: 2004 redis: host: 127.0.0.1 port: 6379 # password: 123456 database: 0 mybatis-plus: configuration: #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: ASSIGN_ID reggie: path: D:\img

缓存菜品数据

实现思路

image

代码实现

DishController​类下注入:

@Autowired private RedisTemplate redisTemplate;

修改 list​方法:

/** * 根据条件查询对应的菜品数据 * @param dish * @return */ @GetMapping("/list") public R<List<DishDto>> list(Dish dish){ List<DishDto> dishDtoList = null; //动态构造key String key="dish_"+dish.getCategoryId()+"_"+dish.getStatus();//dish_11_1 //先从redis中获取缓存数据 dishDtoList=(List<DishDto>)redisTemplate.opsForValue().get(key); if(dishDtoList!=null){ //如果存在,直接返回,无需查询数据库 return R.success(dishDtoList); } //构造查询条件 LambdaQueryWrapper<Dish> queryWrapper=new LambdaQueryWrapper(); queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId()); queryWrapper.eq(Dish::getStatus,1);//查询起售的菜品 //添加排序条件 queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime); List<Dish> list = dishService.list(queryWrapper); dishDtoList = list.stream().map(item->{ DishDto dishDto=new DishDto(); BeanUtils.copyProperties(item,dishDto); //根据id查询分类,让后台显示数据 Long categoryId=item.getCategoryId(); Category category = categoryService.getById(categoryId); //因为导入的数据有些菜品没有分类名称,所以加个判断以防报错 if(category!=null){ String categoryName =category.getName(); dishDto.setCategoryName(categoryName); } //根据id查询口味数据,追加数据,方便移动端显示 LambdaUpdateWrapper<DishFlavor> LambdaQueryWrapper=new LambdaUpdateWrapper<>(); //查到了菜品还要查它对应的口味数据 LambdaQueryWrapper.eq(DishFlavor::getDishId,item.getId()); List<DishFlavor> dishFlavors = dishFlavorService.list(LambdaQueryWrapper); dishDto.setFlavors(dishFlavors); return dishDto; }).collect(Collectors.toList()); //如果不存在,需要查询数据库,将查询到的菜品数据缓存到redis redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES); return R.success(dishDtoList); }

现在有一个问题是如果我们从后台增加或者修改数据,而移动端读取的缓存数据,并没有更新,读到的是脏数据,所以必须要清理。

此时有两种方案:

  1. 清理所有缓存数据。
  2. 精确性清理,只清理修改了的数据。

DishController​类下修改 update​和 save​方法:

/** * 新增菜品,requestbody是反序列化前端传来的json数据 * @param dishDto * @return */ @PostMapping public R<String> save(@RequestBody DishDto dishDto){ log.info(dishDto.toString()); dishService.addWithFlavor(dishDto); //清理某个分类下面的菜品数据 String key="dish_"+dishDto.getCategoryId()+"_1";//这个1是status,可以写死 redisTemplate.delete(key); return R.success("新增菜品成功"); } /** * 修改菜品,requestbody是反序列化前端传来的json数据 * @param dishDto * @return */ @PutMapping public R<String> update(@RequestBody DishDto dishDto){ log.info(dishDto.toString()); dishService.updateWithFlavor(dishDto); //清理所有菜品的缓存数据 // Set keys = redisTemplate.keys("dish_*"); // redisTemplate.delete(keys); //清理某个分类下面的菜品数据 String key="dish_"+dishDto.getCategoryId()+"_1";//这个1是status,可以写死 redisTemplate.delete(key); return R.success("修改菜品成功"); }

再将代码 push 到 git 上,切换到 master​分支,将 v1.0​合并到主分支上。

image

再切换 v1.0​分支上,继续优化套餐数据。

Spring Cache

spring cache 介绍

image

spring cache 常用注解

image

使用方式

可以注册一个 apifox 账号,模拟发送请求。

缓存的数据是基于内存的,停止服务重启,缓存数据就没有了。

存放缓存数据

/** * CachePut:将方法返回值放入缓存 * value:缓存的名称,每个缓存名称下面可以有多个key * key:缓存的key */ @CachePut(value="userCache",key="#user.id") @PostMapping public User save(User user){ userService.save(user); return user; }

image

清理指定缓存数据

@CacheEvict(value="userCache",key="#id") @DeleteMapping("/{id}") public void delete(@PathVariable Long id){ userService.removeById(id); }

三种写法一样:

image

删除和更新数据都需要清理缓存。

查询缓存

@Cacheable(value="userCache",key="#id",condition = "#result != null") @GetMapping("/{id}") public User getById(@PathVariable Long id){ User user = userService.getById(id); return user; }

condition 满足条件才缓存数据,但是这里没有 result 字段,所以必须要改成:unless = "#result == null"​,表示等于空,不缓存数据。

@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; }

不同的查询条件对应不同的 key,所以要动态拼接。

使用 redis 缓存技术

image

缓存套餐数据

实现思路

image

代码实现

导入 maven 坐标:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>

配置缓存数据过期时间:

image

cache: redis: time-to-live: 1800000 #设置缓存有效期

开启缓存功能:

image

image

R 要做序列化接口,才不会报 500 错误。

public class R<T> implements Serializable{

修改以下方法:

/** * 根据条件查询套餐数据 * @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); }
/** * 删除套餐,并删除掉附带的菜品信息,删除所有缓存数据 * @param ids * @return */ @CacheEvict(value="setmealCache",allEntries = true) @DeleteMapping public R<String> delete(@RequestParam List<Long> ids){ log.info("ids{}",ids); setmealService.removeWithDish(ids); return R.success("套餐数据删除成功"); }
/** * 新增套餐,删除一个分类下的所有缓存数据 * @param setmealDto * @return */ @PostMapping @CacheEvict(value="setmealCache",allEntries = true) public R<String> save(@RequestBody SetmealDto setmealDto){ log.info("套餐信息:{}",setmealDto); setmealService.saveWithDish(setmealDto); return R.success("新增套餐成功!"); }

commit&push 完后,回到 master 分支合并 v1.0。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3194 引用 • 8214 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...