高并发系统设计:从踩坑到从容,我用实战告诉你怎么走
背景介绍:为什么我要写这篇文章

2019年的时候,我在一家做在线教育的公司负责后端系统的架构优化。当时我们正准备上线一个“名师直播课”项目,宣传力度不小,用户数量短时间内暴涨,系统也迎来了第一次真正意义上的大考。
这个项目的核心是直播课堂,老师直播时支持万人同时在线互动,包括提问、打赏、答题等操作。听起来很炫酷,但现实远比预期残酷——上线第一天晚上就因为高并发导致服务器雪崩,数据库直接挂掉,几十个用户投诉,运营团队连夜开会……
那次事故让我深刻意识到:高并发不仅仅是技术问题,更是对系统设计、协作流程甚至心理承受能力的一次全方位考验。从那之后,我开始系统地学习高并发系统的构建和优化方法,不断踩坑、总结,在多个项目中反复打磨这套经验。
今天,我就结合自己的经历,分享一下如何设计高并发系统,特别是在真实项目中的挑战和解决思路。
问题描述:一次崩溃引发的深度思考

上面提到的那个直播课项目,最开始我们并没有预料到它会成为爆款。初期的设计方案比较常规:
- 使用 Spring Boot + MySQL 架构
- Redis 做缓存
- 所有接口部署在单台 Nginx 反向代理下面
- 消息队列使用 RabbitMQ 做异步处理
一切看着都没问题,直到第一场大型直播开播当天,用户量瞬间突破5000人,接着:
- 接口响应时间飙升,大量请求超时
- 数据库 CPU 占用率接近100%
- Nginx 出现 connection reset 错误
- 整体服务不可用超过10分钟
那次故障暴露了几个核心问题:
- 连接池配置不合理:HikariCP 默认最大连接数太小,数据库被打爆
- 同步调用过多:大量的日志写入、消息广播都在主线程里完成,拖慢主线程
- Redis 缓存击穿:未设置热点穿透保护,导致数据库压力剧增
- 缺乏限流降级机制:系统不具备自我保护能力,一压就瘫
这些问题背后,其实都指向一个更本质的问题:我们在设计系统时,并没有真正理解“高并发”意味着什么。
解决方案:一步步构建可扛住流量洪峰的系统架构
为了从根本上解决问题,我们用了近两个月的时间进行系统重构。以下是关键改造点和选型决策,以及我在其中踩过的坑。
1. 架构分层与模块解耦
我们重新梳理了业务逻辑,将系统拆分为以下几个模块:
- 网关层(Gateway):负责鉴权、限流、路由
- 业务逻辑层(Service Layer):处理具体业务逻辑,如送礼物、答题记录等
- 数据聚合层(Data Layer):整合 DB、缓存、ES 等数据源
- 任务中心(Job Center):处理后台任务,如统计、日志归档等
- 消息推送层(Message Push):负责 WebSocket 和实时消息通知
通过模块化拆分,我们可以单独扩展某些模块,而不会影响其他部分。
2. 引入限流与熔断机制
我们采用了 Sentinel 作为限流组件,结合 OpenFeign 做服务间调用的熔断控制。这部分配置简单,但真正难点在于阈值的设置。
比如:某个查询接口 QPS 设为多少合适?
答案是:需要压测!我们后来做了基准测试,根据平均响应时间和线程池容量反推 QPS 限制。最终设定规则如下:
resource: get_course_info
threshold: 800
grade: 1 # 基于QPS限流
controlBehavior: 2 # 控制行为为匀速排队
这一步看似简单,但在实际部署时容易忽视线程阻塞造成的连锁反应。一定要做好链路压测!
3. Redis 缓存策略升级
原来的缓存只是加了一层 key-value,结果遇到大量相同请求访问同一个课程信息时,仍会穿透缓存打到数据库。
我们增加了以下措施:
- 空值缓存(Null Caching)防止恶意攻击
- 热点标记 + 自动降级机制
- 多级缓存设计(LocalCache + Redis)
- 熔断策略下自动切换读DB通道
举个例子,我们在 Java 中加入了一个本地缓存实现:
// 使用 Caffeine 做本地缓存
Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
Object data = localCache.getIfPresent(key);
if (data == null) {
data = redisTemplate.opsForValue().get(key); // 远程获取
if (data != null) {
localCache.put(key, data);
}
}
这一改动显著减少了对 Redis 的高频访问压力。
4. 数据库优化与水平扩展
MySQL 在并发高时很容易成为瓶颈,我们做了以下调整:
- 拆分主库与从库,读写分离
- 查询走只读副本,更新走主库
- 分表策略引入(按用户 ID 哈希分片)
另外,我们还引入了 MyCat 中间件做透明分库分表,虽然 MyCat 后来被 ShardingSphere 替代,但在当时的场景下是合适的选型。
有个插曲:分表后我们遇到了一些诡异的 Join 查询错误,最后发现是中间件不支持跨库 Join 导致的。因此建议:能不做复杂 Join 就不要做,能提前预处理就在业务层搞定。
5. WebSockets 与长连接管理
直播间的聊天、弹幕、礼物都需要实时推送,我们一开始用了普通的 WebSocket,但随着人数增长,内存占用越来越高,服务器频繁 Full GC。
后来我们改用 Netty + Redis 发布订阅的方式处理消息推送。Netty 接管长连接管理,收到用户发送的消息后,通过 Redis Pub/Sub 广播到其他节点,其他节点再推送给对应的连接客户端。
代码示例:
@ServerEndpoint("/chat/{userId}")
public class ChatEndpoint {
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
SessionManager.addSession(userId, session);
}
@OnMessage
public void onMessage(String message, Session session) {
// 通过 Redis 发送到其他节点
redisPublisher.publish("chat_channel", message);
}
}
这样做的好处是:
- 每个节点只需管理自己连接的用户
- 消息广播由 Redis 统一处理,避免重复转发
- 易于横向扩展,新增节点不影响已有服务
不过也有缺点:消息可能会重复消费,需要引入幂等性处理机制。
踩坑经验:那些你没踩过就不知道的事儿
讲完了整体的技术方案,这里再聊几个特别典型的“深坑”,希望你在开发过程中避过这些雷。
✅ 陷阱一:数据库事务死锁
我们曾经在送礼物逻辑中出现大量事务卡死的情况。分析发现是因为多个用户同时送礼给同一个主播,导致行锁竞争激烈。
解决方案是:
- 尽可能缩小事务粒度
- 加锁顺序一致(如先锁定主播ID再锁定用户ID)
- 对热点资源加分布式锁(Redis Redlock 实现)
例如:
String lockKey = "gift_lock:" + anchorId;
try {
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS);
if (isLocked != null && isLocked) {
// 执行送礼逻辑
}
} finally {
redisTemplate.delete(lockKey);
}
别图省事裸写事务,特别是涉及多人协作修改的数据表,必须小心加锁!
✅ 陷阱二:线程池拒绝策略不当
我们的线程池原本使用 CallerRunsPolicy,即调用者执行任务。当高峰期请求量太大时,反而把主线程拖死,最终造成服务不可用。
后来我们换成 AbortPolicy,并配合 Sentinel 提前熔断请求:
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
记住一点:线程池拒绝策略要跟限流配合使用,不能孤立看待。
✅ 陷阱三:Nginx 配置失误导致连接耗尽
我们最初在前端加了一层 Nginx,但是连接数一直上不去,后来发现是因为 keepalive 设置不对:
upstream backend {
least_conn;
server 10.0.0.1:8080 weight=2;
server 10.0.0.2:8080;
keepalive 64; # 关键参数
}
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection ''; # 记得清空连接头
}
加上这两项后,连接复用大幅提升,服务器负载明显下降。
效果总结:优化后的变化
经过一系列优化和重构,系统在后续的大规模活动(双十一、暑假招生季)中表现稳定:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 接口平均响应时间 | 500ms+ | <100ms |
| 最大并发支撑能力 | 2k~3k | >1w |
| 系统可用性 | ~97% | >99.5% |
| DB CPU 使用率 | 经常打满 | 最高60%左右 |
| 投诉量 | 数十起 | 个位数 |
最重要的是,我们终于有了信心去承接更大的流量,而不是提心吊胆地等待下一波用户涌入。
经验分享:写给正在战斗的你
如果你现在也在做类似的高并发项目,或者即将接手类似需求,我想给你几个真心建议:

🧱 1. 系统架构不是一成不变的
刚开始可以“简单但清晰”,比如分层设计+基础缓存+基本限流。关键是让结构清晰、边界明确,后期才有足够的扩展空间。
⚡ 2. 性能优化是一个持续过程
不要指望“一次性设计好”,而是要在不同阶段逐步迭代。性能测试、压测、监控、告警都是必备工具。
🛑 3. 不要迷信任何“万能方案”
Sentinel 好用,但不一定适合所有限流场景;Redis 很快,但并不是所有数据都要缓存;Netty 很牛,但也未必适合你的消息推送模型。
💬 4. 和产品、测试保持良好沟通很重要
很多时候,高并发的问题不是技术不行,而是需求不清、节奏混乱造成的。比如一个按钮点击多次,是不是可以前端防抖?比如一个功能非得强一致性?还是可以容忍一点点延迟?
🧪 5. 多做模拟演练,别等到线上出问题才想起预案
我现在的习惯是在每个新项目上线前都会做三个动作:
- 压测(JMeter 或 Gatling)
- 故障演练(Kill Pod、网络分区、CPU 占满)
- 全链路追踪(SkyWalking 或 Zipkin)
这些准备,才是真正保障系统稳定的底气。
结语:高并发没有捷径,只有不断积累的经验

回首这几年,从最初的懵懂到现在能独立设计出一套完整的高并发方案,我深刻体会到一句话:“高并发不是拼凑技术栈,而是对系统设计、工程思维、协作能力的整体考验。”
每一个经历过“线上崩溃”的开发者,都有属于自己的成长故事。愿你在未来的工作中,少些焦虑,多些从容。
如果你也在高并发的路上摸爬滚打,欢迎一起交流,互相取暖。毕竟,这条路,从来都不是一个人的战斗。
文末彩蛋:本文中提及的很多代码片段和配置都可以在 GitHub 上找到完整 demo,关注我的账号 [YourGitHubName],回复【高并发】即可获取相关示例仓库地址。

评论 0