缓存菜品
问题说明: 用户端小程序展示的菜品数据都是通过查询数据库获得, 如果客户端访问量较大, 数据库访问访问压力增大,容易造成响应慢, 用户体验差
解决思路: 通过redis来缓存菜品数据, 减少数据库查询操作, 提高程序运行效率
缓存逻辑分析: 每个分类下的菜品保存一份缓存数据, 数据库中菜品数据有变更时清理缓存数据
存储形式:
- 把分类的id作为key, 把每个分类下的菜品信息作为value,
- 我们把菜品信息集合看做整体, 转成redis中的string存储即可
- redis中的string不同于java, java中的所有数据类型都可以转成redis中的string储存, 类似与序列化的过程
改造代码: 用户端查询菜品时, 优先使用缓存数据, 没有缓存数据就要添加缓存数据
@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<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);
}
}
清理缓存数据: 新增菜品/修改菜品/批量删除菜品/起售停售菜品时, 要删除缓存数据, 保持数据一致性
/**
* 菜品管理
*/
@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 ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBath(ids);
//删除缓存数据
cleanCache("dish_*");
return Result.success();
}
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品: {}", dishDTO);
dishService.updateWithFlavor(dishDTO);
//删除缓存数据
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();
}
/**
* 清理缓存数据
*
* @param pattern
*/
private void cleanCache(String pattern) {
// 获取可以删除的key
Set keys = redisTemplate.keys(pattern);
// 根据key删除缓存数据
redisTemplate.delete(keys);
}
}
功能测试
Spring Cache
Spring Cache是一个框架, 实现了基于注解的缓存功能, 只要加上相应注解, 就能实现缓存功能
缓存实现: Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,而且无需改动业务代码
支持的缓存实现
- EHCache
- Caffeine
- Redis
切换缓存实现: 切换不同的中间件坐标即可切换缓存实现, 无需改动业务代码
// Sring Cache坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
// 具体的缓存中间件坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
常用注解
入门案例
导入资料中的springcache-dome工程, 用于学习springcache的使用
准备工作
- 创建cpring_cache_dome数据库
- 执行springcachedome.sql文件, 创建数据表
- 引入坐标
// Sring Cache坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
// 具体的缓存中间件坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 在启动类上开启功能缓存注解功能
@SpringBootApplication
@Slf4j
@EnableCaching //开启缓存注解功能
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
使用 CachePut()注解 将方法的返回值存在缓存中
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper usereMapper;
@PostMapping
@CachePut(cacheNames="userCache",key = "#user.id")
public User save(@RequestBody User user) {
User user = usereMapper.insert(user);
return user;
}
}
- 通常在Controller中操作缓存
- userCache属性设置缓存名称, 一般命名要与业务相关
- key属性用来设置每一条缓存数据的名称, 最终的值是userCache属性 + spring表达式的值
- 如果key的值是abc, 最终key的完整格式就是 userCache::abc
- spring表达式的书写非常灵活, 以下的写法都可以读取返回值, 拼接key的名称
- redis中的key是支持树形结构的, 通过 冒号: 分割
使用Cacheable()注解, 使用或添加缓存数据
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper usereMapper;
@GetMapping
@Cacheable(cacheNames="userCache",key = "#id")
public User getById(Long id) {
User user = usereMapper.getById(id);
return user;
}
}
- Cacheable()注解是基于代理技术实现, 在方法运行前, 生成当前方法的代理对象,
- 在代理对象中, 查找缓存中是否有数据, 有数据就直接返回, 不会触发controller方法
- 没有数据, 通过反射, 调用目标的controller方法, 把方法的返回结果存入缓存
使用CacheEvict()注解, 使用删除缓存数据
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper usereMapper;
@DeleteMapping
// 精准删除: 通过key的值匹配缓存数据并删除
@CacheEvict(cacheNames="userCache",key = "#id")
public void delById(Long id) {
usereMapper.delById(id);
}
@DeleteMapping("/delAll")
// 全部删除: 删除userCache下的所有缓存数据
@CacheEvict(cacheNames="userCache", allEntries = true)
public void deleteAll() {
usereMapper.deleteAll();
}
}
缓存套餐
导入坐标
// 导入Spring Cache 和Redis 坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
开启缓存注解功能: 在启动类上添加@EnableCaching注解
@SpringBootApplication
@Slf4j
@EnableCaching //开启缓存注解功能
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
添加缓存: 在客户端的SetmealControllere 的list方法上 添加@Cacheable注解
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
@Autowired
private SetmealService setmealService;
/**
* 条件查询
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@Cacheable(cacheNames = "setmealCache", key = "#categoryId")
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,delet,update, startOrShtop等方法上添加 CacheEvict注解
/**
* 套餐管理
*/
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
/**
* 新增套餐
*
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
// 精准删除: 根据key的名称(setmealCache::100)精确删除
@CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
... ...
/**
* 套餐起售停售
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
// 全部删除: 删除setmealCache下面的所有缓存数据
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}
}
- 注意: 使用注解时注意包的名称
功能测试: 第一次访问套餐查询数据库, 后面的访问使用缓存数据, 数据变化后会删除缓存
添加购物车接口
需求分析: 购物车就是暂时存放所选商品的地方
接口设计
购物车表(shopping_cart)的设计
需求:
- 体现出选择的什么商品
- 每个商品的数量
- 不同用户的购物车需要区分开
- 通过少量的冗余字段, 检查连接查询
设计DTO: 封装前端传递的数据
@Data
public class ShoppingCartDTO implements Serializable {
private Long dishId;
private Long setmealId;
private String dishFlavor;
}
Controller: 新建 user/ShoppingCartController
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "客户端购物车相关接口")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 添加购物车
* @param shoppingCartDTO
* @return
*/
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("添加购物车,商品信息为:{}", shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
}
Service: 新建ShoppingCartService接口和实现类
@Service
public interface ShoppingCartService {
// 添加购物车
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
@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) {
// 1.判断当前加入购物车的商品是否已经存在了
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
List<ShoppingCart> 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.如果不存在, 需要插入一条购物车数据
// 判断本次添加到购物车的是菜品还是套餐
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 = shoppingCartDTO.getSetmealId();
Setmeal setmeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}
Mapper: 新建mapper/ShoppingCartMapper
@Mapper
public interface ShoppingCartMapper {
/**
* 添加条件查询
*
* @param shoppingCart
* @return
*/
List<ShoppingCart> list(ShoppingCart shoppingCart);
/**
* 根据id修改商品数量
*
* @param cart
*/
@Update("update shopping_cart set number = #{number} where id = #{id}")
void updateNumberById(ShoppingCart cart);
/**
* 添加商品数据
*
* @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);
}
<?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="dishFlavor != null">
and dish_flavor = #{dishFlavor}
</if>
</where>
</select>
</mapper>
功能测试
查看购物车接口
查看原型, 分析需求
接口设计
Controller
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "客户端购物车相关接口")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list() {
List<ShoppingCart> list = shoppingCartService.showShoppingCart();
return Result.success(list);
}
}
Service
public interface ShoppingCartService {
// 查看购物车
List<ShoppingCart> showShoppingCart();
}
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
/**
* 查看购物车
*
* @return
*/
public List<ShoppingCart> showShoppingCart() {
// 获取当前微信用户的id
Long id = BaseContext.getCurrentId();
ShoppingCart shoppingCart = ShoppingCart.builder().userId(id).build();
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
return list;
}
}
mapper
@Mapper
public interface ShoppingCartMapper {
/**
* 添加条件查询
*
* @param shoppingCart
* @return
*/
List<ShoppingCart> list(ShoppingCart shoppingCart);
}
<?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="dishFlavor != null">
and dish_flavor = #{dishFlavor}
</if>
</where>
</select>
</mapper>
功能测试
清空购物车接口
查看原型, 分析需求
接口设计
Controller
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "客户端购物车相关接口")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
@ApiOperation("清空购物车")
public Result clean() {
shoppingCartService.cleanShoppingCart();
return Result.success();
}
}
Service
public interface ShoppingCartService {
// 清空购物车
void cleanShoppingCart();
}
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
/**
* 清空购物车
*/
public void cleanShoppingCart() {
// 获取当前微信用户的id
Long id = BaseContext.getCurrentId();
shoppingCartMapper.deleteByUserId(id);
}
}
Mapper
@Mapper
public interface ShoppingCartMapper {
/**
* 根据用户id删除购物车数据
*
* @param id
*/
@Delete("delete from shopping_cart where user_id = #{id}")
void deleteByUserId(Long id);
}