1. Redis缓存实战:菜品分类查询优化方案
在餐饮类应用的后端开发中,分类查询菜品是一个高频且性能敏感的操作。每次用户浏览菜单时,系统都需要根据分类ID获取对应的菜品列表。传统做法是直接查询数据库,但当并发量增大时,这会给数据库带来巨大压力。我们团队在实际项目中采用Redis缓存方案,使接口响应时间从平均120ms降至15ms,数据库负载下降70%。
1.1 基础缓存实现原理
核心思路是将分类ID作为Key,菜品列表作为Value存入Redis。当接口被调用时:
- 先检查Redis中是否存在对应缓存
- 命中缓存则直接返回
- 未命中则查询数据库并写入缓存
java复制@GetMapping("/list")
public Result<List<DishVO>> list(Long categoryId) {
String key = "dish_" + categoryId;
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if(list != null && !list.isEmpty()){
return Result.success(list); // 缓存命中
}
// 缓存未命中,查询数据库
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key, list);
return Result.success(list);
}
关键细节:缓存键设计采用"业务前缀_ID"的格式(如dish_123),既避免键冲突,又便于批量管理。状态过滤(只缓存启用的菜品)在数据库查询时完成,保证缓存数据可直接使用。
1.2 缓存更新策略设计
当管理员修改数据时,必须同步清理缓存,否则会导致用户看到过期数据。我们采用"先更新数据库,再删除缓存"的策略:
java复制private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
if(keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
@DeleteMapping
public Result delete(@RequestParam List<Long> ids) {
dishService.deleteBatch(ids);
cleanCache("dish_*"); // 使用通配符清理所有相关缓存
return Result.success();
}
实际踩坑经验:
- 删除操作要放在事务提交之后,避免缓存已删但数据库回滚的情况
- 批量操作时使用通配符(dish_*)比精确删除更可靠,因为菜品可能变更分类
- Redis的keys命令在生产环境要慎用,数据量大时会阻塞线程,可以考虑SCAN迭代
1.3 缓存雪崩与穿透防护
在高并发场景下,我们额外增加了这些保护措施:
java复制// 改进后的获取逻辑
public Result<List<DishVO>> list(Long categoryId) {
String key = "dish_" + categoryId;
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
// 缓存命中
if(list != null) {
return Result.success(list.isEmpty() ? null : list);
}
// 加分布式锁,防止缓存击穿
String lockKey = "lock_" + key;
try {
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if(locked != null && locked) {
// 查询数据库
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);
list = dishService.listWithFlavor(dish);
// 空结果也缓存,防止穿透
redisTemplate.
