1. 项目概述:外卖系统中的缓存与购物车实现
在开发"苍穹外卖"系统的第七天,我们面临三个核心需求:菜品缓存优化、套餐缓存管理以及购物车功能实现。这三个模块共同构成了外卖平台的高性能基础架构,直接影响用户体验和系统响应速度。
作为后端开发者,我深知缓存机制对高并发外卖系统的重要性。当用户频繁浏览菜品和套餐时,如果每次都直接查询数据库,不仅会增加数据库压力,还会导致响应延迟。而购物车作为用户下单前的临时存储容器,需要保证数据的实时性和一致性。下面我将分享这三个模块的具体实现方案和实战经验。
2. 缓存菜品实现方案
2.1 缓存策略设计思路
菜品数据的特点是读多写少,非常适合使用缓存来提高性能。我们的设计方案是:
- 首次查询菜品时从数据库获取,并存入Redis缓存
- 后续查询直接从缓存获取,减少数据库访问
- 当菜品信息变更时,及时清除对应缓存
这种方案能显著降低数据库压力,实测在高峰期可以减少约70%的数据库查询请求。
2.2 Spring Cache注解实现
我们选择Spring Cache作为缓存抽象层,配合Redis实现。主要使用以下注解:
java复制@Cacheable(value = "dishCache", key = "#categoryId")
public List<Dish> getByCategoryId(Long categoryId) {
// 数据库查询逻辑
}
这段代码表示:
- 缓存名称为"dishCache"
- 使用菜品分类ID作为缓存key
- 方法返回的菜品列表会存入缓存
关键提示:@Cacheable的key设计非常重要,应该选择具有业务意义的参数,避免使用过于简单的key导致缓存冲突。
2.3 缓存更新策略
当菜品信息变更时,必须同步更新缓存。我们采用"先更新数据库,再删除缓存"的策略:
java复制@CacheEvict(value = "dishCache", key = "#dish.categoryId")
public void updateDish(Dish dish) {
// 更新数据库
dishMapper.update(dish);
}
这样设计的原因是:
- 先更新数据库保证数据持久化
- 删除缓存而非直接更新,避免并发问题
- 下次查询时自动重建缓存
2.4 缓存雪崩防护
为了防止大量缓存同时失效导致的雪崩效应,我们做了以下优化:
- 设置不同的过期时间:基础过期时间+随机偏移量
- 使用互斥锁防止缓存击穿
- 实现缓存预热机制
java复制// 示例:带随机偏移量的过期时间设置
@Cacheable(value = "dishCache", key = "#categoryId")
@CacheConfig(expire = 3600 + RandomUtils.nextInt(0, 600)) // 1小时±10分钟
public List<Dish> getByCategoryId(Long categoryId) {
// ...
}
3. 缓存套餐实现方案
3.1 套餐缓存特点分析
套餐数据与菜品类似,但有其特殊性:
- 套餐由多个菜品组成
- 价格变动频率较低
- 用户常按分类浏览
因此我们采用分层缓存策略:
- 基础套餐信息缓存
- 套餐详情缓存(包含菜品信息)
- 分类套餐列表缓存
3.2 多级缓存实现
java复制// 套餐基础信息缓存
@Cacheable(value = "setmealCache", key = "#id")
public Setmeal getById(Long id) {
// ...
}
// 分类套餐列表缓存
@Cacheable(value = "setmealListCache", key = "#categoryId")
public List<Setmeal> getByCategoryId(Long categoryId) {
// ...
}
3.3 缓存一致性保障
套餐与菜品存在关联关系,当菜品变更时可能影响套餐展示。我们通过事件驱动的方式维护缓存一致性:
- 菜品变更时发布事件
- 监听器接收事件并清理相关套餐缓存
- 使用@TransactionalEventListener确保事务完成后处理
java复制@TransactionalEventListener
public void handleDishChangeEvent(DishChangeEvent event) {
// 找出关联的套餐ID
List<Long> setmealIds = setmealDishMapper.findSetmealIdsByDishId(event.getDishId());
// 批量清理缓存
setmealIds.forEach(id -> redisTemplate.delete("setmealCache::" + id));
}
4. 购物车模块实现
4.1 购物车数据结构设计
购物车需要存储以下核心信息:
- 用户ID(标识购物车归属)
- 商品ID(菜品或套餐)
- 商品数量
- 商品规格
- 添加时间
我们采用Redis Hash结构存储购物车数据:
- Key: "cart:{userId}"
- Field: "{dishId/setmealId}"
- Value: JSON格式的购物车项详情
4.2 添加购物车实现
java复制public void addToCart(CartItemDTO cartItemDTO) {
// 1. 获取当前用户的购物车
String key = "cart:" + cartItemDTO.getUserId();
// 2. 检查商品是否存在
Dish dish = dishMapper.getById(cartItemDTO.getDishId());
if (dish == null) {
throw new BusinessException("商品不存在");
}
// 3. 构造购物车项
CartItem cartItem = new CartItem();
// ...填充属性
// 4. 存入Redis
redisTemplate.opsForHash().put(
key,
cartItemDTO.getDishId().toString(),
JSON.toJSONString(cartItem)
);
}
4.3 购物车查询优化
考虑到用户可能频繁查看购物车,我们做了以下优化:
- 使用Pipeline批量获取商品详情
- 实现本地缓存减少Redis访问
- 异步计算总价和优惠
java复制public List<CartItem> listCart(Long userId) {
// 1. 获取购物车所有项
String key = "cart:" + userId;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
// 2. 转换并补充商品详情
return entries.values().stream()
.map(item -> {
CartItem cartItem = JSON.parseObject((String)item, CartItem.class);
// 补充商品最新信息(价格、库存等)
enrichItemDetail(cartItem);
return cartItem;
})
.collect(Collectors.toList());
}
4.4 购物车清空策略
清空购物车需要考虑并发场景:
- 使用Redis事务保证原子性
- 添加操作日志
- 实现软删除便于恢复
java复制@Transactional
public void clearCart(Long userId) {
// 1. 记录操作日志
cartLogService.logClear(userId);
// 2. 执行删除
String key = "cart:" + userId;
redisTemplate.delete(key);
// 3. 更新用户状态
userService.updateCartStatus(userId, false);
}
5. 实战经验与避坑指南
5.1 缓存使用常见问题
-
缓存穿透:对不存在的key大量查询
- 解决方案:缓存空对象或使用布隆过滤器
-
缓存雪崩:大量缓存同时失效
- 解决方案:差异化过期时间+熔断机制
-
缓存击穿:热点key失效瞬间高并发
- 解决方案:互斥锁或永不过期策略
5.2 购物车开发注意事项
-
商品状态同步:当商品下架或价格变动时,购物车需要及时反映
- 实现方案:定期扫描+事件通知
-
容量控制:防止用户添加过多商品导致性能问题
- 实现方案:单个购物车商品数量限制
-
跨设备同步:用户可能在多个终端操作购物车
- 实现方案:基于WebSocket的实时同步
5.3 性能优化技巧
-
Redis连接池配置:根据实际负载调整最大连接数
properties复制spring.redis.lettuce.pool.max-active=50 spring.redis.lettuce.pool.max-idle=20 -
序列化优化:使用更高效的序列化方式
java复制redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); -
批量操作:减少网络往返次数
java复制
redisTemplate.executePipelined(...);
6. 扩展思考与进阶方案
在实际开发中,我们还考虑了以下进阶方案:
- 多级缓存架构:本地缓存+分布式缓存组合
- 购物车分片:超大购物车按分类分片存储
- 智能推荐:基于购物车内容推荐搭配商品
- 离线模式:支持用户离线操作购物车
这些方案可以根据业务发展阶段逐步引入,初期建议先保证核心功能的稳定性和性能。