高并发系统设计:从理论到实践的实战思考

高思远
2025-06-28 13:28
阅读 2149

引言:一次“翻车”的教训让我重新理解了高并发

引言:一次“翻车”的教训让我重新理解了高并发

还记得去年公司推出的一个大促项目上线前夕,我们在压测环境上自信满满——QPS能跑到1W+,数据库也做了分库分表。结果活动一上线,服务器瞬间被请求淹没,接口超时严重,部分服务直接宕机,用户体验差到爆。

作为这个项目的负责人之一,那晚我彻夜未眠,在服务器日志里翻来覆去地看问题,最终发现是几个之前我们忽略的小细节在高并发下成了致命的瓶颈。

这件事之后,我开始重新审视整个系统的架构设计和性能优化思路,并结合过去几年的经验,逐步总结出了一套适合中大型项目的高并发系统设计方案。今天就和大家分享一下我是怎么一步步走过来的。


项目背景:一场促销引发的“血案”

项目背景:一场促销引发的“血案”

事情的起因是一个电商类平台在双十一大促前推出的限时秒杀功能。我们计划通过这个功能提升用户活跃度和交易转化率。

初期需求其实并不复杂:每小时开放一次、每次释放500个商品、每个用户限抢一件。听起来很常规是不是?但没想到的是,活动前期用户预约量达到了300万人,实际开抢那一刻同时点击量峰值超过了20万并发请求/秒,远远超出我们预估的上限。

我们当时的技术栈如下:

  • 后端:Spring Boot + MyBatis
  • 数据库:MySQL(单主单从)+ Redis
  • 缓存层:Redis Cluster
  • 前端:Vue.js + Nginx
  • 消息队列:RabbitMQ(用于异步处理订单)

表面上看这套架构还算完整,但我们忽略了几个关键点:

  1. 库存扣减不是原子操作
  2. 大量重复请求集中在同一个热点商品上
  3. 消息队列吞吐跟不上写入速度
  4. 缓存穿透导致数据库被打穿

结果就是我们迎来了人生中的第一次“高并发踩坑”,而这也让我真正意识到一个高并发系统的复杂性远不止堆机器那么简单。


问题描述:高并发下的四大杀手

API接口文档-1

1. 超卖问题频发

最严重的现象就是出现“超额发货”,明明只有500件商品,但后台查出来有600多笔订单生成。原因在于并发环境下对库存的读写没有使用CAS或者加锁机制,两个线程同时判断库存>0,都进行了扣减。

2. DB扛不住压力

虽然用了Redis做缓存,但是大量的“击穿”请求还是绕过了缓存直达数据库。我们没有对空数据做布隆过滤器兜底,也没有限制请求频率,数据库连接池瞬间打满,CPU飙升到90%以上。

3. 接口响应时间暴涨

原本毫秒级的接口突然变成长达几秒甚至十几秒的请求,用户不断刷新页面导致雪崩效应,整个系统进入恶性循环。

4. 日志与监控形同虚设

日志输出格式混乱,没有统一追踪ID,一旦出问题根本无法快速定位。监控只关注了基础指标,缺少业务层面的埋点统计,导致我们只能靠“猜”来排查问题。

这些问题暴露了我们对高并发场景下系统设计的不足,也促使我们开始了架构重构之旅。


解决方案:层层拆解,构建稳定的高并发体系

为了彻底解决这些问题,我在原有基础上做了以下几个关键改造。


一、引入本地缓存 + Redis + LRU策略降低 DB 负载

我们在服务层加入了一个本地缓存层(Caffeine),用于缓存一些频繁访问的配置数据和热点商品信息。具体逻辑如下:

// 伪代码示例
public Product getProductDetail(Long productId) {
    Product product = caffeineCache.getIfPresent(productId);
    if (product == null) {
        product = redisService.get("product:" + productId);
        if (product == null) {
            product = dbMapper.selectById(productId);
            if (product != null) {
                redisService.setex("product:" + productId, 60, product);
            }
        }
        caffeineCache.put(productId, product);
    }
    return product;
}

这样做的好处有几个:

  • 减少Redis调用次数,降低网络损耗
  • 在Redis不可用的情况下也能保证基本可用性
  • 结合TTL设置避免缓存污染

二、分布式锁 + Lua脚本实现原子扣减

为了解决超卖问题,我们使用Redis + Lua脚本来实现原子化的库存扣减操作。

例如,将以下逻辑写成Lua脚本:

-- 扣减库存的Lua脚本
local stockKey = KEYS[1]
local orderId = ARGV[1]
local currentStock = redis.call('GET', stockKey)
if tonumber(currentStock) > 0 then
    redis.call('DECR', stockKey)
    return "success"
else
    return "fail"
end

Java调用方式如下:

String result = redisTemplate.execute(script, Arrays.asList("stock:1001"), "order_123");

这种方式可以确保多个并发请求下,只有一个能成功执行库存扣减,其他请求会立即返回失败,从而有效防止超卖。


三、引入布隆过滤器防御缓存穿透攻击

针对那些不存在的商品ID频繁访问的情况,我们增加了布隆过滤器:

BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000);

// 初始化时将所有合法商品ID加入布隆过滤器
List<Product> productList = productService.getAllProducts();
for (Product p : productList) {
    bloomFilter.put(p.getId().toString());
}

// 访问时先过滤
if (!bloomFilter.mightContain(productId)) {
    return Response.error("商品不存在");
}

这一招在应对恶意刷请求的场景下特别有效,也降低了Redis和DB的压力。


四、基于令牌桶实现流量限流,防止突发冲击

我们使用了Google的Guava提供的RateLimiter来控制接口的访问频率:

RateLimiter rateLimiter = RateLimiter.create(100); // 每秒允许100次请求

boolean isAllowed = rateLimiter.tryAcquire();
if (!isAllowed) {
    return Response.error("系统繁忙,请稍后再试");
}

后来我们又升级到了Sentinel,支持更细粒度的流控规则(比如按IP、用户级别),并且能动态调整限流阈值。


五、消息队列削峰填谷 + 异步落库

我们把下单和支付确认的操作通过RabbitMQ异步化处理,流程大致如下:

  1. 前置检查通过后,将下单请求写入MQ
  2. MQ消费者消费后进行订单创建、库存更新等操作
  3. 返回“排队中”提示给前端,避免用户不断刷新

这样的做法极大地缓解了数据库的压力,同时提升了用户体验。


六、链路追踪 + 日志采集监控一体化

我们在系统中接入了SkyWalking来做全链路追踪,并结合ELK收集日志信息,实现了如下效果:

  • 请求路径可视化,快速定位慢查询
  • 自动统计各接口QPS、成功率、响应时间分布
  • 对异常请求自动报警
  • 支持根据traceId关联上下游调用链

有了这些工具,我们再遇到线上问题的时候再也不用“盲人摸象”了。


七、数据库层面的优化

除了应用层的改动之外,我们在数据库方面也做了几项重要的调整:

  1. 冷热分离:将历史订单和高频实时订单分开存储
  2. 读写分离:采用MyCat做代理,主写从读
  3. 索引优化:去掉不必要的二级索引,重建组合索引
  4. 分库分表:使用ShardingSphere按用户ID哈希分片,减轻单库压力
  5. 事务隔离:使用乐观锁代替悲观锁,减少阻塞

这些措施显著提升了系统的稳定性和扩展性。


效果对比:优化后的表现与收益

指标 优化前 优化后
QPS 1200 18000
接口平均耗时 800ms <200ms
并发能力 2000 35000
DB连接数 150 <50
错误率 15% <0.1%

最重要的是,我们成功支撑住了双十一当天的大促活动,没有任何一起生产事故,用户投诉大幅下降,老板还特意请我们吃了顿火锅表示感谢(笑)。


经验分享:我的几点心得

1. 不要等到上线再考虑性能问题

很多开发者总想着“先把功能做完,性能后面再说”。但往往性能问题都不是临时改出来的。建议在系统设计阶段就把高并发考虑进去,包括:

  • 接口设计是否幂等
  • 状态变更是否可追溯
  • 是否需要异步处理
  • 数据一致性如何保障

2. 避免过度设计,但也别低估系统承载力

我见过很多团队动不动上来就是微服务、多活架构、分布式事务……但其实对于中小项目来说,简单架构+良好的代码规范+合理限流反而更靠谱。

但也不能盲目相信“一台机器就能扛住”,一定要结合自身业务增长预期来设计容量。

3. 技术是手段,业务才是目的

有时候我们会陷入技术细节,纠结某个算法更快还是某个组件更好。但不要忘了,我们要解决的是业务问题,不是技术炫技。选择合适的技术方案比追求最新潮的技术更重要。

比如我们曾想过引入Elasticsearch做商品搜索,结果调研发现现有MySQL全文索引已经能满足业务需求,没必要加重运维负担。

4. 监控、日志、报警缺一不可

高并发系统一定得有一套完整的可观测性体系。建议至少包括:

  • 应用健康状态(JVM、GC、线程)
  • 接口维度的QPS、P99耗时、错误率
  • 中间件监控(Redis、MQ、数据库)
  • 业务埋点统计(用户行为、订单流转)

这些东西可能平时看不见,但在关键时刻能救你一命。


写在最后:高并发的本质是思维转变

经过这次项目洗礼,我逐渐明白:

高并发不是一个纯技术问题,更是一种思维方式的转变。

它要求我们在面对每一个需求的时候,都要多一层“放大”思考:如果这东西变成10倍流量怎么办?会不会拖垮整个系统?有没有替代方案?数据会不会丢失?

很多时候,真正的高并发优化,不在于用多牛逼的中间件,而在于你在设计之初能不能想到这些边界情况,并在编码过程中坚持做好每一件小事。

希望这篇文章能带给你一些启发,也欢迎你在评论区留言交流,我们一起打磨出更好的系统架构。

评论 0

最热最新
暂无评论
高思远Lv.1
0
影响力
0
文章
0
粉丝