【 苍穹外卖day7 | 缓存 】

📅 2026/7/16 8:45:05
【 苍穹外卖day7 | 缓存 】
前言用户端浏览菜品时同一个分类会被反复查询如果每次请求都访问数据库菜品和口味的组装逻辑会重复执行。苍穹外卖在这里把分类菜品列表放进 Redis查询优先读取缓存后台修改菜品后再清理对应缓存保证用户端能拿到新数据。day07 还引入了 Spring Cache。它把缓存读写封装到注解中适合规则清晰的查询接口。本文结合项目现有代码把两种实现串起来。一、先看项目中的 Redis 配置项目在 sky-server 模块中引入了 spring-boot-starter-data-redis 和 spring-boot-starter-cache。连接信息由 application.yml 从当前环境配置中读取RedisConfiguration 再创建 RedisTemplate。BeanpublicRedisTemplateredisTemplate(RedisConnectionFactoryredisConnectionFactory){RedisTemplateredisTemplatenewRedisTemplate();redisTemplate.setConnectionFactory(redisConnectionFactory);redisTemplate.setKeySerializer(newStringRedisSerializer());returnredisTemplate;}这里给 key 配置 StringRedisSerializerRedis 中的键会以可读字符串保存例如 dish_12后续使用 keys(“dish_*”) 按模式清理时也更容易定位。Spring Data Redis 官方文档说明RedisTemplate 的默认序列化器会处理对象读写而 key、value、hash field 可以分别配置序列化器。配置文件中的 Redis 密码、地址等属于环境信息提交代码或写博客时应使用占位符或脱敏值不能直接暴露真实配置。二、菜品列表缓存命中直接返回未命中再查询数据库用户端接口是 GET /user/dish/list参数为分类 id。缓存 key 按分类维度构造规则是 dish_分类 id不同分类的数据彼此隔离。GetMapping(/list)publicResultListDishVOlist(LongcategoryId){Stringkeydish_categoryId;ListDishVOlist(ListDishVO)redisTemplate.opsForValue().get(key);if(list!nulllist.size()0){returnResult.success(list);}DishdishnewDish();dish.setCategoryId(categoryId);dish.setStatus(StatusConstant.ENABLE);listdishService.listWithFlavor(dish);redisTemplate.opsForValue().set(key,list);returnResult.success(list);}这段代码分为两次访问路径缓存命中opsForValue().get(key) 取到列表后直接响应不进入 Service 和数据库。缓存未命中按分类 id 和起售状态查询菜品再把结果写回 Redis下一次同分类请求即可命中缓存。Service 层的 listWithFlavor 不只是查询菜品表还会按菜品 id 查询口味并封装成 DishVO。因此缓存保存的是已经组装好的菜品列表避免每次浏览菜单都重复查询口味。Spring Data Redis 中opsForValue() 对应 Redis 的字符串值操作当前代码用 get 读取、用 set 写入key 必须非空。项目使用原始类型 RedisTemplate所以从缓存取出 List 时出现了强制转换实际开发中可以优先声明 RedisTemplateString, Object 或按项目统一的序列化方案约束数据类型。三、缓存更新菜品变更后必须清理旧数据缓存只解决读性能数据变更后的旧缓存同样需要处理。后台菜品管理接口在新增、修改、删除和起售停售后都会调用 cleanCache。privatevoidcleanCache(Stringpattern){SetkeysredisTemplate.keys(pattern);redisTemplate.delete(keys);}新增菜品只影响当前分类因此按 dish_ 分类 id 清理修改、批量删除、起售停售可能影响多个分类或列表展示当前项目统一清理 dish_*。// 新增菜品后清理当前分类Stringkeydish_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);// 用户端获取状态Integerstatus(Integer)redisTemplate.opsForValue().get(shop_status);if(statusnull){statusStatusConstant.DISABLE;}营业状态由管理员直接维护为一份共享状态不需要先查询数据库再填充列表缓存。两者都使用 RedisTemplate但 key 的设计和写入时机要随着业务含义变化。五、Spring Cache把套餐列表缓存写进注解项目启动类已经添加 EnableCaching开启缓存注解支持。用户端套餐列表使用 Cacheable调用方法前会先根据缓存名称和 key 查询缓存命中时直接返回没有缓存时才执行方法并将返回值缓存起来。GetMapping(/list)Cacheable(cacheNamessetmealCache,key#categoryId)publicResultListSetmeallist(LongcategoryId){SetmealsetmealnewSetmeal();setmeal.setCategoryId(categoryId);setmeal.setStatus(StatusConstant.ENABLE);ListSetmeallistsetmealService.list(setmeal);returnResult.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 设计和数据变更后的清理范围一起设计清楚。