前言
今天的内容主要是访问量提升对数据库访问性能影响问题的解决 ——数据缓存。其中spring cache框架提供底层redis的实现。
缓存菜品
缓存套餐
添加购物车
查看购物车
清空购物车
《苍穹外卖day06》:学生党学习笔记——黑马程序员Java项目实战《苍穹外卖》——Day06 HttpClient&微信登录-CSDN博客
视频学习链接黑马程序员Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战_哔哩哔哩_bilibili
资料网盘链接1、黑马程序员Java项目《苍穹外卖》企业级开发实战_免费高速下载|百度网盘-分享无限制
本人项目远程仓库sky-take-out: 黑马程序员Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战
功能实现效果图:
缓存菜品
问题说明
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。
结果:系统响应慢、用户体验差
实现思路
通过Redis来缓存菜品数据,减少数据库查询操作。在查询数据库前根据内存中是否缓存数据判断是否访问数据库,从而减少数据库压力。
缓存逻辑分析:
- 每个分类下的菜品保存一份缓存数据
- 数据库中菜品数据有变更时清理缓存数据
代码开发
修改用户端接口DishController 的 list方法,加入缓存处理逻辑:
@Autowired private RedisTemplate redisTemplate; /** * 根据分类id查询菜品 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list(Long categoryId) { //构造redis中的key,规则:dish_分类id String key = "dish_" + categoryId; //查询redis中是否存在菜品数据 List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if(list != null && list.size() > 0){ //如果存在,直接返回,无须查询数据库 return Result.success(list); } //////////////////////////////////////////////////////// Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 //如果不存在,查询数据库,将查询到的数据放入redis中 list = dishService.listWithFlavor(dish); //////////////////////////////////////////////////////// redisTemplate.opsForValue().set(key, list); return Result.success(list); }为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController的相关方法,加入清理缓存逻辑。
需要改造的方法:
- 新增菜品
- 修改菜品
- 批量删除菜品
- 起售、停售菜品
抽取清理缓存的方法:
在管理端DishController中添加
注意:java中操作redis删除key的delete方法不能用*匹配所以需要先用keys方法匹配要删除的key再删除
@Autowired private RedisTemplate redisTemplate; /** * 清理缓存数据 * @param pattern */ private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); }调用清理缓存的方法,保证数据一致性:
/** * 新增菜品 * * @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 ids * @return */ @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) { log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); } /** * 修改菜品 * * @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<String> startOrStop(@PathVariable Integer status, Long id) { dishService.startOrStop(status, id); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); }缓存套餐
Spring Cache
介绍
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
-Redis(常用)
<!-- 别忘了redis坐标哦~ --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version> </dependency>常用注解
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
| 注解 | 说明 |
|---|---|
| @EnableCaching | 开启缓存注解功能,通常加在启动类上 |
| @Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
| @CachePut | 将方法的返回值放到缓存中 |
| @CacheEvict | 将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
实现思路
实现步骤:
1). 导入Spring Cache和Redis相关maven坐标(项目已导入)
2). 在启动类上加入@EnableCaching注解,开启缓存注解功能
3). 在用户端接口SetmealController的 list 方法上加入@Cacheable注解
4). 在管理端接口SetmealController的 save、delete、update、startOrStop等(数据库增删改)方法上加入CacheEvict注解
代码开发
在用户端接口SetmealController的list方法上加入@Cacheable注解
cacheNames:key前缀名 key:动态生成,支持spel
/** * 条件查询 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询套餐") @Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key: setmealCache::100 public Result<List<Setmeal>> list(Long categoryId) { Setmeal setmeal = new Setmeal(); setmeal.setCategoryId(categoryId); setmeal.setStatus(StatusConstant.ENABLE); List<Setmeal> list = setmealService.list(setmeal); return Result.success(list); }在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解
/** * 新增套餐 * * @param setmealDTO * @return */ @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100 public Result save(@RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDish(setmealDTO); return Result.success(); } /** * 批量删除套餐 * * @param ids * @return */ @DeleteMapping @ApiOperation("批量删除套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result delete(@RequestParam List<Long> ids) { setmealService.deleteBatch(ids); return Result.success(); } /** * 修改套餐 * * @param setmealDTO * @return */ @PutMapping @ApiOperation("修改套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result update(@RequestBody SetmealDTO setmealDTO) { setmealService.update(setmealDTO); return Result.success(); } /** * 套餐起售停售 * * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("套餐起售停售") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result startOrStop(@PathVariable Integer status, Long id) { setmealService.startOrStop(status, id); return Result.success(); }添加购物车
需求分析和设计
产品原型
用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
接口设计
说明:添加购物车时,有可能添加菜品,也有可能添加套餐。故传入参数要么是菜品id,要么是套餐id。
代码开发
//Controller层 /** * 购物车 */ @RestController @RequestMapping("/user/shoppingCart") @Slf4j @Api(tags = "C端-购物车接口") public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService; /** * 添加购物车 * @param shoppingCartDTO * @return */ @PostMapping("/add") @ApiOperation("添加购物车") public Result<String> add(@RequestBody ShoppingCartDTO shoppingCartDTO){ log.info("添加购物车:{}", shoppingCartDTO); shoppingCartService.addShoppingCart(shoppingCartDTO); return Result.success(); } } //Service public interface ShoppingCartService { /** * 添加购物车 * @param shoppingCartDTO */ void addShoppingCart(ShoppingCartDTO shoppingCartDTO); } //Service实现类 @Service public class ShoppingCartServiceImpl implements ShoppingCartService { @Autowired private ShoppingCartMapper shoppingCartMapper; @Autowired private DishMapper dishMapper; @Autowired private SetmealMapper setmealMapper; /** * 添加购物车 * * @param shoppingCartDTO */ public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) { ShoppingCart shoppingCart = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); //只能查询自己的购物车数据 shoppingCart.setUserId(BaseContext.getCurrentId()); //判断当前商品是否在购物车中 List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart); if (shoppingCartList != null && shoppingCartList.size() == 1) { //如果已经存在,就更新数量,数量加1 shoppingCart = shoppingCartList.get(0); shoppingCart.setNumber(shoppingCart.getNumber() + 1); shoppingCartMapper.updateNumberById(shoppingCart); } else { //如果不存在,插入数据,数量就是1 //判断当前添加到购物车的是菜品还是套餐;为购物车其余字段赋值 Long dishId = shoppingCartDTO.getDishId(); if (dishId != null) { //添加到购物车的是菜品 Dish dish = dishMapper.getById(dishId); shoppingCart.setName(dish.getName()); shoppingCart.setImage(dish.getImage()); shoppingCart.setAmount(dish.getPrice()); } else { //添加到购物车的是套餐 Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId()); shoppingCart.setName(setmeal.getName()); shoppingCart.setImage(setmeal.getImage()); shoppingCart.setAmount(setmeal.getPrice()); } shoppingCart.setNumber(1); shoppingCart.setCreateTime(LocalDateTime.now()); shoppingCartMapper.insert(shoppingCart); } } } //Mapper @Mapper public interface ShoppingCartMapper { /** * 条件查询 * * @param shoppingCart * @return */ List<ShoppingCart> list(ShoppingCart shoppingCart); /** * 更新商品数量 * * @param shoppingCart */ @Update("update shopping_cart set number = #{number} where id = #{id}") void updateNumberById(ShoppingCart shoppingCart); /** * 插入购物车数据 * * @param shoppingCart */ @Insert("insert into 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); }<mapper namespace="com.sky.mapper.ShoppingCartMapper"> <select id="list" parameterType="ShoppingCart" resultType="ShoppingCart"> select * from shopping_cart <where> <if test="userId != null"> and user_id = #{userId} </if> <if test="dishId != null"> and dish_id = #{dishId} </if> <if test="setmealId != null"> and setmeal_id = #{setmealId} </if> <if test="dishFlavor != null"> and dish_flavor = #{dishFlavor} </if> </where> order by create_time desc </select> </mapper>查看购物车
需求分析和设计
产品原型
当用户添加完菜品和套餐后,可进入到购物车中,查看购物中的菜品和套餐。
接口设计
代码开发
/** Controller * 查看购物车 * @return */ @GetMapping("/list") @ApiOperation("查看购物车") public Result<List<ShoppingCart>> list(){ return Result.success(shoppingCartService.showShoppingCart()); } /** service * 查看购物车 * @return */ List<ShoppingCart> showShoppingCart(); /** service实现类 * 查看购物车 * @return */ public List<ShoppingCart> showShoppingCart() { return shoppingCartMapper.list(ShoppingCart. builder(). userId(BaseContext.getCurrentId()). build()); }清空购物车
当点击清空按钮时,会把购物车中的数据全部清空。
接口设计
代码开发
/** Controller * 清空购物车商品 * @return */ @DeleteMapping("/clean") @ApiOperation("清空购物车商品") public Result<String> clean(){ shoppingCartService.cleanShoppingCart(); return Result.success(); } /** Service * 清空购物车商品 */ void cleanShoppingCart(); /** service实现类 * 清空购物车商品 */ public void cleanShoppingCart() { shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId()); } /** mapper * 根据用户id删除购物车数据 * * @param userId */ @Delete("delete from shopping_cart where user_id = #{userId}") void deleteByUserId(Long userId);