欢迎光临
我们一直在努力

苍穹外卖【day7|缓存菜品与清空缓存】

缓存菜品

实现思路

Java中的数据类型与redis中的数据类型并不完全对应。redis中很多东西我们都可以用String来表示,但这里在Java中菜品的缓存数据其实是list集合,我们需要先把集合序列化为字符串。

Redis 中存储的数据结构:

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

@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

@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);
}
}

完整执行流程

用户第一次查 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

// 统一清理缓存数据
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 命令:

Set keys = redisTemplate.keys("dish_*");
// 等价于 Redis 命令: KEYS dish_*
// 返回所有以 "dish_" 开头的键,如: dish_1, dish_2, dish_100…

支持的通配符:

  • * – 匹配任意数量的字符(包括零个)
  • ? – 匹配单个字符
  • [] – 匹配方括号内的任一字符

四、清理策略分类

根据不同业务操作,缓存清理采用差异化策略:

1. 精准清理(新增菜品)

@PostMapping
public Result save(@RequestBody DishDTO dishDTO){
dishService.saveWithFlavor(dishDTO);

// 精准清理:只清理新增菜品所属分类的缓存
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);

return Result.success();
}

设计意图: 新增菜品只会影响其所属分类的缓存,无需清理其他分类。

2. 全量清理(删除/修改/状态变更)

// 批量删除
@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() 方法:

    • 代码复用
    • 统一维护
    • 便于后续扩展(如添加日志、监控等)

    至此,菜品缓存与清理缓存功能模块的代码开发已经实现完毕

    赞(0)
    未经允许不得转载:171主机测评 » 苍穹外卖【day7|缓存菜品与清空缓存】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址