The Sejolyn Memo

Back

在原版教程中,存在两处缓存的应用:

  • 套餐分类:使用Spring Cache进行缓存,底层实现为Redis
  • 店铺状态:使用Redis的String类型缓存

该项目还有一些优化点。

数据类型缓存策略理由
购物车Hash高频读写
菜品分类Hash高频查询
套餐分类Hash高频查询
店铺状态String高频查询
分类列表Hash极少变更,高频查询
套餐详情Hash包含关联数据
地址薄Hash用户独立数据

购物车#

购物车属于典型的**“高频读写、临时性强“**的数据,其临时性很强,非常适合迁移到Redis中而不是数据库表里。

结构设计:

数据结构: Hash
Key格式: shoppingCart_{userId}
Field格式: dish_{dishId}_{flavor} 或 setmeal_{setmealId}
Value: ShoppingCart对象
过期时间: 1小时
plaintext

为什么使用Redis而不是Spring Cache?

与Spring Cache相比,Redis的显著优势在于其对粒度的精细控制,这也是购物车缓存不使用Spring Cache的核心原因

  • Spring Cahce的工作模式是全量/粗粒度的,如果我仅仅在购物车中增加一份米饭,那么整个购物车的缓存都会失效。也就是说,为了修改一个小数据,浪费了其他未改动数据的序列化和传输开销
  • Redis的工作模式是增量/细粒度的,同样是在购物车汇总增加一份米饭,在使用Redis Hash结构的情况下,我只需要清除该米饭的缓存(在查询时懒加载),而其他商品的缓存依然继续使用

后面的缓存基本上都是使用Redis Hash,主要是因为其可以对单一字段进行操作。

既然已经在数据改动时清除了缓存,为什么还要再设置缓存过期时间?

主要有两方面考虑:

  1. 容错机制

    • 极端情况:如果在执行删除操作时,数据库(MySQL)执行成功了,但是 Redis 在执行删除代码时,突然因为某个原因导致服务器宕机或 Redis 挂了
    • 后果:如果没有过期时间,那么这份脏数据将永久驻留在 Redis 中,那么用户将看到错误的数据
    • 设置过期时间则保证了,即使在极端环境下,也能实现数据的最终一致性
  2. 内存管理

    • Redis是内存数据库,其所有数据都是存储在内存的;而我们又都知道,内存是及其昂贵的资源
    • 缓存的目标是热数据,即经常被访问的数据;如果没有设置过期时间,那么Redis可能会被冷数据填满(比如10年前用户的购物车)

菜品分类查询#

缓存策略:

数据结构: Hash
Key格式: dish_category_{categoryId}  
Field格式: dish_{dishId}  
Value: Dish 对象
过期时间: 1小时
plaintext

当更新菜品时,其分类是否更新是不确定的。如果更新了分类,那么需要把旧分类和新分类的缓存全都清除

另外一点需要注意的是,DishServiceImpl 中存在两个菜品分类查询的接口:

  • getByCategoryId:返回 Dish,被管理端调用
  • getWithFlavorByCategoryId:返回 DishVO,被用户端调用

如果全都实现缓存,那么需要使用两个不同的 Key;但是由于管理端访问频率较低,所以这里只实现用户端的接口。

为什么不能使用同一个 HashKey?

因为二者返回的数据结构不同,会导致类型转换异常和相互覆盖。

套餐分类查询#

与「菜品分类查询」类似。

缓存策略

数据结构: Hash  
Key格式: setmeal_category_{categoryId}  
Field格式: setmeal_{setmealId}  
Value: Setmeal 对象
过期时间: 1小时
plaintext

当更新套餐时,其分类是否更新是不确定的。如果更新了分类,那么需要把旧分类和新分类的缓存全都清除

套餐详情查询#

用户查看套餐详情时,服务端涉及 setmeal + setmeal_dish 的联表查询,且套餐的修改频率较低,因此同样可以用缓存来提高效率。

缓存策略:

数据结构: Hash
Key: setmeal_detail_{setmealId}
Field: dish_{index}_{name}
Value: DishItemVO 对象
过期时间: 2小时
plaintext
  • 由于 DishItemVO 中没有 DishId,为了确保唯一性,所以使用「索引+名称」作为 field

分类列表#

用户打开小程序首页时必查分类,而且分类是基础数据,变更极少,因此非常推荐缓存。

缓存策略:

数据结构: Hash
Key: category_type_{type}
Field: category_{categoryId}
Value: Category 对象
过期时间:24小时
plaintext
  • 这里采用分类型缓存,type:1-菜品分类,2-套餐分类
  • 分类的变更频率极低,因此过期时间可以适当延长

对于新增的分类,其状态默认为 0(禁用 ),因此可以不必立即清理缓存,可以等到启用的时候再清理。

店铺营业状态#

店铺营业状态应该是读取频率最高的了,而且其实现也很简单。

缓存策略:

数据结构: String
Key: SHOP_STATUS
Value: Integer (0-停业, 1-营业)
过期时间: 永久(手动更新)
plaintext

用户地址簿#

用户地址簿可能变更相对频繁,之所以将其缓存是因为其查询次数较多,且数据是按用户隔离的。

缓存策略

数据结构: Hash
Key: address_book_{userId}
Field: address_{addressId}
Value: addressBook对象
过期时间: 30分钟(会话期间)
plaintext
「苍穹外卖」复盘:Redis缓存
https://sejolyn.fyi/blog/sky-take-out/redis-cache
Author Sejolyn
Published at December 19, 2025
Comment seems to stuck. Try to refresh?✨