高并发系统设计:从一次线上事故说开去
引言:为什么我们要重视高并发?

作为一个从业多年的后端开发工程师,我参与过不少项目,从传统行业的小系统到日均上百万访问量的互联网产品。而真正让我对“高并发”这个词有了深刻认识的,是一次令人印象深刻的生产事故。
那次事故发生在我们团队主导的一个社交电商项目上线不久之后。原本预计首日UV大概在10万左右,结果因为一个短视频突然在抖音火了,当天的访问量直接飙升到了200万+,而且是集中在几个小时内的突增流量。我们的服务瞬间打满,数据库连接池爆掉,Redis连接超时,消息队列堆积如山,最终导致整个平台瘫痪,用户无法下单、登录、甚至首页都打不开。
这次事故之后,我们开始重新审视系统的整体架构和性能瓶颈,并逐步重构了一个具备抗高并发能力的新系统。这篇文章就是基于这个真实项目的经历来写的,我会从问题出发,结合实战经验,聊聊怎么设计和优化一个真正的高并发系统。
问题描述:高并发下的系统表现有多脆弱?

回到那次事故,当时的系统架构大致如下:
- 前端Vue单页应用
- 后端Spring Boot提供REST接口
- MySQL主从架构 + Redis缓存
- RabbitMQ做异步任务分发
- Nginx作为反向代理
看起来配置不算太差,但面对突发的200万PV,整个系统几乎瞬间崩溃。原因如下:
Nginx层面:
- 没有做限流,所有的请求都被放进来;
- 短时间内大量请求导致上游服务负载激增。
Java应用层:
- Spring Boot默认线程池数量有限;
- 接口未加缓存或降级策略,所有请求都直击数据库。
数据库层:
- 主库读写混合,压力过大;
- 查询未走索引;
- 最大连接数被撑爆(max_connections=100)。
缓存层:
- Redis连接池设置不合理,没有连接复用;
- 缓存穿透场景未做处理;
- 热点数据未能有效预热。
异步处理失败:
- RabbitMQ堆积严重,消费者消费速度跟不上;
- 没有死信队列兜底机制;
- 消息积压导致后续处理延迟越来越大。
这些问题叠加起来,最终导致系统完全不可用。最头疼的是,当时还没有自动扩容的能力,只能靠手动重启服务勉强恢复。
解决方案:高并发系统设计的核心思路

经历了那次事故后,我们痛定思痛,开始了为期两个月的技术攻坚。下面是我们在系统层面做的核心优化:
一、流量控制先行 —— 分布式限流与熔断机制
首先要在流量入口就加上限流措施。我们采用了阿里开源的 Sentinel,部署在每个业务节点前进行QPS限流。
举个例子:对于 /product/detail 这个接口,我们设置了每秒最多接受 2000 次请求,超过则返回 429 或降级响应。
@SentinelResource(value = "product-detail", fallback = "fallbackProductDetail")
@GetMapping("/product/detail")
public Product getProduct(@RequestParam("id") Long id) {
// 业务逻辑
}
private Product fallbackProductDetail(Long id, Throwable ex) {
return cacheService.getProductFromLocalCache(id);
}
同时,在网关层使用 Nginx + Lua 脚本做了简单的限流,防止恶意刷接口。
http {
limit_req_zone $binary_remote_addr zone=my_limit:10m rate=20r/s;
server {
location / {
limit_req zone=my_limit burst=5;
proxy_pass http://backend;
}
}
}
二、缓存打底 —— 多级缓存体系构建
为了避免每次请求都访问数据库,我们构建了三级缓存:
- 浏览器本地缓存
- Nginx 层缓存(静态资源)
- Redis + 本地 Caffeine 双缓存
以商品详情为例,缓存加载顺序如下:
- 先查本地内存缓存(Caffeine),命中则返回;
- 未命中,则查 Redis;
- Redis 也没有命中,则查数据库并更新两级缓存;
- 更新完 Redis 后再异步刷新本地缓存(比如通过 Kafka 或定时任务)。
这样既减少了网络 IO,又缓解了缓存雪崩风险。
三、数据库优化 —— 分表分库 + 读写分离
原来的 MySQL 是一个主库扛所有请求,后来我们做了以下调整:
- 垂直分库:把订单、用户、商品等模块拆成单独的数据库;
- 水平分表:使用 ShardingSphere 对订单表按时间进行分片;
- 读写分离:通过 MyCat 做主从切换,提升查询能力;
- 索引优化:针对慢查询做 Explain 分析,增加合适索引;
- 连接池优化:将 Druid 替换为 HikariCP,并调大 maxPoolSize。
部分配置示例:
spring:
datasource:
url: jdbc:mysql://master-host:3306/order_db?useSSL=false&serverTimezone=UTC
username: root
password: xxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 200
minimum-idle: 20
auto-commit: true
四、消息队列解耦 —— RabbitMQ + 死信队列兜底
我们重新设计了异步流程,将下单、支付、库存扣减等关键步骤异步化,避免同步阻塞。
主要结构如下:
下单请求 → 写入 MySQL(事务完成) → 发送 MQ → 消费者异步执行其他业务动作
同时配置了死信队列:
// 定义死信交换器和队列
@Bean
public CustomExchange dlxExchange() {
return new CustomExchange("order.dlx.exchange");
}
@Bean
public Queue dlxQueue() {
return QueueBuilder.durable("order.dlx.queue").build();
}
@Bean
public Binding bindingDLX(Queue dlxQueue, CustomExchange dlxExchange) {
return BindingBuilder.bind(dlxQueue).to(dlxExchange).with("dlx.order.key").noargs();
}
消费者失败后,会被转发到 DLQ 中,再配合监控告警做补救处理。
五、弹性伸缩 + 监控告警
我们迁移到了 Kubernetes 平台,利用 HPA(Horizontal Pod Autoscaler)根据 CPU 使用率自动扩缩容,极大提升了系统的弹性能力。
此外还接入了 Prometheus + Grafana 实现全链路监控,并设置了关键指标告警(如 QPS、JVM 内存、数据库连接数等)。
踩坑经验:那些你以为不会出问题的地方,最后都出了问题
在整个过程中,我们踩了不少坑,这里分享几个典型的:
1. Redis缓存穿透没做兜底
刚开始的时候我们没有对不存在的商品ID做缓存拦截,结果攻击脚本狂刷无效 ID,导致大量请求打到数据库,差点又挂一次。后来我们加了个布隆过滤器,解决这个问题。
2. 本地缓存和 Redis 不一致
有一段时间发现商品价格经常显示旧值。原因是本地缓存和 Redis 的更新不同步,导致页面展示的数据是错的。后来我们引入了一套缓存一致性策略:Redis删除 -> 本地TTL到期自动刷新。
3. Sentinel 规则未持久化
Sentinel 默认规则是放在内存中的,重启后规则丢失。这个问题在测试环境没问题,但上线时没人注意到这点,导致重启服务后限流失效。后来改用 Sentinel Dashboard + nacos 存储规则才搞定。
4. MQ消费者处理不幂等
一开始我们的订单状态变更并没有做幂等处理,MQ重试导致重复发货。后来加了个“唯一业务ID”的记录机制,在插入数据库前先判断是否已处理过这条消息。
效果总结:重构后的系统到底怎么样了?

这次重构完成后,我们又迎来了一次大规模活动促销。这次效果非常显著:
| 指标 | 之前 | 之后 |
|---|---|---|
| QPS峰值 | <500 | >12,000 |
| 数据库连接数 | 经常达到上限 | 稳定在 100 左右 |
| 接口平均响应时间 | 1s+ | <150ms |
| 服务可用性 | 崩溃频繁 | 99.8%以上 |
更重要的是,即使在极端情况(比如某个接口被打爆)下,系统也能保持基本可用,不再出现全局崩溃的情况。
经验分享:给正在路上的你几点建议

如果你也在做高并发系统,或者准备搭建这样的系统,下面几点是我个人的经验总结:
1. 永远不要相信“一切正常”的假象
线上环境远比你想象中复杂,各种异常都有可能出现,要时刻做好兜底预案。
2. 技术选型要看适用性和成熟度
有时候新技术听起来很厉害,但不一定适合你的场景。例如我们尝试过 Kafka 来做订单异步处理,但由于消息延迟低要求高,最终还是用了 RabbitMQ 更合适。
3. 性能优化是一个持续的过程,不是一次工程
你不能指望第一次就把系统做到完美。我们需要不断压测、观察、分析,才能找到真正的瓶颈。
4. 系统设计要有取舍,能妥协才是成熟的体现
有时候为了快速上线,可能先牺牲一部分可维护性,但一定要留好升级路径。比如前期没做缓存,后期加缓存就会很麻烦。
5. 监控和报警比技术本身更重要
你不知道哪里会出问题,所以必须有一个完整的监控体系。当问题发生时,你能第一时间知道,而不是等到用户投诉。
结语:高并发系统没有银弹,只有不断打磨
回望这段经历,最大的收获并不是我们用上了多么牛的技术栈,而是学会了如何在复杂的业务场景中做出合理的权衡。
高并发系统设计从来都不是一蹴而就的事情,它需要你在每一个细节上下功夫,也考验你对系统整体掌控的能力。
希望这篇文章能给你带来一些启发,少走一点弯路。如果你也经历过类似的挑战,欢迎留言交流,一起成长!
如果你觉得这篇内容对你有帮助,不妨点个赞,转发一下,让更多的开发者看到~

评论 0