高并发系统设计:从理论到实践——一个Spark老油条的血泪复盘
大家好,我是阿哲,坐标深圳南山科技园,混迹于某腾讯系大厂,做了三年大数据开发,日常主要和 Spark、Kafka、Flink 打交道。你可能会问:一个搞大数据的,怎么突然写起高并发系统设计了?
这事还得从去年双11说起。
我们团队本来负责的是离线数仓和实时计算管道,但去年老板拍板要做一个“秒杀活动实时监控大盘”——要求支持每秒10万+的事件写入,并在500ms内完成聚合展示。听起来是不是很熟悉?没错,这活儿本该后端兄弟扛,但产品经理甩锅说:“你们不是天天处理海量数据吗?这点并发算啥?”
于是,我这个只会写 Scala 的 Spark 工程师,被迫拿起了 Java,踏上了高并发系统的“代码人生”之路。
一、现实狠狠打了我的脸
刚开始我还挺自信:不就是高并发嘛,多线程、连接池、缓存,背过八股文谁不会?结果第一版上线当天晚上,测试同学压测到 3w QPS 时,系统直接 OOM,CPU 爆满,日志里全是 java.lang.OutOfMemoryError: GC overhead limit exceeded。
当时坐在工位上,耳机里放着 Lo-fi Beats(我写代码必备),看着监控面板一片红,真的想砸电脑。
更尴尬的是,第二天晨会,运维大哥幽幽来一句:“你们这服务把 Redis cluster 的某个分片打满了,其他业务都受影响了。”
我:😅
痛定思痛,我意识到——高并发不是堆技术名词,而是对资源的极致调度。
二、重新理解“资源”:CPU、内存、IO、网络,一个都不能少
以前写 Spark 作业,关注的是 executor 内存、shuffle 分区数、GC 调优。但做在线高并发服务,资源维度更细:
- CPU:线程上下文切换成本、锁竞争
- 内存:对象生命周期、缓存大小、GC 频率
- IO:数据库连接、磁盘写、网络往返
- 网络:TCP 连接数、带宽、延迟
我们的核心链路是:用户点击 → HTTP 请求 → 写入 Kafka → 实时计算 → 写入 Redis → 前端拉取。
瓶颈居然出在最前面的 HTTP 入口层!用 Spring Boot 写的 Controller,每个请求新建线程,线程池配置还是默认的 corePoolSize=8,根本扛不住。
解法1:异步非阻塞 + 合理线程模型
改用 CompletableFuture + 自定义线程池,关键代码如下:
@RestController
public class EventController {
private final ExecutorService ioExecutor = new ThreadPoolExecutor(
50, 200, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("event-io-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略很重要!
);
@PostMapping("/event")
public CompletableFuture<ResponseEntity<String>> handleEvent(@RequestBody Event event) {
return CompletableFuture.supplyAsync(() -> {
// 异步写 Kafka
kafkaProducer.send(event);
return ResponseEntity.ok("ok");
}, ioExecutor);
}
}
注:这里特意用了
CallerRunsPolicy,当队列满时由调用线程自己执行,避免丢请求(虽然会慢一点,但比 500 强)。
三、数据库不是垃圾桶,别乱塞!
早期为了快,我们直接把原始事件写 MySQL,结果主库 CPU 100%,binlog 堆积如山。DBA 在群里@我:“再这么搞,明天就停你权限。”
后来改用 分层写入策略:
| 层级 | 存储 | 用途 | TTL |
|---|---|---|---|
| L1 | Kafka | 原始事件缓冲 | 7天 |
| L2 | Redis (Hash + SortedSet) | 实时聚合指标 | 24h |
| L3 | ClickHouse | 离线分析 | 永久 |
Redis 设计细节:
- 用
HINCRBY统计 UV(用户ID去重用 HyperLogLog) - 用
ZADD+ZREVRANGE做 TopN 排行榜 - Key 按小时分片:
event:2023111114:user_click
这样,MySQL 彻底解放,只用于存储元数据(比如活动配置),不再承受写压力。
四、限流、熔断、降级:保命三件套
高并发场景下,系统稳定比功能完整更重要。我们引入了 Sentinel(阿里开源的流量控制组件),配置如下:
# sentinel规则(动态加载)
flow:
- resource: /event
count: 8000 # QPS 阈值
grade: 1 # QPS 模式
controlBehavior: 0 # 快速失败
当流量突增,超过 8k QPS 时,直接返回 {"code": 503, "msg": "too many requests"},前端做友好提示。虽然用户体验打点折,但系统没崩——这才是真正的“优雅”。
另外,我们还做了柔性降级:
- 非核心字段(如用户设备信息)在高压下可丢弃
- 实时排行榜延迟 1s 更新(用本地缓存暂存)
五、面试题 vs 真实生产:别被八股文骗了
很多人准备高并发面试题,背得滚瓜烂熟:“用 Redis 做缓存穿透防护”、“用消息队列削峰”。但真实场景远比这复杂。
举个例子:缓存雪崩。
面试答案通常是“加随机过期时间”。但在生产环境,我们遇到的是:大量 Key 同时失效,导致 DB 瞬间被打爆。
我们的解法是:
- 热点 Key 永不过期(后台定时刷新)
- 缓存空值(防止恶意刷不存在的 ID)
- 二级缓存:本地 Caffeine + Redis
LoadingCache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> redisClient.get(key)); // 本地缓存未命中才查 Redis
这种组合拳,才是真实世界的防御。
六、性能对比:优化前后数据说话
经过三轮迭代,系统终于扛住了双11峰值。以下是关键指标对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 最大 QPS | 12,000 | 120,000 | 10x |
| P99 延迟 | 1800ms | 320ms | ↓82% |
| 内存占用 | 8GB | 3.5GB | ↓56% |
| GC 频率 | 15次/分钟 | 2次/分钟 | ↓87% |
最爽的是,双11当晚我在公司躺平刷 B站,系统稳如老狗。隔壁组还在救火,我默默点了杯瑞幸,心想:代码人生,不过如此。
七、给同行的几点真心话
- 不要迷信“高并发架构图”:很多文章画一堆微服务、网关、注册中心,但你的业务可能只需要一个合理的线程池 + 缓存策略。
- 压测要贴近真实:用 JMeter 模拟 10w 并发没用,要看请求分布、参数多样性、网络抖动。
- 可观测性 > 一切:日志、Metrics、Trace 缺一不可。我们靠 Arthas + Prometheus + SkyWalking 定位了 80% 的问题。
- Java 依然是高并发王者:别听风就是雨说 Go 多快。JVM 经过这么多年优化,配合 Netty、Disruptor,性能完全够用。
结语:从 Spark 到高并发,我的成长
说实话,这次跨界让我重新理解了“系统设计”——它不是炫技,而是在资源受限下,用工程思维做最优权衡。
现在回看那段边听音乐边 debug 的日子,虽然苦,但值得。毕竟,能扛住线上流量冲击的代码,才是有生命力的代码。
如果你也在深圳,欢迎约 coffee 聊技术(或者一起吐槽产品经理)。最后送大家一句我工位贴的座右铭:
“高并发不是目标,稳定交付才是。”
—— 一个不想再背八股文的大数据工程师,阿哲

评论 0