缓存菜品

实现思路


Java中的数据类型与redis中的数据类型并不完全对应。redis中很多东西我们都可以用String来表示,但这里在Java中菜品的缓存数据其实是list集合,我们需要先把集合序列化为字符串。
Redis 中存储的数据结构:
java
Redis 是一个 key-value 数据库,类似一个大 Map<String, Object>:
Key Value(序列化后的 Java 对象)
─────────────────────────────────────────────────────────
"dish_1" → [{name:"宫保鸡丁", price:28,...}, ...]
"dish_2" → [{name:"米饭", price:2,...}, ...]
"dish_3" → [{name:"可乐", price:5,...}, ...]
"SHOP_STATUS" → 1
代码开发------缓存菜品数据
不使用缓存: 使用缓存:
用户请求 用户请求
│ │
▼ ▼
查数据库 查 Redis
│ │
▼ ┌──────┴──────┐
返回数据 有数据? │
┌──────┘ 没有│
▼ ▼
直接返回 查数据库
(很快!) │
▼
存入 Redis
再返回数据
涉及的文件
| 文件 | 作用 |
|---|---|
| DishController.java | 缓存菜品的核心逻辑 |
| RedisConfiguration.java | 创建 Redis 连接模板 |
| application.yml | Redis 连接配置(占位符) |
| application-dev.yml | Redis 实际连接信息 |
配置redis连接

配置 RedisTemplate(连接工具)
RedisConfiguration.java
java
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
// 1. 设置连接工厂(告诉 RedisTemplate 连哪个 Redis)
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 2. 设置 key 的序列化器(让 key 在 Redis 中显示为可读的字符串)
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
RedisTemplate是 Spring 提供的一个工具类,帮你用代码操作 Redis,就像JdbcTemplate帮你操作 MySQL 一样@Bean表示这个方法返回的对象会交给 Spring 容器管理,之后在 Controller 里用@Autowired就能直接拿到- 序列化器 :默认情况下 Java 对象存入 Redis 会变成二进制乱码(如
\xAC\xED\x00\x05),设置了StringRedisSerializer后,key 就会以人类可读的字符串形式存储
核心缓存逻辑
DishController.java
java
@RestController("userDishController")
@RequestMapping("/user/dish")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate; // 注入 Redis 操作工具
@GetMapping("/list")
public Result<List<DishVO>> list(Long categoryId) {
// ═══════════ 第 1 步:构造缓存 Key ═══════════
String key = "dish_" + categoryId;
// 例如:categoryId=3 → key = "dish_3"
// ═══════════ 第 2 步:查 Redis 缓存 ═══════════
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
// opsForValue() → 操作字符串类型(Redis 的 String 类型)
// get(key) → 从 Redis 中取出 key 对应的值
if (list != null && list.size() > 0) {
// ═══════ 缓存命中!直接返回,不查数据库 ═══════
return Result.success(list);
}
// ═══════════ 第 3 步:缓存没命中,查数据库 ═══════════
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);
list = dishService.listWithFlavor(dish); // 查数据库(慢)
// ═══════════ 第 4 步:把结果存入 Redis ═══════════
redisTemplate.opsForValue().set(key, list);
// 下次再查同样的 categoryId,直接从 Redis 返回
return Result.success(list);
}
}
完整执行流程
java
用户第一次查 categoryId=3 的菜品:
────────────────────────────────────────────────
GET /user/dish/list?categoryId=3
│
▼
构造 key = "dish_3"
│
▼
Redis 查 "dish_3" → ❌ 没有(返回 null)
│
▼
查 MySQL → 返回 [宫保鸡丁, 鱼香肉丝, 米饭]
│
▼
把结果存入 Redis:
key: "dish_3"
value: [{name:"宫保鸡丁",...}, {name:"鱼香肉丝",...}, ...]
│
▼
返回数据给前端 ✅
用户第二次查 categoryId=3 的菜品:
────────────────────────────────────────────────
GET /user/dish/list?categoryId=3
│
▼
构造 key = "dish_3"
│
▼
Redis 查 "dish_3" → ✅ 有数据!
│
▼
直接返回(不查 MySQL)✅ ⚡ 比第一次快 100 倍
清理缓存数据
问题:不清理缓存,可能导致数据不一致。比如我们在后端修改菜品价格,修改的是数据库里的数据,而前端展示的是之前从redis里查出来的,就会出现数据不一致的情况。
因此当数据库的数据发生变化,我们应当及时清理这些缓存数据。

一、核心清理方法
位置 : sky-server/src/main/java/com/sky/controller/admin/DishController.java
java
// 统一清理缓存数据
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
二、方法解析
| 组成部分 | 代码 | 作用说明 |
|---|---|---|
| 参数 | String pattern |
缓存键的匹配模式(支持通配符) |
| 获取匹配键 | redisTemplate.keys(pattern) |
根据模式查询所有匹配的缓存键 |
| 批量删除 | redisTemplate.delete(keys) |
批量删除所有匹配的缓存 |
三、Redis Keys 命令详解
redisTemplate.keys(pattern) 底层调用 Redis 的 KEYS 命令:
java
Set keys = redisTemplate.keys("dish_*");
// 等价于 Redis 命令: KEYS dish_*
// 返回所有以 "dish_" 开头的键,如: dish_1, dish_2, dish_100...
支持的通配符:
*- 匹配任意数量的字符(包括零个)?- 匹配单个字符[]- 匹配方括号内的任一字符
四、清理策略分类
根据不同业务操作,缓存清理采用差异化策略:
1. 精准清理(新增菜品)
java
@PostMapping
public Result save(@RequestBody DishDTO dishDTO){
dishService.saveWithFlavor(dishDTO);
// 精准清理:只清理新增菜品所属分类的缓存
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);
return Result.success();
}
设计意图: 新增菜品只会影响其所属分类的缓存,无需清理其他分类。
2. 全量清理(删除/修改/状态变更)
java
// 批量删除
@DeleteMapping
public Result delete(@RequestParam List<Long> ids){
dishService.deleteBatch(ids);
cleanCache("dish_*"); // 清理所有菜品缓存
return Result.success(ids);
}
// 修改菜品
@PutMapping
public Result update(@RequestBody DishDTO dishDTO){
dishService.updateWithFlavor(dishDTO);
cleanCache("dish_*"); // 清理所有菜品缓存
return Result.success();
}
// 起售停售
@PostMapping("/status/{status}")
public Result<String> startOrStop(@PathVariable Integer status, Long id){
dishService.startOrStop(status, id);
cleanCache("dish_*"); // 清理所有菜品缓存
return Result.success();
}
设计意图:
- 删除操作:无法预知被删菜品属于哪些分类
- 修改操作:菜品可能从一个分类转移到另一个分类
- 状态变更:影响菜品的可用性,所有分类列表都可能受影响
五、清理策略对比
| 策略 | Key模式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 精准清理 | dish_{categoryId} |
新增菜品 | 优点:影响范围小,缓存命中率高 缺点:仅适用于明确知道影响范围的场景 |
| 全量清理 | dish_* |
删除/修改/状态变更 | 优点:简单可靠,保证数据一致性 缺点:缓存命中率暂时下降,需重新预热 |
六、缓存清理流程图
┌────────────────────────────────────────────────────────────┐
│ 缓存清理流程 │
├────────────────────────────────────────────────────────────┤
│ │
│ 管理操作触发 │
│ │ │
│ ▼ │
│ 判断操作类型 │
│ │ │
│ ├─── 新增菜品 ───→ cleanCache("dish_"+categoryId) │
│ │ │
│ └─── 删除/修改/状态 ─→ cleanCache("dish_*") │
│ │
│ ▼ │
│ redisTemplate.keys(pattern) │
│ │ │
│ ▼ │
│ 获取所有匹配的key集合 │
│ │ │
│ ▼ │
│ redisTemplate.delete(keys) │
│ │ │
│ ▼ │
│ 缓存失效完成 │
│ │
└────────────────────────────────────────────────────────────┘
七、设计原则与考量
1. 数据一致性优先
采用 Write-Behind + Cache-Invalidate 模式:
- 先更新数据库
- 再删除缓存(而非更新缓存)
为什么不直接更新缓存?
- 更新缓存可能导致并发场景下的数据不一致
- 删除缓存更简单可靠,下次读取时自动重建
2. 最小影响原则
能精准清理的场景绝不全量清理:
- 新增菜品 → 精准清理(只影响一个分类)
- 删除/修改 → 全量清理(影响范围不确定)
3. 统一抽象
将缓存清理逻辑封装为 cleanCache() 方法:
- 代码复用
- 统一维护
- 便于后续扩展(如添加日志、监控等)
至此,菜品缓存与清理缓存功能模块的代码开发已经实现完毕