高并发系统设计:从理论到实践

AI产品手记
2025-06-27 14:05
阅读 745

开篇:为什么我会去思考这个问题?

开篇:为什么我会去思考这个问题?

我在一家做内容推荐的互联网公司工作,主要负责后端架构和核心服务的设计与维护。这几年间,我亲身经历了一个从日活几万到千万级流量系统的演进过程。在这个过程中,高并发成了我们最常挂在嘴边的问题——不是因为喜欢它,而是因为它几乎每一步都在“找事”。

记得最早接手的一个项目,是一个用户行为埋点上报接口。当时团队觉得这不就是个 POST 接口嘛,能有多难?结果刚上线一天,就被打爆了。数据库连接池满了、消息队列堆积、服务器 CPU 疯狂飙升……那真是一次血泪教训。

从此以后,我对“高并发”这个词就格外敏感,也开始有意识地在设计阶段就把性能、容量、扩展性这些因素考虑进去。这篇文章我希望通过几个真实案例,结合我的经验和踩过的坑,带你一起走过一次从需求到落地的高并发系统设计之路。


问题描述:那些年我遇到的并发挑战

问题描述:那些年我遇到的并发挑战

我们来看看几个典型的业务场景:

场景一:实时推荐系统

我们的主站首页采用的是千人千面的推荐策略,每个用户访问首页时,后端会调用一个推荐接口生成10条推荐内容。这个接口本身依赖于多个下游数据源(比如用户画像、商品信息、召回服务等),而且要求响应时间控制在300ms以内。

随着用户量的增长,接口TP99逐渐从250ms上涨到了480ms以上,用户开始出现流失迹象,产品经理天天跑来催:“推荐慢了一秒,DAU掉了好几千!”

场景二:活动报名抢购

在一次促销活动中,我们需要提供一个限时抢购接口。用户点击“立即抢购”按钮后,需要完成库存扣减、订单创建、优惠券发放等多个操作。刚开始预估并发量是1W QPS左右,实际开抢那一刻直接飙到了3W+,MySQL锁等待严重,很多请求超时,最后客服电话被打爆……

场景三:数据上报接口

我们在App中嵌入了大量的埋点数据采集代码,用户操作行为会实时上报到服务端。原本是直接写数据库的逻辑,在百万级请求下导致数据库连接池耗尽,进而引发整个服务崩溃。

这三个场景虽然不同,但背后都藏着相似的技术挑战:

  • 高QPS带来的压力
  • 长尾请求拖垮整体响应
  • 资源竞争导致的瓶颈
  • 突发流量难以预料

面对这些问题,单纯靠堆机器已经无法满足需求。我们必须从系统设计、架构分层、缓存策略、异步处理、数据库优化等多个角度去统筹规划。


解决方案:一步步打造高并发能力

解决方案:一步步打造高并发能力

下面我以“实时推荐系统”为例,详细讲讲我们是如何从零开始优化这个系统的。

第一步:性能分析,找到瓶颈所在

首先我们要明确系统性能瓶颈在哪。我们使用了如下几种手段进行排查:

  • 监控告警系统:观察QPS、RT、错误率
  • 链路追踪工具(SkyWalking):定位接口各个子调用耗时
  • JVM监控(Prometheus + Grafana):看GC、线程、内存情况

最终发现,推荐接口的主要延迟集中在以下几个环节:

  1. 用户画像服务调用较慢(平均250ms)
  2. 多个数据源并行拉取合并效率低
  3. 数据组装和排序逻辑复杂且同步执行
  4. Redis穿透导致部分热点查询落到DB

第二步:拆解模块,提升并行度

为了解决这些问题,我们做了几项关键改造:

1. 抽象公共画像服务为独立组件

之前所有需要用户标签的服务都是直连画像服务,导致画像服务压力巨大,成为单点瓶颈。于是我们将画像服务抽象成统一网关,加了一层LRU本地缓存 + Redis二级缓存,并做多级降级机制。

2. 使用CompletableFuture实现多数据源并行获取

Java原生的CompletableFuture非常适合这种需要并行调用多个服务的场景。我们通过封装,让开发者可以用非常直观的方式组织多个异步任务,并指定超时时间、失败重试策略。

CompletableFuture<UserProfile> profileFuture = CompletableFuture.supplyAsync(this::fetchUserProfile);
CompletableFuture<List<Product>> productFuture = CompletableFuture.supplyAsync(this::fetchProducts);

// 合并两个future结果
profileFuture.thenCombine(productFuture, (profile, products) -> {
    // 组装推荐结果
    return recommend(profile, products);
});

3. 引入Caffeine缓存减少冗余计算

推荐算法中有大量重复计算,特别是对热门用户和商品组合。我们在服务侧引入Caffeine缓存,设置合理的过期时间和大小,大大减少了计算量。

LoadingCache<Key, List<Recommendation>> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .build(key -> computeRecommendations(key));

4. 异步化处理非核心流程

一些不影响当前推荐结果的数据收集任务,例如曝光埋点记录、推荐命中路径分析等,我们改用Kafka进行异步落盘处理,避免阻塞主线程。

// 异步发送曝光数据
kafkaProducer.send(new ProducerRecord<>("exposure", exposureData));

踩坑经验:这些坑我替你踩过了

踩坑经验:这些坑我替你踩过了

高并发系统从来不是一蹴而就的,中间我们也踩了不少坑,这里分享几个典型例子:

坑点1:Redis雪崩 + 缓存击穿 + 穿透

我们一开始用了简单的Redis缓存机制,结果某个深夜凌晨两点,突然报警说数据库连接数暴涨。查下来发现是因为大批热点数据同时过期,导致大量请求直接打到数据库。我们后来加上了随机过期时间,并引入布隆过滤器防止非法请求穿透到底层。

解决方案示例:

String key = "user_profile:1001";
String cacheValue = redis.get(key);
if (cacheValue == null) {
    if (bloomFilter.contains(key)) {
        // 可能存在,走数据库查询
        String dbValue = queryFromDatabase();
        int expireTime = 60 * 5 + new Random().nextInt(60);  // 加上随机过期时间
        redis.setex(key, expireTime, dbValue);
        return dbValue;
    } else {
        // 布隆过滤器判断不存在,直接返回空
        return null;
    }
} else {
    return cacheValue;
}

坑点2:线程池配置不合理

服务器部署方案-2

为了提高并发,我们给很多服务调用配上了自定义线程池。但有一回线上接口大面积超时,才发现线程池默认是无界队列,导致内存爆掉。后来我们全部换成有界的队列,并设置了合适的拒绝策略。

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(40);
executor.setQueueCapacity(200);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();

缓存策略对比-1

坑点3:跨数据中心调用未隔离

我们有一个服务部署在北京机房,另一个在上海机房。当北京机房网络波动时,大量请求被挂起,导致上海服务也被拖死。解决办法是在调用时加熔断机制(我们用了Sentinel),并在服务发现层面做同城优先路由。


效果总结:优化之后的变化

经过这一系列优化之后,我们的推荐接口表现有了显著提升:

指标 优化前 优化后
平均响应时间 480ms 220ms
TP99 780ms 310ms
错误率 3%~5% <0.1%
单实例QPS 1200 3500

不仅提升了用户体验,还帮产品部门争取到了更多的投放空间。最重要的是,稳定性明显增强,夜里的报警频率也大幅下降。


经验分享:我想告诉你的几点建议

经历过这么多次“修修补补”的优化,我也积累了一些心得,希望能给你带来启发。

1. 性能永远要在设计阶段就开始考虑

很多人习惯先完成功能再优化性能,但在高并发系统里,这一点往往代价很高。一定要在初期就想清楚服务的负载预期、数据流向、热点处理等问题。

2. 不要过度迷信分布式

很多人一上来就要搞微服务、Docker、K8s,其实很多时候,单体服务也能扛住很大的并发,特别是在业务初期。把精力放在真正的瓶颈上才是正道。

3. 做好“防御式”编程

对于每一个对外暴露的接口,都要考虑:

  • 限流:别让外部流量把内部压垮
  • 降级:出了问题至少能返回基本数据
  • 熔断:不要连锁反应,一个服务崩影响整体

4. 学会在监控中“听风辨位”

一个好的监控系统可以让我们提前感知风险。我们自己搭建了一整套Prometheus+Grafana+AlertManager的体系,每天早上看一眼就知道昨天有没有异常,哪些接口需要进一步优化。

5. 技术方案也要权衡成本和收益

有时候一个Redis缓存就能搞定的事情,何必用ElasticSearch?有时候一个定时补偿就能解决的问题,非要搞得那么复杂吗?记住一句话:越简单越好,能不用就不上。


结语:高并发不是终点,而是起点

高并发系统的设计,从来都不是一件轻松的事。它需要你有足够的技术视野,也需要你在细节处下功夫。但我始终相信,只有真正经历过线上问题的人,才会理解什么叫“稳如老狗”。

希望这篇文章能帮你少走点弯路,也能让你在面试或工作中说出一句“哦,这个问题我以前做过,我们可以这样处理”。

如果你正在经历类似的挑战,欢迎留言交流。说不定哪天我们就合作在一起优化下一个高并发项目了呢 😎。


附录:推荐阅读 & 工具清单

  • 《高性能MySQL》 - Baron Schwartz 等著
  • 《Reactive Design Patterns》 - Jonas Bonér 著
  • SkyWalking(链路追踪)
  • Prometheus + Grafana(监控)
  • Sentinel(流量控制)
  • Caffeine / Redis(缓存)
  • Kafka / RocketMQ(消息队列)

如需完整代码示例或者想要具体某一部分的深入探讨,也可以随时联系我。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝