在苍穹外卖这类高并发的餐饮系统中,菜品的新增和删除功能看似简单,实则对后端架构提出了不小的挑战。稍有不慎,就会导致数据不一致、接口响应缓慢,甚至引发雪崩效应。本文将深入探讨苍穹外卖系统中菜品新增和删除功能的底层原理、架构设计、以及实战中需要注意的各种坑,并提供具体的代码和配置示例。
问题场景重现:高并发下的数据一致性挑战
想象一下,在高峰时段,大量的商家同时进行菜品的新增和删除操作。如果没有合理的设计,很容易出现以下问题:
- 数据库锁竞争激烈: 多个线程同时修改同一张表,导致数据库锁等待,影响性能。
- 缓存数据不一致: 菜品信息修改后,缓存没有及时更新,导致用户看到过时的数据。
- 消息队列积压: 如果使用消息队列异步更新相关服务,在高并发下可能导致消息积压,延缓数据同步。
底层原理深度剖析:CAP 理论与最终一致性
在分布式系统中,CAP 理论告诉我们,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三者不可兼得。对于苍穹外卖系统,分区容错性是必须保证的,因此我们需要在一致性和可用性之间做出权衡。通常情况下,我们会选择最终一致性,即允许数据在短时间内不一致,但最终会达到一致的状态。
为了实现最终一致性,我们可以采用以下策略:
- 读写分离: 将读操作和写操作分离到不同的数据库节点上,减轻数据库的压力。
- 缓存: 使用 Redis 等缓存服务,缓存热点数据,提高读取速度。同时,需要考虑缓存失效策略,防止缓存雪崩。
- 消息队列: 使用 RabbitMQ、Kafka 等消息队列,异步更新相关服务的数据,降低耦合性。
具体的代码/配置解决方案
下面以 Spring Boot + MySQL + Redis + RabbitMQ 为例,演示如何实现菜品新增和删除功能。
1. 数据库设计
CREATE TABLE `dish` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`category_id` bigint NOT NULL COMMENT '菜品分类id',
`name` varchar(255) NOT NULL COMMENT '菜品名称',
`price` decimal(10,2) NOT NULL COMMENT '菜品价格',
`image` varchar(255) DEFAULT NULL COMMENT '菜品图片',
`description` varchar(255) DEFAULT NULL COMMENT '菜品描述',
`status` int DEFAULT '0' COMMENT '0表示禁用,1表示启用',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
`create_user` bigint NOT NULL COMMENT '创建人',
`update_user` bigint NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';
2. 新增菜品接口
@PostMapping("/dish")
public Result<String> save(@RequestBody DishDTO dishDTO) {
dishService.saveWithFlavor(dishDTO);
return Result.success("新增菜品成功");
}
3. 删除菜品接口
@DeleteMapping("/dish")
public Result<String> delete(@RequestParam List<Long> ids) {
dishService.deleteBatch(ids);
return Result.success("菜品删除成功");
}
4. 使用 Redis 缓存菜品信息
@Autowired
private RedisTemplate redisTemplate;
public Dish getDishById(Long id) {
String key = "dish:" + id;
Dish dish = (Dish) redisTemplate.opsForValue().get(key);
if (dish == null) {
dish = dishMapper.selectById(id);
redisTemplate.opsForValue().set(key, dish, 60, TimeUnit.MINUTES); // 设置过期时间为 60 分钟
}
return dish;
}
public void deleteDishCache(Long id) {
String key = "dish:" + id;
redisTemplate.delete(key);
}
5. 使用 RabbitMQ 异步更新相关服务
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(Long dishId) {
rabbitTemplate.convertAndSend("dish.exchange", "dish.update", dishId);
}
6. Nginx 配置 (作为反向代理和负载均衡)
http {
upstream backend {
server 192.168.1.100:8080; # 服务实例1
server 192.168.1.101:8080; # 服务实例2
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
宝塔面板可以方便地管理 Nginx 和其他服务,但需要注意安全配置,例如限制 IP 访问、设置防火墙等。在处理高并发连接时,需要合理调整 Nginx 的 worker 进程数、连接超时时间等参数。
实战避坑经验总结
- 避免大事务: 菜品新增和删除操作可能涉及多个表,尽量避免使用大事务,可以考虑使用 Saga 模式或 TCC 模式。
- 缓存穿透: 如果大量请求查询不存在的菜品,会导致缓存穿透,直接请求数据库。可以使用布隆过滤器来解决。
- 缓存击穿: 如果某个热点菜品的缓存失效,大量请求同时请求数据库,会导致缓存击穿。可以使用互斥锁或永不过期的缓存来解决。
- 消息队列积压: 如果消息队列积压,需要及时处理,例如扩容队列、优化消费者逻辑等。可以考虑设置死信队列,处理失败的消息。
- 数据库连接池配置: 合理配置数据库连接池的大小,避免连接数不足或连接数过多导致性能下降。常用的连接池有 Druid、HikariCP 等。
在实际应用中,还需要根据具体场景进行优化。例如,可以使用 CDN 加速静态资源的访问,使用 Elasticsearch 进行菜品搜索,使用 Prometheus 监控系统性能等。
冠军资讯
代码旅行家