1. 菜品与套餐缓存架构设计
在用户量较大的外卖平台中,数据库查询压力主要来自高频访问的菜品展示页面。我们采用Redis作为缓存中间件,设计了两级缓存策略:
1.1 菜品缓存实现方案
菜品数据的特点是分类明确、变更频率中等。我们采用分类维度进行缓存,每个菜品分类对应一个Redis键:
// 构造Redis键示例 String key = "dish_" + categoryId; // 缓存查询逻辑 List<DishVO> cachedData = (List<DishVO>) redisTemplate.opsForValue().get(key); if(cachedData != null && !cachedData.isEmpty()) { return cachedData; }关键设计要点:
- 缓存键设计:采用
dish_分类ID的命名规则,避免键冲突 - 缓存雪崩防护:为不同分类设置随机过期时间(30分钟±5分钟随机值)
- 数据一致性:通过双删策略保证缓存与数据库同步
1.2 缓存更新策略
当菜品数据变更时,需要同步清理对应缓存。我们在DishController中封装了统一的缓存清理方法:
private void cleanCache(String pattern) { Set keys = redisTemplate.keys(pattern); if(keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } }具体应用场景:
- 新增菜品:清理对应分类的缓存
- 批量删除:清理所有菜品缓存(
dish_*) - 菜品状态变更:清理所有缓存保证数据强一致
2. Spring Cache在套餐缓存中的应用
2.1 Spring Cache核心注解
套餐数据更适合使用声明式缓存,我们采用Spring Cache + Redis的方案:
<!-- 依赖配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>主要注解使用场景:
| 注解 | 应用场景 | 示例 |
|---|---|---|
| @Cacheable | 套餐查询接口 | @Cacheable(cacheNames="setmealCache", key="#categoryId") |
| @CacheEvict | 套餐修改/删除接口 | @CacheEvict(cacheNames="setmealCache", allEntries=true) |
2.2 缓存一致性实践
套餐与菜品不同,其关联数据更多,我们采用更激进的缓存清理策略:
@PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result save(@RequestBody SetmealDTO setmealDTO) { // 业务逻辑 }这种设计虽然会短暂增加数据库压力,但能确保:
- 套餐基础信息变更立即生效
- 关联的菜品数据变化能被及时感知
- 避免复杂的关联缓存维护逻辑
3. 购物车系统实现细节
3.1 数据结构设计
购物车采用关系型数据库存储,核心字段设计:
CREATE TABLE `shopping_cart` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `image` varchar(255) DEFAULT NULL, `user_id` bigint NOT NULL, `dish_id` bigint DEFAULT NULL, `setmeal_id` bigint DEFAULT NULL, `dish_flavor` varchar(50) DEFAULT NULL, `number` int NOT NULL DEFAULT '1', `amount` decimal(10,2) NOT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;设计特点:
- 用户维度分区:通过user_id关联用户
- 商品类型区分:dish_id和setmeal_id互斥
- 数量合并:相同商品多次添加只增加number字段值
- 冗余字段:存储商品快照信息,避免商品变更影响历史订单
3.2 核心业务逻辑实现
添加购物车的核心流程:
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) { // 1. 参数转换 ShoppingCart cart = convertToEntity(shoppingCartDTO); // 2. 查询是否已存在 List<ShoppingCart> existing = queryExistingCart(cart); if(!existing.isEmpty()) { // 3. 存在则更新数量 updateCartNumber(existing.get(0)); } else { // 4. 不存在则新增记录 insertNewCart(cart); } }关键处理逻辑:
- 线程安全:通过数据库唯一索引防止并发问题
- 数据校验:检查菜品/套餐是否存在且有效
- 口味处理:对菜品需要特殊处理口味信息
4. 性能优化与异常处理
4.1 缓存性能优化
针对高并发场景的优化措施:
- 缓存预热:定时任务在低峰期预加载热门分类数据
- 本地缓存:使用Caffeine作为Redis前置缓存
- 批量操作:使用Redis管道技术提升批量删除效率
// 使用管道批量删除 redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for(String key : keys) { connection.del(key.getBytes()); } return null; });4.2 购物车并发控制
解决购物车并发问题的两种方案:
方案一:乐观锁实现
@Update("UPDATE shopping_cart SET number = #{number}, version = version + 1 WHERE id = #{id} AND version = #{version}") int optimisticUpdate(ShoppingCart cart);方案二:分布式锁实现
public void safeAddCart(ShoppingCartDTO dto) { String lockKey = "cart_lock:" + dto.getUserId(); try { // 尝试获取锁 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if(Boolean.TRUE.equals(locked)) { addShoppingCart(dto); } else { throw new BusinessException("操作太频繁"); } } finally { // 释放锁 redisTemplate.delete(lockKey); } }4.3 异常处理策略
针对不同异常的处理建议:
缓存穿透:
- 对空结果进行缓存(设置较短过期时间)
- 使用布隆过滤器预先校验Key
缓存雪崩:
- 差异化过期时间
- 实现熔断降级机制
数据不一致:
- 采用延迟双删策略
- 通过消息队列实现最终一致性
5. 实际开发中的经验总结
5.1 缓存使用注意事项
键设计规范:
- 使用冒号分层(如
dish:category:1) - 避免过长的键名
- 统一大小写规范
- 使用冒号分层(如
序列化选择:
- 优先使用JSON序列化
- 避免Java原生序列化带来的兼容问题
内存控制:
- 设置合理的过期时间
- 监控Redis内存使用情况
- 对大对象考虑分片存储
5.2 购物车开发心得
用户体验优化:
- 合并相同商品条目
- 保留商品快照信息
- 实现快速增减数量功能
性能优化技巧:
- 批量查询商品信息
- 异步记录操作日志
- 使用冗余字段减少关联查询
扩展性考虑:
- 设计支持多店铺的购物车
- 预留优惠券计算接口
- 支持购物车商品收藏功能
在具体实现时,我们发现使用Redis Hash结构存储购物车数据可能更适合大型电商平台,但对于外卖这种轻量级场景,关系型数据库的方案更易于维护和保证事务一致性。