高并发系统设计:一次真实的“扛住百万请求”的旅程

马超·
2025-06-27 16:35
阅读 493

开篇:为什么我会想写这篇技术文章?

开篇:为什么我会想写这篇技术文章?

记得几年前,我刚加入一家快速发展的电商平台公司。当时我们正在筹备一年一度的“双十一大促”,作为一个后端架构师的新手,我第一次真正感受到高并发系统的威力。

那天晚上,我们在压测环境模拟每秒10万次请求时,数据库直接崩溃了三次。Redis被打满,接口延迟飙升到1.5秒以上,服务之间频繁超时,整个系统像一锅煮沸的开水——热闹但失控。

这场灾难让我深刻认识到:纸上谈兵的架构永远撑不起真正的流量洪峰,只有在实践中不断试错、打磨,才能构建出可靠的高并发系统

所以今天我想和你分享这段真实经历,讲一讲我是怎么从一个只会用Spring Boot搭个简单API的开发,一步步设计出能够承载百万级QPS的系统的全过程。


问题描述:百万级别的用户请求,系统随时可能崩盘

问题描述:百万级别的用户请求,系统随时可能崩盘

我们当时负责的是用户登录和订单提交两个核心链路。这两部分在大促当天会集中爆发访问压力:

  • 登录模块:单点登录(SSO),用户访问量集中在开售前30分钟
  • 订单模块:抢购开始后的前几分钟内,会有大量用户同时下单

当时的系统存在几个严重问题:

1. 数据库瓶颈严重

  • 所有数据都直连MySQL主库,没有读写分离
  • 用户信息查询全表扫描,执行计划糟糕
  • 没有做连接池管理,应用频繁创建数据库连接

2. 系统调用链过长

  • 多层业务校验和服务调用串行化
  • 超时时间设置混乱,导致雪崩效应
  • 缓存命中率不到40%,大多数请求穿透到了数据库

3. 容灾机制基本缺失

  • 没有服务降级策略,出现异常直接抛错误给前端
  • 单节点部署,某台服务器宕机直接影响全局可用性
  • 没有监控告警,只能靠人工发现故障

那段时间,每天都在改代码、调参数、看日志,团队加班成常态。最崩溃的一次是预热期间数据库主从同步延迟高达20分钟,导致用户看到的数据严重不一致,投诉电话差点打爆客服。


解决方案:从架构调整到细节优化的全流程改造

面对这些挑战,我们采取了一系列技术和架构层面的措施。下面我就按照实际落地顺序来聊聊。

1. 架构拆分与服务化改造

第一步是对系统进行解耦和模块化:

旧结构:
Client -> Gateway -> OrderService + AuthService -> MySQL

新结构:
Client
 ├── AuthGateway (登录流程独立)
 │     └── AuthCache → AuthDB
 └── OrderGateway
       ├── Cache Layer(本地缓存+Redis)
       ├── InventoryService(库存微服务)
       ├── UserService(用户微服务)
       └── OrderDB

将原来的大单体拆分成多个微服务模块,通过Gateway统一接入。每个服务都有独立的部署环境和资源配额,避免彼此干扰。

2. 缓存层建设

我们引入了多级缓存体系:

  • 客户端缓存:浏览器端设置LocalStorage保存静态数据
  • Nginx本地缓存:对于完全静态的内容(如商品详情页)缓存在CDN或Nginx上
  • Redis集群:用户身份、优惠券状态等热点数据由Redis承担
  • 本地缓存(Caffeine):用于存储频繁访问的小对象,减少跨网络IO

Redis设计关键点:

  • 使用二级Key设计:{business}:{key}方式便于排查
  • 合理设置TTL:不同类别的数据生命周期不同
  • 对热点Key单独配置,防止穿透、击穿

代码示例(Spring Data Redis):

public class UserService {
    
    private final UserRepository userRepository;
    private final RedisTemplate<String, User> redisTemplate;

    public UserService(UserRepository repo, RedisTemplate<String, User> redis) {
        this.userRepository = repo;
        this.redisTemplate = redis;
    }

    public User getUserById(String userId) {
        String key = "user:" + userId;
        
        // 先查缓存
        User user = redisTemplate.opsForValue().get(key);
        if (user != null) return user;

        // 缓存未命中,查数据库
        user = userRepository.findById(userId).orElse(null);

        // 设置缓存(随机过期时间,防止集体失效)
        int expirationTime = (int)(Math.random() * 600 + 300); 
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, expirationTime, TimeUnit.SECONDS);
        }
        return user;
    }
}

系统架构设计图-1

3. 异步化与削峰填谷

为了缓解数据库压力,我们将一些非即时性操作异步化:

  • 下单动作中库存扣减使用MQ消息队列(Kafka)
  • 支付状态更新采用事件驱动方式处理
  • 日志记录转为批量入库

这样做的好处是能有效控制系统的吞吐量峰值,提升整体稳定性。

4. 数据库优化

数据库是最后一道防线,我们也做了很多优化工作:

  • 建立合理的索引,尤其是组合索引的设计要符合高频查询场景
  • 分库分表:根据用户ID哈希分8库,订单按时间分片
  • 实施读写分离:写走主库,读走从库(延迟容忍度高的操作可接受几秒同步滞后)

分库配置示例(MyCat中间件):

<schema name="ORDER_DB" checkSQLschema="false" sqlMaxLimit="100">
   <table name="orders" dataNode="dn1,dn2,dn3,dn4,dn5,dn6,dn7,dn8"
           rule="mod-userid-8"/>
</schema>

5. 接口性能优化

在接口层面我们也做了不少细节优化:

  • 统一返回格式,尽量精简字段输出
  • 将多个RPC调用合并为一个(Batch API)
  • 使用CompletableFuture实现异步编排

比如原本需要3个接口串联调用:

// 同步调用
public OrderInfo getOrderDetailSync(String orderId) {
    UserInfo userInfo = userService.getUser(orderId);
    Product product = productService.getProduct(orderId);
    Coupon coupon = couponService.getCoupon(orderId);
    return new OrderInfo(userInfo, product, coupon);
}

// 异步并行调用优化
public OrderInfo getOrderDetailAsync(String orderId) {
    CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(orderId));
    CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productService.getProduct(orderId));
    CompletableFuture<Coupon> couponFuture = CompletableFuture.supplyAsync(() -> couponService.getCoupon(orderId));

    return userFuture.thenCombine(productFuture, (u, p) -> new OrderInfo(u, p))
                     .thenApply(orderInfo -> {
                         try {
                             orderInfo.setCoupon(couponFuture.get());
                         } catch (Exception e) {
                             // handle exception
                         }
                         return orderInfo;
                     }).join();
}

踩坑经验:那些深夜调试教会我的事

1. 不合理限流引发连锁反应

一开始我们使用Guava RateLimiter来做限流,但在实际压测中发现它并不是线程安全的,在并发环境下会导致计数不准。后来换成了Sentinel,结合网关实现了更精细的速率控制。

教训:选型不能只看文档,一定要测试!

2. 缓存穿透问题

曾经有个接口因为查询不存在的ID,导致大量请求穿透到MySQL,进而触发慢查询报警。我们后来加了一层布隆过滤器来拦截非法请求,效果非常明显。

代码片段(使用RedisBloom扩展):

BF.ADD itemExistsFilter abc123
BF.EXISTS itemExistsFilter abc123 --> 1
BF.EXISTS itemExistsFilter invalidKey --> 0

3. 服务雪崩的惨痛教训

有一个促销活动当天,第三方配送系统挂掉,我们的服务也因为未设置超时熔断机制而跟着阻塞。这次事故让我们意识到断路器的重要性,之后在所有外部服务调用上都加上了Hystrix熔断逻辑。

代码示例(Resilience4j + Spring Cloud Sleuth):

@CircuitBreaker(name = "delivery-service", fallbackMethod = "fallbackDelivery")
public DeliveryStatus getDeliveryStatus(String orderId) {
    return restTemplate.getForObject("/delivery/status?orderId=" + orderId, DeliveryStatus.class);
}

private DeliveryStatus fallbackDelivery(Throwable t) {
    return DeliveryStatus.UNKNOWN;
}

效果总结:从十万到百万QPS的突破

经过三个多月的改造和测试,我们的系统最终支撑住了双十一大促的实际流量峰值:

指标 改造前 改造后
最大并发量 8k QPS 120k QPS
接口平均响应时间 900ms 120ms
数据库连接数 超过1500 控制在300以内
服务可用性 98% 99.99%

更关键的是:系统具备了弹性扩缩容能力。我们可以根据实时流量动态增加计算资源,这在过去是不可想象的。


经验分享:给你几点真诚的建议

如果你正准备做高并发系统的优化,下面是我亲身经历总结出来的几点实用建议:

1. 架构必须早于需求设计阶段就介入

不要等出了问题再想着补救。在项目初期就要考虑并发问题,哪怕当前需求还没达到很高并发。否则将来重构的成本将是现在的5倍以上。

2. 技术方案要“接地气”

别上来就搞什么Pulsar/Kafka/ElasticSearch全家桶,先想想你的系统真的需要吗?我见过太多堆了很多高端组件却没人会调优的系统。适合的才是最好的

3. 监控先行,预防为主

搭建一套完整的监控体系比任何临时修复手段更重要。我当时用的是Prometheus + Grafana + ELK这套组合拳,效果很好。提前预警远胜事后补救。

4. 性能优化要“有取舍”

不是每个模块都要追求极致性能,优先优化高频路径。比如我们把支付环节做到毫秒级响应,但退货退款这种低频功能就不值得花那么大力气。

5. 团队协作至关重要

一个人的能力终究有限,特别是遇到复杂分布式问题时,团队的配合和分工特别重要。定期做Code Review和压力测试演练,也是发现问题的好方法。


写在最后:高并发没有银弹,只有不断进化

系统架构设计图-2

这些年下来,我越来越觉得:高并发系统设计从来不是一个“完成品”,它更像是一个持续演进的过程。技术在变、业务在变、用户也在变,唯一不变的就是变化本身。

希望这篇文章能为你带来一些启发和思考。也许你现在还在纠结要不要引入Redisson做分布式锁,或者是不是该上Kubernetes集群,没关系,慢慢来。只要坚持站在实际需求的基础上解决问题,终有一天,你会感谢那个不断学习、不怕失败的自己。

如果你也经历过类似的挑战,欢迎留言交流;如果你还在路上,请相信:每一个半夜1点修改配置文件的你,终将换来白天用户流畅体验的笑容 🎉

评论 0

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