外卖点餐系统07
缓存菜品
问题说明
用户通过微信小程序查询菜品,小程序会将请求发送给后端服务,后端就会查询MySQL数据库。
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大,就会造成系统响应慢,用户体验差。
解决思路
- 通过Redis来缓存菜品数据,减少数据库查询操作。
微信小程序查询数据后,会向后端服务发送请求,判断请求的数据在缓存中是否存在,如果存在直接读取缓存,不存在再查询MySQL,并将该数据载入缓存。 - 缓存逻辑分析
微信小程序展示菜品粒度是根据分类展示,所以缓存数据应该根据分类缓存菜品。- 每个分类下的菜品保存一份缓存数据
- 使用分类id作为key,分类下的菜品数据使用String字符串保存,分类下的菜品应该是List集合,在Java中可以通过序列化将这个集合转换成Redis的字符串类型
- 数据库中菜品数据有变更时及时清理缓存数据
代码开发
- 修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑
public Result<List<DishVO>> list(Long categoryId) {
// 微信小程序通过这个controller显示商品浏览界面,
// 这个请求操作经常被执行,每次请求都需要查询数据库,如果用户过多会对MySQL造成太大压力,所以我们将每个分类的菜品缓存起来
// 查询redis中是否存在菜品数据,查询数据需要获取key,所以我们首先要构造redis中的key
// 我们统一key规则,dish_分类id
String key = "dish_" + categoryId;
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
// 如果存在,直接返回菜品数据,不需要查询MySQL
if(list == null && list.size() > 0){
return Result.success(list);
}
// 如果不存在,查询MySQL,并将查询到数据,放入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);
}
- 修改管理端接口 DishController 的相关方法,加入清理缓存的逻辑,需要改造的方法:
- 新增菜品 save
- 修改菜品 update
- 批量删除菜品 deleteBatch
- 起售、停售菜品 startOrStop
DishController.java
/**
* 新增菜品
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO); // 新增菜品时也需要操作flavor表
// 清理缓存数据,这里可以精确清理改变的那个分类
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);
return Result.success();
}
/**
* 统一清理菜品缓存数据
* @param pattern
*/
private void cleanCache(String pattern) {
// Set keys = redistemplate.keys("dish_*"); // 获取redis中 以dish_开头的所有key
Set keys = redisTemplate.keys(pattern );
redisTemplate.delete(keys);
}
使用Spring Cache缓存框架 缓存套餐
Spring Cache
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis
这让我们能够轻松的切换缓存实现。
为了在项目中使用Spring Cache,只需要在项目pom.xml文件中导入Maven坐标,项目导入redis的Maven坐标后,Spring Cache自动使用redis缓存实现。
Spring Cache常用注解
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
举例:
@PostMapping
// 使用Spring Expression Language (SpEL)expression for computing the key dynamically
// 这里的key是通过Spring EL动态获得的
@CachePut(cacheNames = "userCache", key = "#user.id") // 如果使用Spring Cache缓存数据,key的生成:userCache::2
public User save(@RequestBody User user) {
userMapper.insert(user);
return user;
}
这里涉及到了Redis的目录结构,redis使用冒号" : "来逻辑上形成树形目录
缓存套餐代码开发
具体思路如下:
- 导入Spring Cache和Redis相关maven坐标
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
- 在用户端接口SetmealController的list方法(因为用户端是通过该方法查询套餐较多)上加入@Cacheable注解
- 在管理端接口SetmealController的save、delete、update、startOrStop等方法上加入CacheEvict注解
购物车代码开发
需求分析和设计
- 需求分析: 用户暂存所选菜品和套餐功能
- 对于套餐直接加入购物车
- 菜品如果没有设置口味数据,直接添加到购物车
- 菜品如果设置口味数据,需要先选择口味数据,再加入购物车
- 如果商品已经添加购物车,想要添加同样的商品,直接增加份数
- 接口设计:
- 请求方式:POST
- 请求路径:/user/shoppingCart/add
- 请求参数:套餐id、菜品id、口味
- 返回结构:code、data、msg
- 数据库设计(shopping_cart):
- 作用:暂时存放所选商品的地方
- 选的什么商品:套餐setmeal_id, 菜品dish_id,口味字段dish_flavor
- 每个商品买了几个:商品数量 number
- 不同用户的购物车需要区分开: user_id
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 商品名称 | 冗余字段 |
image | varchar(255) | 商品图片路径 | 冗余字段 |
user_id | bigint | 用户 id | 逻辑外键 |
dish_id | bigint | 菜品 id | 逻辑外键 |
setmeal_id | bigint | 套餐 id | 逻辑外键 |
dish_flavor | varchar(50) | 菜品口味 | |
number | int | 商品数量 | |
amount | decimal(10, 2) | 商品单价 | 冗余字段 |
create_time | datetime | 创建时间 | |
冗余字段,反复出现重复的字段,在菜品表dish中含有name,image,amount这些字段,但是这个表再加上这些冗余字段,可以提高查询数据效率,因为增加了冗余字段就只需要单表查询,如果不设置这些冗余字段可能就需要查找套餐表或者菜品表。 | |||
注意:冗余字段不能大量使用,设置的字段不能经常变化 |
购物车代码开发
购物车增加商品
我们要将某个菜品添加到购物车之前要先判断该菜品是否已经存在,如果存在,只需要执行修改操作数量字段加一,如果不存在,再执行插入操作。
/**
* 添加购物车
* @param shoppingCartDTO
*/
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
// 传入的shoppingCartDTO中,可能包含 setmealId或者dishId,dishFlavor
// 不同用户需要不同的购物车,所以我们需要首先获取当前用户id
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
// 通过threadLocal获取当前用户id,在拦截器中解析token获取到了用户id,并在拦截器中存储到threadlocal中
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
// 我们要将某个菜品添加到购物车之前要先判断该菜品是否已经存在,使用套餐或者菜品id加上用户id查询菜品是否依旧存在购物车
// select * from shopping_cart where user_id = ? and setmeal_id = ?
// select * from shopping-cart where user_id = ? and dish_id = ? and dish_flavor = ?
// 使用MyBatis动态SQL实现
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
// 如果存在,只需要执行修改操作数量字段加一
if(list != null && list.size() > 0) {
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber() + 1);
shoppingCartMapper.update(cart);
} else {
//如果不存在,再执行插入操作
// 判断添加到购物车的商品是菜品还是套餐,只有知道了,才能去具体查某个表
Long dishId = shoppingCartDTO.getDishId();
if(dishId != null) {
// 本次添加到购物车的商品是菜品,根据dishID查出菜品信息,拷贝到ShoppingCart然后插入
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setAmount(dish.getPrice());
shoppingCart.setImage(dish.getImage());
} else {
// 本次添加到购物车的商品是套餐
Setmeal setmealId = setmealMapper.getById(shoppingCartDTO.getSetmealId());
shoppingCart.setName(setmealId.getName());
shoppingCart.setAmount(setmealId.getPrice());
shoppingCart.setImage(setmealId.getImage());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
删除购物车中的商品
/**
* 删除购物车商品
* @param shoppingCartDTO
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
// 如果要删除的菜品或者套餐,份数大于1,直接修改number字段
// 如果要删除的商品只有一份,定执行删除操作,直接删除
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
// 获取要删除的商品份数
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if(list != null && list.size() > 0) {
ShoppingCart cart = list.get(0);
int number = list.get(0).getNumber();
if (number > 1) {
cart.setNumber(number - 1);
shoppingCartMapper.update(cart);
} else {
// 如果要删除的商品只有一份,执行删除操作,确定执行删除操作,要判断时删除菜品,还是套餐
shoppingCartMapper.subDelete(shoppingCart);
}
}
// delete from shopping_cart where dish_id = #{shoppingCartDTO.dishId}
// delete from shopping_cart where setmeal_id = #{shoppingCartDTO.setmealId}
}