高并发系统设计:从一次线上事故说开去

闪电鸟
2025-06-14 12:23
阅读 438

引言:为什么我们要重视高并发?

引言:为什么我们要重视高并发?

作为一个从业多年的后端开发工程师,我参与过不少项目,从传统行业的小系统到日均上百万访问量的互联网产品。而真正让我对“高并发”这个词有了深刻认识的,是一次令人印象深刻的生产事故。

那次事故发生在我们团队主导的一个社交电商项目上线不久之后。原本预计首日UV大概在10万左右,结果因为一个短视频突然在抖音火了,当天的访问量直接飙升到了200万+,而且是集中在几个小时内的突增流量。我们的服务瞬间打满,数据库连接池爆掉,Redis连接超时,消息队列堆积如山,最终导致整个平台瘫痪,用户无法下单、登录、甚至首页都打不开。

这次事故之后,我们开始重新审视系统的整体架构和性能瓶颈,并逐步重构了一个具备抗高并发能力的新系统。这篇文章就是基于这个真实项目的经历来写的,我会从问题出发,结合实战经验,聊聊怎么设计和优化一个真正的高并发系统。


问题描述:高并发下的系统表现有多脆弱?

问题描述:高并发下的系统表现有多脆弱?

回到那次事故,当时的系统架构大致如下:

  • 前端Vue单页应用
  • 后端Spring Boot提供REST接口
  • MySQL主从架构 + Redis缓存
  • RabbitMQ做异步任务分发
  • Nginx作为反向代理

看起来配置不算太差,但面对突发的200万PV,整个系统几乎瞬间崩溃。原因如下:

  1. Nginx层面

    • 没有做限流,所有的请求都被放进来;
    • 短时间内大量请求导致上游服务负载激增。
  2. Java应用层

    • Spring Boot默认线程池数量有限;
    • 接口未加缓存或降级策略,所有请求都直击数据库。
  3. 数据库层

    • 主库读写混合,压力过大;
    • 查询未走索引;
    • 最大连接数被撑爆(max_connections=100)。
  4. 缓存层

    • Redis连接池设置不合理,没有连接复用;
    • 缓存穿透场景未做处理;
    • 热点数据未能有效预热。
  5. 异步处理失败

    • 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;
        }
    }
}

二、缓存打底 —— 多级缓存体系构建

为了避免每次请求都访问数据库,我们构建了三级缓存:

  1. 浏览器本地缓存
  2. Nginx 层缓存(静态资源)
  3. Redis + 本地 Caffeine 双缓存

以商品详情为例,缓存加载顺序如下:

  1. 先查本地内存缓存(Caffeine),命中则返回;
  2. 未命中,则查 Redis;
  3. Redis 也没有命中,则查数据库并更新两级缓存;
  4. 更新完 Redis 后再异步刷新本地缓存(比如通过 Kafka 或定时任务)。

这样既减少了网络 IO,又缓解了缓存雪崩风险。

三、数据库优化 —— 分表分库 + 读写分离

原来的 MySQL 是一个主库扛所有请求,后来我们做了以下调整:

  1. 垂直分库:把订单、用户、商品等模块拆成单独的数据库;
  2. 水平分表:使用 ShardingSphere 对订单表按时间进行分片;
  3. 读写分离:通过 MyCat 做主从切换,提升查询能力;
  4. 索引优化:针对慢查询做 Explain 分析,增加合适索引;
  5. 连接池优化:将 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”的记录机制,在插入数据库前先判断是否已处理过这条消息。


效果总结:重构后的系统到底怎么样了?

微服务架构示意图-1

这次重构完成后,我们又迎来了一次大规模活动促销。这次效果非常显著:

指标 之前 之后
QPS峰值 <500 >12,000
数据库连接数 经常达到上限 稳定在 100 左右
接口平均响应时间 1s+ <150ms
服务可用性 崩溃频繁 99.8%以上

更重要的是,即使在极端情况(比如某个接口被打爆)下,系统也能保持基本可用,不再出现全局崩溃的情况。


经验分享:给正在路上的你几点建议

缓存策略对比-2

如果你也在做高并发系统,或者准备搭建这样的系统,下面几点是我个人的经验总结:

1. 永远不要相信“一切正常”的假象

线上环境远比你想象中复杂,各种异常都有可能出现,要时刻做好兜底预案。

2. 技术选型要看适用性和成熟度

有时候新技术听起来很厉害,但不一定适合你的场景。例如我们尝试过 Kafka 来做订单异步处理,但由于消息延迟低要求高,最终还是用了 RabbitMQ 更合适。

3. 性能优化是一个持续的过程,不是一次工程

你不能指望第一次就把系统做到完美。我们需要不断压测、观察、分析,才能找到真正的瓶颈。

4. 系统设计要有取舍,能妥协才是成熟的体现

有时候为了快速上线,可能先牺牲一部分可维护性,但一定要留好升级路径。比如前期没做缓存,后期加缓存就会很麻烦。

5. 监控和报警比技术本身更重要

你不知道哪里会出问题,所以必须有一个完整的监控体系。当问题发生时,你能第一时间知道,而不是等到用户投诉。


结语:高并发系统没有银弹,只有不断打磨

回望这段经历,最大的收获并不是我们用上了多么牛的技术栈,而是学会了如何在复杂的业务场景中做出合理的权衡。

高并发系统设计从来都不是一蹴而就的事情,它需要你在每一个细节上下功夫,也考验你对系统整体掌控的能力。

希望这篇文章能给你带来一些启发,少走一点弯路。如果你也经历过类似的挑战,欢迎留言交流,一起成长!


如果你觉得这篇内容对你有帮助,不妨点个赞,转发一下,让更多的开发者看到~

评论 0

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