在原版教程中,存在两处缓存的应用:
- 套餐分类:使用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,主要是因为其可以对单一字段进行操作。
既然已经在数据改动时清除了缓存,为什么还要再设置缓存过期时间?
主要有两方面考虑:
-
容错机制
- 极端情况:如果在执行删除操作时,数据库(MySQL)执行成功了,但是 Redis 在执行删除代码时,突然因为某个原因导致服务器宕机或 Redis 挂了
- 后果:如果没有过期时间,那么这份脏数据将永久驻留在 Redis 中,那么用户将看到错误的数据
- 设置过期时间则保证了,即使在极端环境下,也能实现数据的最终一致性
-
内存管理
- 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