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

缓存菜品

实现思路

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 模式:

  1. 先更新数据库
  2. 再删除缓存(而非更新缓存)

为什么不直接更新缓存?

  • 更新缓存可能导致并发场景下的数据不一致
  • 删除缓存更简单可靠,下次读取时自动重建

2. 最小影响原则

能精准清理的场景绝不全量清理:

  • 新增菜品 → 精准清理(只影响一个分类)
  • 删除/修改 → 全量清理(影响范围不确定)

3. 统一抽象

将缓存清理逻辑封装为 cleanCache() 方法:

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

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