欢迎光临
我们一直在努力

【 苍穹外卖day7 | 缓存 】

前言

用户端浏览菜品时,同一个分类会被反复查询;如果每次请求都访问数据库,菜品和口味的组装逻辑会重复执行。苍穹外卖在这里把分类菜品列表放进 Redis,查询优先读取缓存;后台修改菜品后再清理对应缓存,保证用户端能拿到新数据。

day07 还引入了 Spring Cache。它把缓存读写封装到注解中,适合规则清晰的查询接口。本文结合项目现有代码,把两种实现串起来。

一、先看项目中的 Redis 配置

项目在 sky-server 模块中引入了 spring-boot-starter-data-redis 和 spring-boot-starter-cache。连接信息由 application.yml 从当前环境配置中读取,RedisConfiguration 再创建 RedisTemplate。

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}

这里给 key 配置 StringRedisSerializer,Redis 中的键会以可读字符串保存,例如 dish_12;后续使用 keys(“dish_*”) 按模式清理时也更容易定位。Spring Data Redis 官方文档说明,RedisTemplate 的默认序列化器会处理对象读写,而 key、value、hash field 可以分别配置序列化器。

配置文件中的 Redis 密码、地址等属于环境信息,提交代码或写博客时应使用占位符或脱敏值,不能直接暴露真实配置。

二、菜品列表:缓存命中直接返回,未命中再查询数据库

用户端接口是 GET /user/dish/list,参数为分类 id。缓存 key 按分类维度构造,规则是 dish_分类 id;不同分类的数据彼此隔离。

@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.size() > 0) {
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);
}

这段代码分为两次访问路径:

  • 缓存命中:opsForValue().get(key) 取到列表后直接响应,不进入 Service 和数据库。
  • 缓存未命中:按分类 id 和起售状态查询菜品,再把结果写回 Redis,下一次同分类请求即可命中缓存。
  • Service 层的 listWithFlavor 不只是查询菜品表,还会按菜品 id 查询口味并封装成 DishVO。因此缓存保存的是已经组装好的菜品列表,避免每次浏览菜单都重复查询口味。

    Spring Data Redis 中,opsForValue() 对应 Redis 的字符串值操作;当前代码用 get 读取、用 set 写入,key 必须非空。项目使用原始类型 RedisTemplate,所以从缓存取出 List 时出现了强制转换;实际开发中可以优先声明 RedisTemplate<String, Object> 或按项目统一的序列化方案约束数据类型。

    三、缓存更新:菜品变更后必须清理旧数据

    缓存只解决读性能,数据变更后的旧缓存同样需要处理。后台菜品管理接口在新增、修改、删除和起售停售后都会调用 cleanCache。

    private void cleanCache(String pattern) {
    Set keys = redisTemplate.keys(pattern);
    redisTemplate.delete(keys);
    }

    新增菜品只影响当前分类,因此按 dish_ + 分类 id 清理;修改、批量删除、起售停售可能影响多个分类或列表展示,当前项目统一清理 dish_*。

    // 新增菜品后,清理当前分类
    String key = "dish_" + dishDTO.getCategoryId();
    cleanCache(key);

    // 修改、删除、起售停售后,清理全部菜品缓存
    cleanCache("dish_*");

    下一次用户端请求会发现 Redis 中没有对应 key,于是回源数据库并写入最新列表。这种“更新数据库后删除缓存、读取时再填充缓存”的方式,代码路径直观,也能避免用户端长期拿到旧菜品图片、价格或状态。

    需要注意:keys(pattern) 会按模式扫描键。当前课程项目的键数量很少,便于理解和实现;在键空间很大的生产环境中,应评估扫描成本,并根据缓存前缀、批量删除策略或更精确的 key 设计控制影响范围。

    四、店铺营业状态:Redis 也适合保存轻量状态

    项目还使用固定 key shop_status 保存营业状态。管理员接口写入 1 或 0,用户端接口读取该值;当 Redis 中还没有该 key 时,代码默认返回打烊状态。

    // 管理端设置状态
    redisTemplate.opsForValue().set("shop_status", status);

    // 用户端获取状态
    Integer status = (Integer) redisTemplate.opsForValue().get("shop_status");
    if (status == null) {
    status = StatusConstant.DISABLE;
    }

    营业状态由管理员直接维护为一份共享状态,不需要先查询数据库再填充列表缓存。两者都使用 RedisTemplate,但 key 的设计和写入时机要随着业务含义变化。

    五、Spring Cache:把套餐列表缓存写进注解

    项目启动类已经添加 @EnableCaching,开启缓存注解支持。用户端套餐列表使用 @Cacheable:调用方法前会先根据缓存名称和 key 查询缓存,命中时直接返回;没有缓存时才执行方法,并将返回值缓存起来。

    @GetMapping("/list")
    @Cacheable(cacheNames = "setmealCache", key = "#categoryId")
    public Result<List<Setmeal>> list(Long categoryId) {
    Setmeal setmeal = new Setmeal();
    setmeal.setCategoryId(categoryId);
    setmeal.setStatus(StatusConstant.ENABLE);

    List<Setmeal> list = setmealService.list(setmeal);
    return Result.success(list);
    }

    cacheNames 是缓存名称,key 使用 SpEL 表达式 #categoryId,表示取方法参数 categoryId 作为 key。按项目代码注释,生成的 Redis key 形式为 setmealCache::分类 id。

    day07 中的另外两个常用注解可以这样理解:

    注解执行时机适用场景
    @Cacheable 先查缓存;未命中才执行方法并缓存返回值 查询接口
    @CachePut 方法执行后把返回值写入缓存 新增或更新后需要立即刷新单条缓存
    @CacheEvict 方法执行后删除指定缓存 删除、修改、状态变更后清理旧缓存

    当前项目的套餐查询已经使用 @Cacheable,但后台套餐新增、修改、删除和状态变更代码中暂未看到对应的缓存清理注解。这意味着如果套餐数据发生变化,setmealCache 的失效策略需要在后续功能中补齐;否则已缓存的套餐列表可能继续返回旧结果。

    六、手动 RedisTemplate 和 Spring Cache 怎么选择

    菜品列表选择手动操作 RedisTemplate,是因为 key 前缀、按分类清理和 dish_* 批量失效规则都写得很明确;开发者可以完整控制每一步。

    套餐列表选择 @Cacheable,查询规则单一,注解能省去显式 get、set 的样板代码。两种方式的共同点是:缓存 key 必须能区分不同查询条件,写操作必须考虑旧缓存的失效。

    可以按下面的思路判断:

    • 查询逻辑简单、键规则稳定:优先使用 Spring Cache 注解。
    • 需要组合 key、按模式删除、设置特殊过期策略或实现复杂缓存流程:使用 RedisTemplate 更合适。
    • 无论使用哪种方式,都要先明确“数据在什么操作后变化、哪些 key 会受影响”。

    七、如何验证缓存是否生效

    可以按接口链路验证,而不依赖截图:

  • 清理某个 dish_分类 id key,调用用户端菜品列表接口,确认请求会走 Service 查询并写入缓存。
  • 再次用相同分类 id 调用接口,预期直接命中 Redis,不再执行菜品和口味的查询链路。
  • 在后台新增、修改、删除或停售该分类菜品后,检查对应 dish key 是否被删除。
  • 再次查询用户端列表,预期缓存未命中、回源数据库并写入新数据。
  • 调用用户端套餐列表两次,观察第二次是否绕过套餐查询;随后修改套餐数据时,重点检查 setmealCache 是否有失效处理。
  • 如果用户端仍显示旧菜品信息,排查顺序可以先看 Redis 服务是否连接正常,再检查 dish_* 是否残留,最后核对后台写操作是否走到了 cleanCache。

    总结

    day07 的核心是把“查询先读缓存、未命中再查询数据库、数据变化后删除缓存”落到实际接口中。菜品列表通过 RedisTemplate 手动控制缓存读写与失效,套餐列表通过 Spring Cache 注解压缩查询缓存代码。缓存实现的重点不只在写入 Redis,更在于把 key 设计和数据变更后的清理范围一起设计清楚。

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

    评论 抢沙发

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