高并发系统设计:那些年我们踩过的坑和闯过的关

代码轻食主义
2025-06-20 08:33
阅读 313

作为一个在互联网公司工作的后端开发,我经常被卷入一场没有硝烟的战争——如何在有限的资源下支撑起几十万甚至上百万用户同时访问的业务场景。这听起来很酷,但只有真正干过的人才知道,背后藏着多少夜不能寐、抓耳挠腮的时刻。

今天我想以亲身经历为例,分享一个典型的高并发项目背景、遇到的问题,以及最终我们是如何解决这些问题的。这篇文章不仅会涉及一些技术选型、架构调整和性能优化的具体方法,也会谈谈我在实际工作中踩过的坑,以及从中学到的经验教训。

项目背景:一次秒杀活动的“灾难”

项目背景:一次秒杀活动的“灾难”

时间回到两年前,我所在的公司正在做一次大型线上促销活动,其中有一个核心模块是“限时秒杀商品”。按照预期,每场秒杀会有超过 50 万人参与抢购,高峰期间 QPS(每秒查询数)预计会超过 10 万。

当时的系统结构比较简单,基本是一个 Spring Boot + MySQL + Redis 的单体服务,部署在阿里云 ECS 上。我们第一次压测就发现问题很大:

  • 接口响应延迟严重,平均 RT 超过 3 秒
  • 数据库 CPU 长时间处于 95% 以上
  • 有大量数据库连接等待,请求失败率飙升
  • 某些节点出现 OOM(内存溢出)

最严重的是,一次预演上线的时候直接把数据库打挂了,导致整个服务瘫痪一小时。这对我们来说是个非常大的打击,也让我们意识到:如果再不做架构层面的调整,这种级别的并发量,根本撑不住。

遇到的问题与挑战

遇到的问题与挑战

1. 数据库瓶颈突出

MySQL 单点写入性能有限,尤其是在秒杀这种强一致性要求高的场景下,所有操作都必须写数据库。而并发一上来,数据库连接池被打满,事务堆积,锁等待现象频发。

2. 缓存穿透 & 击穿 & 雪崩同时存在

为了缓解数据库压力,我们一开始用了 Redis 做缓存。但没控制好缓存失效策略,结果大量请求在缓存失效的一瞬间涌入数据库,形成了击穿效应。

3. 系统缺乏限流降级机制

当流量突然暴涨时,系统没有自动熔断机制,导致部分接口阻塞整个线程池,从而引发雪崩效应。整个服务链路都在互相拖累,最终一起垮掉。

4. 日志和监控缺失,排查困难

当时我们的日志收集还停留在简单输出到本地文件阶段,面对海量请求时根本无法快速定位问题。也没有完整的监控体系,CPU、线程、QPS、慢查询等信息全靠手动查,效率极其低下。


解决方案:从架构升级到细节打磨

为了解决这些问题,我们开始了一场为期两周的技术攻坚。下面我会详细讲讲我们在每个维度所做的努力。

1. 架构分层改造:微服务化 + 异步解耦

我们将原来的单体应用拆分为多个服务单元,主要划分为:

  • 商品服务(读取库存)
  • 订单服务(下单、支付回调)
  • 库存服务(处理扣减逻辑)
  • 用户服务(校验限购条件)

通过 RPC 或者 MQ(Kafka)进行通信,实现异步消息解耦。

技术栈选型对比:

组件 使用前 使用后
架构风格 单体架构 微服务架构
通信方式 HTTP 内部调用 Dubbo + Kafka
存储层 单实例 MySQL MySQL 分库分表 + Redis 缓存集群

这样做的好处是:

  • 模块职责更清晰,便于维护
  • 单个服务崩溃不会波及整体
  • 可以按需扩容某一个热点服务

小插曲:刚开始拆服务那阵子,我们团队内部吵得不可开交,有人说:“拆服务不过是给运维增加负担”,有人反驳说“不拆连命都保不住”。最终还是老板拍板,逼着我们硬着头皮上了。事实证明,这个决定救了我们。

2. 数据库优化:读写分离 + 分库分表 + 缓存双写

我们在原有 MySQL 主库基础上引入了读写分离机制,并通过 MyCat 实现了水平分表(按用户 ID hash),将原来一张订单表拆成 8 张分表,极大提升了数据库的承载能力。

同时我们在 Redis 和 MySQL 之间做了缓存双写策略,使用本地 Guava 缓存 + Redis L1/L2 缓存结构,提升访问速度的同时也降低了数据库压力。

关键代码片段:

// 示例:缓存读取逻辑
public Product getProductFromCache(String productId) {
    Product product = localCache.getIfPresent(productId);
    if (product == null) {
        product = redisTemplate.opsForValue().get("product:" + productId);
        if (product != null) {
            localCache.put(productId, product); // 回种本地缓存
        }
    }
    return product;
}

此外,为了避免缓存雪崩,我们采用随机过期时间策略:

int expireTime = 60 * 60 + new Random().nextInt(300); // 1小时+随机偏移
redisTemplate.expire("product:" + productId, expireTime, TimeUnit.SECONDS);

3. 流控与降级:Sentinel 大显神威

我们引入了 Alibaba Sentinel 作为统一的流控组件,对关键路径接口设置 QPS 限流、线程隔离等策略。

配置示例:

rules:
  flow:
    - resource: /api/seckill
      limitApp: default
      grade: 1 # QPS
      count: 20000
      strategy: 0

结合 Spring Cloud Gateway,在网关层做了限流拦截:

@Configuration
public class GatewayConfig {
    
    @Bean
    public GlobalFilter sentinelGlobalFilter() {
        return new SentinelGatewayFilter();
    }
}

一旦某个服务响应变慢,我们就触发熔断机制,返回兜底数据或提示信息,不至于整条链路全部卡住。

4. 异步化处理:Kafka + RabbitMQ 双保险

对于非核心流程(如发送短信通知、记录日志等),我们采用 Kafka 进行异步投递;而对于需要一定可靠性的操作(如下单完成后的状态同步),使用 RabbitMQ 保证消息必达。

例如,下单完成后发布一个事件到 Kafka:

kafkaTemplate.send("order-created", JSON.toJSONString(order));

订单服务监听该 topic 并处理后续动作:

@KafkaListener(topics = "order-created")
public void handleOrderCreated(String message) {
    Order order = JSON.parseObject(message, Order.class);
    sendEmailNotification(order.getUserId());
}

5. 监控体系建设:Prometheus + Grafana + ELK

我们搭建了一套完善的监控体系:

  • 使用 Prometheus + Node Exporter 收集服务器指标
  • 用 Grafana 做可视化大盘展示 QPS、RT、CPU、内存等核心指标
  • ELK 做日志集中收集和分析,支持按 traceId 快速检索

这套体系帮助我们在后续几次压测中,迅速发现线程池打满、GC 频繁等问题。

感悟:监控真的不是锦上添花,而是雪中送炭。你永远不知道你的系统哪天会“生病”,而监控就是医生听诊器。


踩坑经验:那些年我们踩过的坑

❗坑一:Redis 缓存击穿造成数据库爆表

有一次秒杀结束后,缓存集体失效,大量请求瞬间涌向数据库,导致数据库连接超时、服务不可用。

解决方案:

  • 使用互斥锁 + 双检机制重建缓存:
if (!redis.exists(key)) {
    synchronized (this) {
        if (!redis.exists(key)) {
            Product product = loadFromDB(productId);
            redis.setex(key, 3600, product.toJson());
        }
    }
}
  • 更高级的做法是使用 Redisson 的 RLock 来实现分布式锁:
RLock lock = redisson.getLock("product-lock:" + productId);
if (lock.tryLock()) {
    try {
        // 重新加载数据并写回缓存
    } finally {
        lock.unlock();
    }
}

❗坑二:线程池配置不合理导致接口卡死

我们最初使用 Tomcat 默认线程池处理请求,结果在压测中发现部分接口长时间无响应,日志显示大量线程处于 WAITING 状态。

后来我们改为自定义线程池,区分同步/异步任务类型,并设定了拒绝策略:

@Bean("seckillExecutor")
public ExecutorService seckillExecutor() {
    int corePoolSize = 50;
    int maxPoolSize = 100;
    BlockingQueue<Runnable> workQueue = new LinkedBlockingDeque<>(2000);
    RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 由调用方处理

    return new ThreadPoolExecutor(
        corePoolSize, maxPoolSize, 
        60L, TimeUnit.SECONDS, 
        workQueue, 
        new ThreadPoolExecutor.AbortPolicy()
    );
}

这个改进让系统的容错性大幅提升。


效果总结:从事故不断到稳如老狗

改造完成后,我们又进行了三次大规模压测,最终达到了以下效果:

指标 改造前 改造后
最大 QPS 12000 105000
平均响应时间 >3s <300ms
系统可用率 不足 99% 达到 99.99%
故障恢复时间 数小时 <10分钟
成功率 ≈80% >99.5%

最关键的是,在真正的生产环境中,我们成功扛住了多轮百万级别并发请求,零宕机、零事故。


经验分享:给想做好高并发系统的兄弟们几点建议

  1. 别怕重构,早重构比晚重构好。
    面对复杂场景一定要敢于对现有架构动刀子,不要等到出了事才想起拆服务。

  2. 高并发本质是风险控制的艺术。
    你要考虑哪里可能会慢、哪里可能出错、哪里可能被打垮。提前埋好“逃生通道”——比如限流、降级、兜底。

  3. 细节决定成败。
    像缓存穿透、线程池配置这些看似小问题,往往决定了你能否抗住压力。别光看理论,要动手验证。

  4. 要有敬畏之心。
    高并发系统没有银弹,每个方案都有代价,要根据业务特性做权衡。有时候不是越先进越好,而是越合适越好。

  5. 持续演进是王道。
    我们现在也在尝试往 Service Mesh 方向演进,未来还会接入更多云原生能力。技术没有终点,只有不断前行。


写在最后:技术是一场修行

回想起那段熬夜改代码、半夜跑压测的日子,虽然很苦,但也让我真正理解了一个“稳定、高性能”的系统背后,到底意味着什么。

高并发系统设计不是一个简单的技术选型问题,它考验的是开发者对业务的理解、对架构的认知、以及面对压力时的冷静判断力。

如果你正走在构建高并发系统的路上,或者正面临类似的挑战,希望这篇文章能给你带来启发和信心。

毕竟,我们都曾经历过那个“系统挂了,心也跟着凉了”的深夜。但只要坚持下来,你会感谢那个咬牙前行的自己。


📌 本文源码已整理成 GitHub 示例工程,欢迎 star 和交流。 仓库地址:github.com/example/high-concurrency-case

如有疑问或讨论,欢迎留言或私信,我在一线搬砖,也在一线成长。

评论 0

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