当前位置: 首页 > news >正文

苍穹外卖Day7 | 缓存商品、购物车、SpringCache、缓存雪崩、缓存套餐

目录

缓存菜品

1. 问题说明

2. 实现思路

3. 代码开发

4. 功能测试

补充:缓存雪崩

缓存雪崩的定义

大量 key 失效引发缓存雪崩的原因分析

缓存雪崩的危害

缓存雪崩的解决方案

缓存套餐

1. Spring Cache

学习用例

@EnableCaching

@CachePut

@Cachable

@CacheEvict

2. 实现思路

3. 代码开发

4. 功能测试

添加购物车

1. 需求分析和设计

2. 代码开发

3. 功能测试

查看购物车

1. 需求分析和设计

2. 代码开发

3. 功能测试

清空购物车

1. 需求分析和设计

 2. 代码开发

 3. 测试


缓存菜品

对于小程序,如果短时间内有大量用户访问,对后端数据库的压力很大,需要大量查询,导致性能下降、用户体验感变差。

解决:将菜品数据存储到redis,使用spring data redis进行操作

1. 问题说明

2. 实现思路

先查缓存:这个思路类似于计算机网络中的缓存代理服务器机制。

会存储用户经常访问的 Web 页面、图片等资源。当有用户请求访问这些资源时,缓存代理服务器先检查本地缓存中是否有对应的内容。若有,直接将缓存的内容返回给用户,而不需要再从原始的 Web 服务器获取数据,减少了对原始服务器的请求压力 。

由于java中的数据类型和redis中的不完全相同,按照分类的粒度来存储,value部分对应java的List

,将这个List序列化成字符串存储到redis

3. 代码开发

第一部分:改造user/dishCotroller

package com.sky.controller.user;import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {@Autowiredprivate DishService dishService;@Autowiredprivate RedisTemplate redisTemplate;/*** 根据分类id查询菜品* @param categoryId* @return*/@GetMapping("/list")@ApiOperation("根据分类id查询菜品")public Result<List<DishVO>> list(Long categoryId){// 构造redis中的key,规则:dish_分类idString 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中的数据缓存

这一部分是在admin端的DishController

package com.sky.controller.admin;import com.sky.constant.StatusConstant;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.Set;/*** 菜品管理*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {@Autowiredprivate DishService dishService;@Autowiredprivate 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<PageResult> page(DishPageQueryDTO dishPageQueryDTO){log.info("菜品分页查询:{}", dishPageQueryDTO);PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);return Result.success(pageResult);}/*** 菜品批量删除* @param ids* @return*/@DeleteMapping@ApiOperation("菜品批量删除")// 希望SpringMVC框架解析用逗号分隔的多个id,必须加一个注解 @RequestParampublic Result delete(@RequestParam List<Long> ids){log.info("菜品批量删除:{}",ids);dishService.deleteBatch(ids);// 清理相关缓存数据,可能影响多个key,还需要查询数据库// 简单起见,如果执行批量删除,就把redis中的缓存全部删掉// 缓存雪崩cleanCache("dish_*");return Result.success();}/*** 根据id查询菜品和对应的口味数据,需要回显到前端,所以使用VO* @param id* @return*/@GetMapping("/{id}")@ApiOperation("根据id查询菜品")public Result<DishVO> getById(@PathVariable Long id){log.info("根据id查询菜品:{}",id);DishVO dishVO = dishService.getByIdWithFlavor(id);return Result.success(dishVO);}/*** 根据id修改菜品基本信息和对应的口味信息* @param dishDTO* @return*/@PutMapping@ApiOperation("修改菜品")public Result update(@RequestBody DishDTO dishDTO){log.info("修改菜品:{}",dishDTO);dishService.updateWithFlavor(dishDTO);// 由于修改操作可能影响1/2份缓存数据,比较复杂// 也是直接清理所有缓存数据cleanCache("dish_*");return Result.success();}@PostMapping("/status/{status}")@ApiOperation("菜品起售停售")public Result<String> startOrStop(@PathVariable Integer status, Long id){dishService.startOrStop(status, id);// 将所有菜品缓存数据清理掉,所有以dish_开头的keycleanCache("dish_*");return Result.success();}/*** 根据分类id查询菜品** @param categoryId* @return*/@GetMapping("/list")@ApiOperation("根据分类id查询菜品")public Result<List<DishVO>> list(Long categoryId) {Dish dish = new Dish();dish.setCategoryId(categoryId);dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品List<DishVO> list = dishService.listWithFlavor(dish);return Result.success(list);}/*** 清理缓存数据* @param pattern*/private void cleanCache(String pattern){Set keys = redisTemplate.keys(pattern);redisTemplate.delete(keys);}
}

这里要特别注意,在处理批量删除时,有缓存雪崩的问题,所以直接将redis中所有缓存都删掉

4. 功能测试

第一部分代码可以正常缓存数据并正确读取

补充:缓存雪崩

大量 key 失效导致查询数据库过多,这种情况属于缓存雪崩。缓存雪崩是缓存使用中比较常见的一种问题,以下是关于它的详细介绍:

缓存雪崩的定义

缓存雪崩是指在某一个时间段,缓存中大量的 key 同时失效 ,或者缓存服务整体不可用,导致大量原本应该访问缓存的请求直接落到了数据库上,使得数据库的负载瞬间过高,甚至可能导致数据库被压垮,进而使整个应用系统不可用。

大量 key 失效引发缓存雪崩的原因分析

  • 过期时间集中设置:在项目中,如果对大量缓存 key 设置了相同或相近的过期时间,比如为了更新一批商品数据,将对应商品信息的缓存 key 都设置了 1 小时的过期时间。当 1 小时到期后,这些 key 同时失效,此时大量针对这些商品信息的请求就会直接打到数据库上,对数据库造成巨大的冲击。
  • 缓存服务故障:除了大量 key 同时失效,如果缓存服务(如 Redis 集群)因为网络故障、服务器硬件问题、软件崩溃等原因突然不可用,所有依赖缓存的请求也都会直接转向数据库,这同样会引发缓存雪崩,导致数据库压力剧增。

缓存雪崩的危害

  • 数据库负载过高:大量请求绕过缓存直接访问数据库,会使数据库的 CPU、内存、磁盘 I/O 等资源被迅速耗尽,导致数据库性能急剧下降,甚至出现服务不可用的情况。
  • 系统响应变慢:由于数据库处理能力有限,大量请求排队等待处理,使得应用系统的响应时间大幅增加,用户体验变差,严重时可能导致用户流失。
  • 服务可用性降低:如果数据库被压垮,整个依赖数据库的应用服务都可能无法正常提供服务,造成系统停机,给企业带来巨大的经济损失。

缓存雪崩的解决方案

  • 设置随机过期时间:在设置缓存过期时间时,给每个 key 的过期时间加上一个随机值,避免大量 key 在同一时间失效。例如,原本设置的过期时间是 1 小时,可以改为在 50 分钟到 70 分钟之间随机取值。
  • 缓存预热:在系统启动时,提前将一些热点数据加载到缓存中,避免在系统运行过程中大量请求同时查询数据库并写入缓存。可以通过定时任务、数据初始化脚本等方式来实现缓存预热。
  • 多级缓存:采用多级缓存架构,比如同时使用本地缓存(如 Ehcache、Caffeine)和分布式缓存(如 Redis)。本地缓存可以快速响应用户请求,减少对分布式缓存的压力,当本地缓存未命中时再去查询分布式缓存,分布式缓存也未命中时才查询数据库。
  • 缓存服务高可用:构建缓存服务的高可用集群,如使用 Redis Sentinel 或 Redis Cluster,当部分节点出现故障时,其他节点可以继续提供服务,保证缓存服务的可用性,降低因缓存服务不可用导致缓存雪崩的风险。

缓存套餐

解决:将菜品数据存储到redis,使用spring cache(由spring提供的缓存框架),进一步简化编码,提升开发效率

1. Spring Cache

使用时只需要在service上加一个缓存注解,很简单

SpringCache是如何知道我们使用哪个缓存,只需要在pom文件中导入redis的客户端,如spring data redis

@Cachable的逻辑和上面自定义的缓存逻辑非常类似

学习用例

@EnableCaching

在启动处CacheDemoApplication开启 @EnableCaching

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {public static void main(String[] args) {SpringApplication.run(CacheDemoApplication.class,args);log.info("项目启动成功...");}
}

在UserController中,针对不同的存取场景,为相应的函数添加合适的注解

@CachePut

知识点:SpEL表达式

例子: 这里的.是对象导航

@PostMapping// 如果使用SpringCache缓存数据,key的生成=userCache::id// SpEL表达式//也可以写成cacheNames = "userCache", key = "#result.id"// 从0开始,0表示取第一个参数// @CachePut(cacheNames = "userCache", key = "#p0.id")// @CachePut(cacheNames = "userCache", key = "#a0.id")// @CachePut(cacheNames = "userCache", key = "#root.args[0].id")@CachePut(cacheNames = "userCache", key = "#user.id")//将方法返回值存储到缓存中public User save(@RequestBody User user){userMapper.insert(user);return user;}

redis可以对key保存树形结构

测试这个@CachePut,可以正确写入

@Cachable

SpringCache底层是基于代理技术,一旦加入这个注解,SpringCache就会为其创建一个代理对象,在请求方法之前,先进入代理对象查询redis,如果查到之后就直接返回,不进入方法内部。如果redis中没有,就通过反射进入方法内部执行查询

@GetMapping//key的生成=userCache::id// 如果在redis中查找到了,直接返回// 如果没找到,会通过反射进入函数内部执行--查数据库,返回数据,并将结果存到redis@Cacheable(cacheNames = "userCache",key = "#id")public User getById(Long id){User user = userMapper.getById(id);return user;}

@CacheEvict

通过代理对象先将缓存中的数据删除,再执行方法内的代码--删除数据库的数据

下面两个方法分别是:删除一个、删除所有

@DeleteMapping// key的生成 = userCache::id// 这样配置只删除缓存中的一条数据@CacheEvict(cacheNames = "userCache",key = "#id")public void deleteById(Long id){userMapper.deleteById(id);}@DeleteMapping("/delAll")// 删除userCache下面所有的缓存数据@CacheEvict(cacheNames = "userCache", allEntries = true)public void deleteAll(){userMapper.deleteAll();}

测试可以成功删除缓存和数据库中的数据

2. 实现思路

3. 代码开发

user/SetmealController

/*** 条件查询* @param categoryId* @return*/@GetMapping("/list")@ApiOperation("根据分类id查询套餐")// key = setmealCache::categoryId@Cacheable(cacheNames = "setmealCache", key = "#categoryId")public Result<List<Setmeal>> list(Long categoryId) {

admin/SetmealController

/*** 新增套餐* @param setmealDTO* @return*/@PostMapping@ApiOperation("新增套餐")@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")public Result save(@RequestBody SetmealDTO setmealDTO) {

批量删除、修改套餐、套餐起售停售都直接删掉所有

    // 清理redis中所有缓存数据@CacheEvict(cacheNames = "setmealCache", allEntries = true)

4. 功能测试

添加购物车

1. 需求分析和设计

小巧思:通过name、image这样的冗余字段可以减少数据库IO,提高查询速度,值查询一张表即可

2. 代码开发

user/ShoppingCartController

package com.sky.controller.user;import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端购物车相关接口")
public class ShoppingCartController {@Autowiredprivate ShoppingCartService shoppingCartService;@PostMapping("/add")@ApiOperation("添加购物车")public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){log.info("添加购物车,商品信息为:{}", shoppingCartDTO);shoppingCartService.addShoppingCart(shoppingCartDTO);return Result.success();}
}

ShoppingCartMapper

package com.sky.mapper;import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;@Mapper
public interface ShoppingCartMapper {/*** 动态条件查询* @param shoppingCart* @return*/List<ShoppingCart> list(ShoppingCart shoppingCart);/*** 根据id修改商品数量* @param shoppingCart*/@Update("update shopping_cart set number = #{number} where id = #{id}")void updateNumberById(ShoppingCart shoppingCart);/*** 插入购物车数据* @param shoppingCart*/@Select("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);
}

ShoppingCartServiceImpl

package com.sky.service.impl;import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.List;@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {@Autowiredprivate ShoppingCartMapper shoppingCartMapper;@Autowiredprivate DishMapper dishMapper;@Autowiredprivate SetmealMapper setmealMapper;/*** 添加购物车* @param shoppingCartDTO*/public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {// 判断当前加入到购物车的商品是否已经存在了ShoppingCart shoppingCart = new ShoppingCart();BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);// 通过ThreadLocal获得当前用户idLong userId = BaseContext.getCurrentId();shoppingCart.setUserId(userId);List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);// 如果已经存在,只需将数量加一if (list != null && list.size() > 0){ShoppingCart cart = list.get(0);cart.setNumber(cart.getNumber() + 1); //数量加一shoppingCartMapper.updateNumberById(cart);} else {// 如果不存在,或者口味不一样,需要插入一条购物车数据// 判断本次添加到购物车的是菜品还是套餐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 {// 本次添加的是套餐Long setmealId = shoppingCart.getSetmealId();Setmeal setmeal = setmealMapper.getById(setmealId);shoppingCart.setName(setmeal.getName());shoppingCart.setImage(setmeal.getImage());shoppingCart.setAmount(setmeal.getPrice());}//同样的代码,放到if-else外面shoppingCart.setNumber(1);shoppingCart.setCreateTime(LocalDateTime.now());// 统一插入shoppingCartMapper.insert(shoppingCart);}}
}

SHoppingCartMapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper"><select id="list" resultType="com.sky.entity.ShoppingCart">select * from shopping_cart<where><if test="userId != null">and user_id = #{userId}</if><if test="setmealId != null">and setmeal_id = #{setmealId}</if><if test="dishId != null">and dish_id = #{dishId}</if><if test="dishFlavor != null">and dish_flavor = #{dishFlavor}</if></where></select>
</mapper>

ShoppingCartService

package com.sky.service;import com.sky.dto.ShoppingCartDTO;public interface ShoppingCartService {/*** 添加购物车* @param shoppingCartDTO*/void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}

3. 功能测试

购物车数据库:口味不同的相同商品是不同的两条数据

查看购物车

1. 需求分析和设计

不需要请求参数,用户 id可以从ThreadLocal获取

2. 代码开发

ShoppingCartController

 /*** 查看购物车* @return*/@GetMapping("/list")@ApiOperation("查看购物车")public Result<List<ShoppingCart>> list(){List<ShoppingCart> list = shoppingCartService.showShoppingCart();return Result.success(list);}

ShoppingCartServiceImpl

   /*** 查看购物车* @return*/public List<ShoppingCart> showShoppingCart() {Long userId = BaseContext.getCurrentId();ShoppingCart shoppingCart = ShoppingCart.builder().userId(userId).build();List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);return list;}

3. 功能测试

清空购物车

1. 需求分析和设计

不需要传参,可从ThreadLocal获取用户id

 2. 代码开发

user/ShoppingCartConrtroller

 /*** 清空购物车* @return*/@DeleteMapping("/clean")@ApiOperation("清空购物车")public Result clean(){shoppingCartService.cleanShoppingCart();return Result.success();}

ShoppingCartServiceImpl

    /*** 清空购物车*/public void cleanShoppingCart() {Long userId = BaseContext.getCurrentId();shoppingCartMapper.deleteByUserId(userId);}

ShoppingCartMapper

 /*** 根据用户id删除购物车数据* @param userId*/@Delete("delete from shopping_cart where user_id = #{userId}")void deleteByUserId(Long userId);

 3. 测试

可以成功删除!

外卖刚好到了!今天任务完成!

http://www.dtcms.com/a/358276.html

相关文章:

  • SpringCloud Alibaba微服务--Sentinel的使用
  • docker 部署Skywalking
  • 基于大模型与 PubMed 检索的光谱数据分析系统
  • 大语言模型的“可解释性”探究——李宏毅大模型2025第三讲笔记
  • Java类加载与JVM详解:从基础到双亲委托机制
  • idea 普通项目转换成spring boot项目
  • Python实现半角数字转全角数字的完整教程
  • 《中国棒垒球》垒球世界纪录多少米·垒球8号位
  • Visual Studio(vs)免费版下载安装C/C++运行环境配置
  • LeetCode 287.寻找重复数
  • Java试题-选择题(23)
  • 【LeetCode 热题 100】62. 不同路径——(解法四)组合数学
  • 聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集
  • rhel-server-7.9-x86_64-dvd.iso
  • 机器学习中KNN算法介绍
  • 笔记共享平台|基于Java+vue的读书笔记共享平台系统(源码+数据库+文档)
  • 数据库原理及应用_数据库基础_第3章数据库编程_常用系统函数
  • 骑行商城怎么开发
  • 【金仓数据库产品体验官】KingbaseES-ORACLE兼容版快速体验
  • 国家统计局数据分析01——机器学习
  • GD32VW553-IOT 基于 vscode 的 bootloader 移植(基于Cmake)
  • 【DreamCamera2】相机应用修改成横屏后常见问题解决方案
  • 阿里云营业执照OCR接口的PHP实现与技术解析:从签名机制到企业级应用
  • LZ4 解压工具(WPF / .NET 8)说明书
  • Java Stream API并行流性能优化实践指南
  • 基于Kubernetes自定义调度器的资源隔离与性能优化实践指南
  • 从 0 到 1 构建零丢失 RabbitMQ 数据同步堡垒:第三方接口数据零丢失的终极方案
  • 人工智能学习:Python相关面试题
  • 人工智能学习:Linux相关面试题
  • 98、23种设计模式之代理模式(7/23)