苍穹外卖中的菜品缓存与更新策略

菜品列表常被重复读取,适合评估缓存。但缓存会成为数据库数据的副本,更新菜品、分类或售卖状态时,必须明确哪些缓存键受影响,以及更新失败时如何处理。

1. 明确缓存对象和 Key

例如缓存“某门店某分类下的可售菜品列表”,Key 至少要包含门店和分类两个维度:

1
dish:list:{shopId}:{categoryId}

如果查询结果还受售卖状态、用户身份或分页参数影响,这些因素也要纳入 Key,或调整缓存粒度。缓存整个列表实现简单,但任一菜品变化时可能要清理较大范围的数据。

2. 读取流程

1
2
3
查询 Redis
├─ 命中:反序列化并返回
└─ 未命中:查询数据库 → 写入缓存并设置 TTL → 返回

缓存 TTL 可以限制陈旧数据最长保留时间,但不能替代主动失效。还要处理空结果、序列化变化和 Redis 不可用等情况,避免缓存故障直接拖垮数据库。

3. 更新时清理相关缓存

更新菜品时,常见策略是数据库事务成功后删除对应列表缓存,让后续读取重新加载。新增、删除和改变售卖状态也要失效相关 Key。若分类下列表缓存很多,可以采用按分类清理或版本化 Key,具体取决于数据规模与实现成本。

重要竞态是:缓存先被删除,并发读在数据库提交之前读到旧值,再把旧值写回缓存。需要按事务提交时机安排失效,并结合 TTL、重试或事件补偿。单靠 @CacheEvict 的存在,不足以证明所有并发场景都一致。

4. Spring Cache 注解的取舍

@Cacheable 可以表达查询缓存,@CacheEvict 可以表达更新后的失效,@CachePut 则是在执行目标方法后写入返回值。注解方案简洁,但 Key 规则、代理边界、事务顺序和批量失效仍需要显式设计。

一个类内部直接调用自己的缓存方法可能绕过代理;需要先确认项目的代理模式和调用路径。对于复杂失效逻辑,直接调用缓存 API 有时更容易看清时序。

5. 如何验证策略

用一组固定数据按以下顺序检查:

  1. 首次读取是否从数据库加载并写入缓存?
  2. 再次读取是否命中缓存?
  3. 修改菜品后,旧缓存是否被清理?
  4. 并发更新时是否可能重新写入旧值?
  5. Redis 不可用时,数据库的连接池和并发是否有保护?

6. 小结

缓存设计先确定数据粒度,再确定 Key 和失效范围,最后处理并发和故障。项目笔记记录了“分类菜品变更时清理缓存”的思路;文章发布前最好对照实际 Service 代码补上具体时序和测试数据。