目录
一、缓存菜品模块(代码实现redis缓存)
1、问题分析
2、缓存菜品数据
3、清理缓存数据
为什么在管理员端对缓存数据进行清理?
二、缓存套餐模块(注解实现redis缓存)
1、Spring Cache框架
(1)常用注解
2、用注解方式实现缓存套餐功能
三、购物车模块
1、添加购物车 - POST接口
(1)需求分析
像name、image、amount这些属于冗余字段,为什么要设置冗余字段?
(2)代码开发
2、查看购物车 - GET接口
3、清空购物车 - DELETE接口
4、删除购物车中的一个商品 - POST接口
一、缓存菜品模块(代码实现redis缓存)
1、问题分析
- 用户端小程序展示菜品数据都是通过查询数据库获得,如果用户访问量大,数据库访问压力就会随之增大
- 因此我们通过 Redis 缓存菜品数据,可以减少数据库查询操作,提高效率
2、缓存菜品数据
- 在DishController中加入查询Redis是否存在缓存的判断逻辑
- 若Redis存在缓存数据,直接返回,否则查询数据库并存入Redis
@RestController("userDishController") @RequestMapping("/user/dish") @Slf4j @Api(tags = "C端-菜品浏览接口") public class DishController { @Autowired private DishService dishService; @Autowired private RedisTemplate redisTemplate; /** * 根据分类id查询菜品 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result> list(Long categoryId) { //1.构造redis中的key,规则:dish_分类id String key = "dish_" + categoryId; //2.查询redis是否存在菜品数据 List list = (List) redisTemplate.opsForValue().get(key); if(list != null && list.size() > 0){ //3.如果存在,直接返回,无需查询数据库 return Result.success(list); } //3.如果不存在,查询数据库,将查询的数据存入redis Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key,list); return Result.success(list); } }
3、清理缓存数据
如果没有清理redis缓存,以下功能会出现问题:
- 新增菜品:管理员虽然新增了菜品,但由于redis内旧数据未清理,用户仍然只能看到以前的菜品数据,看不到新增的菜品(controller层查询redis是否有对应数据,如果查到了直接在controller层返回,根本不会去数据库拿新增的数据)
- 删除菜品:管理员虽然在后端删除了菜品,但由于redis内旧数据未清理,用户仍然还能看到被删除的菜品数据
- 修改菜品:管理员虽然修改了菜品,但由于redis内旧数据未清理,用户看到的菜品数据依旧是未更新之前的
- 菜品起售停售:管理员虽然更改了菜品状态,但由于redis内旧数据未清理,用户看到的菜品状态依旧是未更新之前的
所以我们需要在Admin端对缓存数据进行清理
为什么在管理员端对缓存数据进行清理?
因为管理员端需要清理缓存后,再进行增删改,这样用户端才能看到最新数据
下面是增加清理缓存功能的Admin - DishController
/** * 菜品管理 */ @RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; @Autowired private RedisTemplate redisTemplate; /** * 新增菜品 * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result save(@RequestBody DishDTO dishDTO){ log.info("新增菜品:{}",dishDTO); dishService.saveWithFlavor(dishDTO); //清理缓存数据 String key = "dish_" + dishDTO.getCategoryId(); cleanCache(key); return Result.success(); } /** * 菜品分页查询 * @param dishPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("菜品分页查询") public Result page(DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询:{}",dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); } /** * 批量删除菜品 * @param ids * @return */ @DeleteMapping @ApiOperation("批量删除菜品") public Result detele(@RequestParam List ids){ log.info("批量删除:{}",ids); dishService.deleteBatch(ids); //将所有菜品缓存数据清除,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); } /** * 根据id查询菜品 * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result getById(@PathVariable Long id){ log.info("根据id查询菜品:{}",id); DishVO dishVO = dishService.getByIdWithFlavor(id); return Result.success(dishVO); } /** * 修改菜品 * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}",dishDTO); dishService.updateWithFlavor(dishDTO); //将所有菜品缓存数据清除,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); } /** * 菜品起售停售 * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("起售停售菜品") public Result startOrStop(@PathVariable Integer status, Long id){ log.info("起售停售菜品:{},{}",status,id); dishService.startOrStop(status,id); //将所有菜品缓存数据清除,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); } /** * 根据分类id查询菜品 * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result> list(Long categoryId){ List list = dishService.getByCategoryId(categoryId); return Result.success(list); } /** * 清除缓存数据 * @param pattern */ private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); } }
这样,当我们在后端修改、新增、删除、改变菜品状态时,用户端也能将数据及时更新
二、缓存套餐模块(注解实现redis缓存)
1、Spring Cache框架
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:EHCache、Caffeine、Redis(1)常用注解
注解 说明 位置 @EnableCaching 开启缓存注解功能 放启动类上 @Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据则直接返回缓存数据
如果没有缓存数据,调用方法并将方法返回值放到缓存中(既能取数据,又能放缓存)
放方法上 @CachePut 将方法的返回值放到缓存中(只能放缓存) 放方法上 @CacheEvict 将一条或多条数据从缓存中删除 放方法上
2、用注解方式实现缓存套餐功能
步骤:
- 导入Spring Cache和Redis相关maven坐标
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
- 在用户端接口SetmealController的 list 方法上加入@Cacheable注解
- 在管理端接口SetmealController的 save、delete、update、startOrstop等方法上加入CacheEvict注解
(1)导入Spring Cache和Redis的maven
org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-cache
(2)在启动类上加入@EnableCaching注解
(3)用户端接口SetmealController的 list 方法上加入@Cacheable注解
为什么只在【根据分类id查询套餐】方法上加Cache注解?
- 因为该方法是基础查询,用户可能会频繁访问查询套餐,因此需要在该方法上加入Cache注解,而下面【根据套餐id查询菜品】的功能,相对于该方法的访问就没有那么频繁,因此不加Cache注解
/** * 条件查询 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询套餐") //查询前先在redis中查是否有相应数据,如果有,直接返回;如果没有,再从数据库查,查完存入redis缓存 @Cacheable(cacheNames = "setmealCache",key = "#categoryId") //生成key:setmealCache::100 public Result> list(Long categoryId) { Setmeal setmeal = new Setmeal(); setmeal.setCategoryId(categoryId); setmeal.setStatus(StatusConstant.ENABLE); List list = setmealService.list(setmeal); return Result.success(list); }
(4)管理端接口SetmealController的 save、delete、update、startOrstop等方法上加入CacheEvict注解
【1】save
为什么key是setmealDTO.categoryId而不是setmealDTO.id?
- 因为新增套餐前,需要把该套餐【所属分类下的所有套餐】缓存全部删除,否则新增后,用户界面读取的依旧是缓存内的旧数据
- 比如:缓存内 → 分类A[套餐1,套餐2,套餐3],现在管理员新增套餐4,需要先将分类A下的套餐1、2、3缓存数据清理掉,再新增套餐4,此时用户查询分类A下的套餐,后端发现缓存中并没有分类A套餐信息,故从数据库中查询出数据 分类A[套餐1,套餐2,套餐3,套餐4],并将其存入redis缓存,这样用户端就能看到新增的套餐4
- 如果key = setmealDTO.id会怎么样?
- 比如新增套餐4,新增前删除缓存,key为套餐id,即删除【key=套餐4的id】这条缓存,问题是删除的时候套餐4还没加进来,哪里来的缓存?因此我们删除时应该采用分类id,这样删除的是该分类下的所有菜品缓存
/** * 新增套餐 * @param setmealDTO * @return */ @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId") public Result save(@RequestBody SetmealDTO setmealDTO){ log.info("新增套餐:{}",setmealDTO); setmealService.saveWithDish(setmealDTO); return Result.success(); }
【2】delete
/** * 批量删除套餐 * @param ids * @return */ @DeleteMapping @ApiOperation("批量删除套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result delete(@RequestParam List ids){ log.info("批量删除:{}",ids); setmealService.deleteBatch(ids); return Result.success(); }
【3】update
/** * 修改套餐 * @param setmealDTO * @return */ @PutMapping @ApiOperation("修改套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result update(@RequestBody SetmealDTO setmealDTO){ log.info("修改套餐信息:{}",setmealDTO); setmealService.update(setmealDTO); return Result.success(); }
【4】startOrstop
/** * 起售停售套餐 * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("起售停售套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result startOrStop(@PathVariable Integer status,Long id){ log.info("起售停售套餐:{},{}",status,id); setmealService.startOrStop(status,id); return Result.success(); }
三、购物车模块
1、添加购物车 - POST接口
(1)需求分析
像name、image、amount这些属于冗余字段,为什么要设置冗余字段?
设置冗余字段主要是为了用空间换时间,提升查询性能
冗余字段要求:不频繁改变、相对稳定
(2)代码开发
【1】controller层
/** * 添加购物车 * @param shoppingCartDTO * @return */ @PostMapping("/add") @ApiOperation("添加购物车") public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){ log.info("添加购物车:{}",shoppingCartDTO); shoppingCartService.addShoppingCart(shoppingCartDTO); return Result.success(); }
【2】service层
将菜品/套餐加入购物车时,需要进行以下判断:
- 查询购物车中是否存在该菜品/套餐:
- DTO中只含dish_id菜品id、setmeal_id套餐id、dish_flavor口味表,但我们查询时需要通过【user_id】+【菜品/套餐id】,因此我们用实体类存数据,并用线程获取当前userid绑定在实体类中
- 将该实体类传入mapper层进行条件查询
- 如果购物车中存在该菜品/套餐:
- 获取该菜品/套餐数量,然后+1,更新这条购物车记录
- 如果购物车中不存在该菜品/套餐:
- 判断一下添加到是套餐or菜品,然后通过获取菜品表/套餐表的image图片、amount价格、name名字,将其补充至这条购物车记录中(补充冗余数据)
- 接着设置该商品数量为1,并设置记录创建时间
- 最后将这条补充好的记录插入购物车表中
/** * 添加购物车 * @param shoppingCartDTO */ public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) { //1.判断购物车中,该商品是否存在 // 因为DTO中只有dishid、setmealid、dishFlavor,而我们需要userid查询 // 所以将DTO转为实体类,如果数据库表结构变化,只需要调整Entity和转换逻辑,不影响对外的DTO接口,保持接口稳定性 ShoppingCart shoppingCart = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO,shoppingCart); Long currentId = BaseContext.getCurrentId(); shoppingCart.setUserId(currentId); //为什么用list集合? //返回集合是为了接口通用,后面查询购物车也能用,但是这里只会有一个数据 List list = shoppingCartMapper.list(shoppingCart); //2.如果存在,商品数量+1 if(list != null && list.size() > 0){ //每一次请求操作只会传入一条数据,也就是第一条 ShoppingCart cart = list.get(0); cart.setNumber(cart.getNumber() + 1); //更新一下商品数量 shoppingCartMapper.updateNumberById(cart); }else{ //3.如果不存在,添加进购物车 //判断一下新增的是菜品or套餐 Long dishId = shoppingCart.getDishId(); if(dishId != null){ //本次加入购物车的是菜品 Dish dish = dishMapper.getById(dishId); //加入一些原本购物车内没有的元素:照片、价格、菜名 shoppingCart.setImage(dish.getImage()); shoppingCart.setAmount(dish.getPrice()); shoppingCart.setName(dish.getName()); }else{ //本次加入购物车的是套餐 Long setmealId = shoppingCart.getSetmealId(); Setmeal setmeal = setmealMapper.getById(setmealId); shoppingCart.setImage(setmeal.getImage()); shoppingCart.setAmount(setmeal.getPrice()); shoppingCart.setName(setmeal.getName()); } shoppingCart.setNumber(1); //新加入购物车的数量为1 shoppingCart.setCreateTime(LocalDateTime.now()); shoppingCartMapper.insert(shoppingCart); } }
【3】mapper层
@Mapper public interface ShoppingCartMapper { /** * 条件查询 * @param shoppingCart * @return */ List list(ShoppingCart shoppingCart); /** * 修改购物车数量 * @param shoppingCart */ @Update("update sky_take_out.shopping_cart set number = #{number} where id = #{id}") void updateNumberById(ShoppingCart shoppingCart); /** * 插入购物车数据 * @param shoppingCart */ @Insert("insert into sky_take_out.shopping_cart (name,user_id,dish_id,setmeal_id,dish_flavor,number,amount,image,create_time)" + "values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})" ) void insert(ShoppingCart shoppingCart); }
【4】mybatis文件
select * from sky_take_out.shopping_cart and user_id = #{userId} and dish_id = #{dishId} and setmeal_id = #{setmealId} and dish_flavor = #{dishFlavor}
2、查看购物车 - GET接口
【1】controller层
/** * 查看购物车 * @return */ @GetMapping("/list") @ApiOperation("查看购物车") public Result> list(){ List list = shoppingCartService.showShoppingCart(); return Result.success(list); }
【2】service层
/** * 查看购物车 * @return */ public List showShoppingCart() { ShoppingCart shoppingCart = new ShoppingCart(); Long currentId = BaseContext.getCurrentId(); shoppingCart.setUserId(currentId); List list = shoppingCartMapper.list(shoppingCart); return list; }
3、清空购物车 - DELETE接口
【1】controller层
/** * 清空购物车 * @return */ @DeleteMapping("/clean") @ApiOperation("清空购物车") public Result delete(){ shoppingCartService.deleteAll(); return Result.success(); }
【2】service层
/** * 清空购物车 */ public void deleteAll() { Long user_id = BaseContext.getCurrentId(); shoppingCartMapper.deleteAllByUserId(user_id); }
【3】mapper层
/** * 删除当前用户的全部购物车数据 * @param user_id */ @Delete("delete from sky_take_out.shopping_cart where user_id = #{user_id}") void deleteAllByUserId(Long user_id);
4、删除购物车中的一个商品 - POST接口
【1】controller层
/** * 删除购物车的一个商品 * @param shoppingCartDTO * @return */ @PostMapping("/sub") @ApiOperation("删除购物车的一个商品") public Result delete(@RequestBody ShoppingCartDTO shoppingCartDTO){ shoppingCartService.delete(shoppingCartDTO); return Result.success(); }
【2】service层
这里逻辑和添加购物车差不多
- 添加购物车时要先看该商品是否在车里,同理,在删除该商品前,要先看该商品是否在车里
- 如何查询该商品是否在车里?
- 将DTO中存在的【dish_id、setmeal_id、dish_flavor】 + 线程获得的【user_id】加入购物车实体类
- 将这个购物车实体传入mapper层的条件查询list中
- 如果该商品存在车内,那么list查询返回的列表不为空,且商品唯一,我们就进行删除;否则就不需要做删除操作
- 当确定商品存在购物车内,又分两种情况:
- 1、该商品只有1件,此时只需要根据【当前购物车记录id】直接删除这条商品购物车记录即可
- 2、该商品有多件,此时只需要修改商品数量为原来-1即可
/** * 删除购物车的一个商品 * @param shoppingCartDTO */ public void delete(ShoppingCartDTO shoppingCartDTO) { //删除前先看一下购物车内是否有该商品 ShoppingCart shoppingCart = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO,shoppingCart); shoppingCart.setUserId(BaseContext.getCurrentId()); //获取当前匹配的购物车记录(只有一条) List list = shoppingCartMapper.list(shoppingCart); //如果存在该商品,就删掉 if(list != null && list.size() > 0){ shoppingCart = list.get(0); Integer num = shoppingCart.getNumber(); if(num == 1){ //如果该商品只有一个,直接删掉该商品 shoppingCartMapper.deleteById(shoppingCart.getId()); }else{ //其他情况修改份数 shoppingCart.setNumber(num - 1); shoppingCartMapper.updateNumberById(shoppingCart); } } }
【2】mapper层
/** * 根据id删除购物车记录 * @param id */ @Delete("delete from sky_take_out.shopping_cart where id = #{id}") void deleteById(Long id);