为什么要缓存?
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。用户端就会卡。
实现思路
通过Redis来缓存菜品数据,减少数据库查询操作。
- 每个分类下的菜品保存一份缓存数据
- 数据库中菜品数据有变更时清理缓存数据
缓存菜品
修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑:
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 的相关方法,加入清理缓存逻辑。
管理端controller改造,加入缓存逻辑:
@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<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("开始菜品分页查询...");
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
/**
* 批量删除菜品
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除菜品")
public Result delete(@RequestParam List<Long> ids) {
log.info("批量删除菜品:{}", ids);
dishService.deleteDish(ids);
// 将所有菜品缓存数据清除(所有以dish_开头的key)
cleanCache("dish_*");
return Result.success(ids);
}
/**
* 根据id查询商品
* @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);
}
/**
* 修改菜品
* @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();
}
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId) {
log.info("根据分类id查询菜品:{}", categoryId);
Dish dish = new Dish();
dish.setCategoryId(categoryId);
List<Dish> list = dishService.list(dish);
return Result.success(list);
}
/**
* 清除缓存数据
* @param pattern
*/
private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
}
缓存套餐
Spring Cache
是什么?干什么用的?
Spring Cache 是一个框架,实现了基于注解的缓存功能。
怎么用?
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version>
</dependency>
常用注解
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
案例-Spring Cache
@EnableCaching,@CachePut
Day07-08-缓存套餐_Spring Cache_入门案例_2_哔哩哔哩_bilibili
/**
* 启动类
*/
@Slf4j
@SpringBootApplication
@EnableCaching // 开启缓存注解功能,通常加在启动类上
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
log.info("项目启动成功...");
}
}
/**
* 将方法的返回值放到缓存中
*/
@PostMapping
// @CachePut(cacheNames = "userCache", key = "abc") 如果使用springCache缓存数据,key的生成:userCache::abc
// 改进:key需要是动态的 -> Spring表达式语言
@CachePut(cacheNames = "userCache", key = "#user.id")
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
@CachePut 说明:
作用: 将方法返回值,放入缓存
value: 缓存的名称, 每个缓存名称下面可以有很多key
key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法
说明:key的写法如下
#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;
#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数
注意此处的redis存储的是树形结构,可以观察并做一个测试:
@Cacheable
Day07-09-缓存套餐_Spring Cache_入门案例_3_哔哩哔哩_bilibili
@GetMapping
@Cacheable(cacheNames = "userCache", key = "#id") // 注意不能写成 #result
public User getById(Long id){
User user = userMapper.getById(id); // 如果redis里查询到就不会调用这个方法了
return user;
}
访问UserController的getById()方法第一次访问,会请求我们controller的方法,查询数据库。后面再查询相同的id,就直接从Redis中查询数据,不用再查询数据库了,就说明缓存生效了。提前在redis中手动删除掉id=1的用户数据。
原理是基于动态代理,使用代理对象先去查询redis,如果没有查询到就再次调用方法。
@CacheEvict
Day07-10-缓存套餐_Spring Cache_入门案例_4_哔哩哔哩_bilibili
@DeleteMapping
// 将一条或多条数据从缓存中删除
@CacheEvict(cacheNames = "userCache",key = "#id") // key的生成:userCache::10
public void deleteById(Long id){
userMapper.deleteById(id);
}
@CacheEvict(cacheNames = "userCache",allEntries = true) // 清除所有缓存
@DeleteMapping("/delAll")
public void deleteAll(){
userMapper.deleteAll();
}
缓存代码开发
购物车功能
DTO代码略。
// 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();
}
/**
* 减少购物车商品数量
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("减少购物车商品数量")
public Result<String> sub(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("减少购物车商品数量:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list() {
return Result.success(shoppingCartService.showShoppingCart());
}
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
@ApiOperation("清空购物车商品")
public Result<String> clean() {
shoppingCartService.cleanShoppingCart();
return Result.success();
}
}
// service
public interface ShoppingCartService {
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
/**
* 减少购物车商品数量
* @param shoppingCartDTO
*/
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
/**
* 查看购物车
* @return
*/
List<ShoppingCart> showShoppingCart();
/**
* 清空购物车
*/
void cleanShoppingCart();
}
// impl
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
@Override
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); // 根据表结构,只有唯一数据(id值是唯一),所以是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);
}
}
/**
* 减少购物车商品数量
* @param shoppingCartDTO
*/
@Override
public void subShoppingCart(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() > 0) {
shoppingCart = shoppingCartList.get(0);
Integer number = shoppingCart.getNumber();
// 如果商品数量等于1,直接删除
if (number == 1) {
shoppingCartMapper.deleteById(shoppingCart.getId());
} else {
// 如果商品数量大于1,更新数量-1
shoppingCart.setNumber(number - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}
@Override
public List<ShoppingCart> showShoppingCart() {
return shoppingCartMapper.list(ShoppingCart.
builder().userId(BaseContext.getCurrentId()).build());
}
/**
* 清空购物车
*/
@Override
public void cleanShoppingCart() {
shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
}
}
// 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, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) "
+ "values (#{name},#{image},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{createTime})"
)
void insert(ShoppingCart shoppingCart);
/**
* 根据用户id删除购物车数据
* @param currentId
*/
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long currentId);
/**
* 根据id删除购物车数据
* @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);
}
// xml
<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>