高并发系统设计:从理论到实践 —— 我的踩坑与成长之路

贪心没贪够
2025-06-25 02:25
阅读 703

开篇:一次“崩溃”让我重新认识高并发

开篇:一次“崩溃”让我重新认识高并发

作为一名工作五年的后端工程师,我经历过多个项目的开发、维护甚至重构,但真正让我对“高并发”这个概念有深入理解的,是一次让我彻夜难眠的产品上线事故。

那是我加入一家电商公司不久后负责的一个秒杀活动。当时我们预计会有10万用户参与,结果开抢的一瞬间,我们的服务器直接崩了。数据库连接池爆满、服务响应超时、接口返回500错误……整个系统像被潮水冲垮的大坝一样,完全失控。

那场事故之后,我开始深刻反思:到底什么是高并发系统?它的本质是什么?为什么理论看起来没问题,实际部署就出问题?于是,我花了大量的时间去研究和实践,也踩了不少坑。今天我就想把这些经验和大家分享出来,希望你们在面对高并发问题时,能少走点弯路。


项目背景:一个真实的挑战

项目背景:一个真实的挑战

项目介绍

我们要做一个限时限量的秒杀平台,支持百万级用户同时在线抢购商品,商品数量有限(比如100件),要求保证不超卖、不重复下单、数据一致性,以及尽可能低的延迟。

技术架构初稿

  • 后端框架:Spring Boot + MyBatis
  • 数据库:MySQL(读写分离)
  • 缓存:Redis
  • 消息队列:RabbitMQ
  • 网关层:Nginx 做负载均衡
  • 限流组件:Guava RateLimiter(后来换成Sentinel)

听起来很完整,对吧?但理想很丰满,现实却很骨感。


问题描述:那些让人崩溃的时刻

问题描述:那些让人崩溃的时刻

上线前压测一切正常,QPS 能做到2k左右,理论上是够用的。但正式上线后,情况完全不同:

  1. MySQL 连接池爆满
    所有请求都打到 MySQL 上做库存判断,导致数据库连接数急剧上升,连接池耗尽,进而引发线程阻塞,最终雪崩。

  2. Redis 分布式锁频繁失败
    使用 Redlock 实现分布式减库存时,由于 Redis 集群配置不合理,出现了锁竞争激烈、获取失败的情况。

  3. 消息堆积严重,无法及时处理订单
    RabbitMQ 的消费者消费速度跟不上生产速度,导致大量订单积压,后续出现超卖现象。

  4. 前端请求没控制,大量无效请求冲击接口
    有些用户使用脚本疯狂刷接口,根本没有进入排队逻辑,直接压垮后台。

  5. 代码逻辑存在竞态条件
    使用乐观锁更新库存时,没有做好重试机制,导致多次扣减失败,数据不一致。

这些问题,每一项都能单独写一篇文章,合在一起,简直就是一场灾难。


解决方案:一步步构建高并发架构

1. 接口限流 + 请求排队

我们第一件事就是加限流。最初用的是 Guava 的 RateLimiter,但它只适合单机环境,无法跨节点统一限流。后来换成了阿里开源的 Sentinel,支持集群限流。

// 使用 Sentinel 对接口进行限流
@SentinelResource(value = "seckill", blockHandler = "handleSeckillBlock")
public ResponseEntity<String> seckill(String userId, String productId) {
    // 核心逻辑...
}

此外,我们在 Nginx 层面加了一个请求排队策略(fair queueing):

upstream backend {
    least_conn;
    server 192.168.0.1;
    server 192.168.0.2;
}

location /seckill {
    proxy_pass http://backend;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;

    limit_req zone=one burst=5 nodelay; # 限流配置
}

这样可以避免短时间大量请求涌入造成服务雪崩。


2. 异步化 + 消息队列解耦业务逻辑

原来的所有操作都在一次 HTTP 请求里完成:查库存 -> 减库存 -> 下订单 -> 发消息 -> 返回结果。这种同步调用非常吃资源,特别是在高并发下容易卡死。

我们把核心逻辑拆分成异步任务:

  • 用户点击“秒杀”按钮,先写入一个排队队列(Kafka/RabbitMQ)
  • 消费者按顺序处理每个排队请求,执行真正的业务逻辑

伪代码如下:

// 入队操作
public void submitToQueue(String userId, String productId) {
    rabbitTemplate.convertAndSend("seckill_queue", new SeckillMessage(userId, productId));
}

// 消费者逻辑
@RabbitListener(queues = "seckill_queue")
public void processSeckill(SeckillMessage message) {
    // 1. 查看库存是否充足
    // 2. 更新库存(带CAS)
    // 3. 创建订单
    // 4. 记录日志
}

这样的方式极大地缓解了系统压力,也让我们有机会去做流量削峰。


3. 缓存预热 + 冷启动优化

为了减少对 MySQL 的依赖,我们引入了 Redis 作为前置缓存,并且在活动开始前做缓存预热:

  • 商品库存信息提前加载进 Redis
  • 设置过期时间,防止长期缓存污染
  • 使用 Lua 脚本保证 Redis 操作的原子性

Lua 示例代码如下:

-- redis.lua
local key = KEYS[1]
local userKey = KEYS[2]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or "0")

if current <= 0 then
    return -1 -- 已售罄
end

if redis.call('SISMEMBER', userKey, user_id) == 1 then
    return -2 -- 已抢购过
end

redis.call('DECR', key)
redis.call('SADD', userKey, user_id)

return current - 1

这段脚本保证了两个关键点:库存扣除和用户防重,都是原子操作。


4. 数据库层面优化:热点隔离 + 分库分表

MySQL 最初扛不住的原因,是因为所有商品库存都集中在一个表中,且使用了默认的 InnoDB 引擎。每次并发修改都要加行锁,效率极低。

我们做了以下优化:

  1. 热点商品单独建表:将大促商品独立出来,避免与其他非热点商品争锁。
  2. 使用内存引擎(Memory Engine)缓存热点库存:虽然不持久,但在大促期间临时使用效果显著。
  3. 引入本地缓存 + 定时落库机制:使用 Caffeine 做一层本地缓存,每秒钟异步落库一次。

部分代码示例:

// 使用 Caffeine 做本地缓存
Cache<String, Integer> localCache = Caffeine.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(1, TimeUnit.SECONDS)
    .build();

int stock = localCache.getIfPresent(productId);
if (stock > 0) {
    // 执行更新操作
    localCache.put(productId, stock - 1);
}

当然,这只是过渡方案。最终还是要走向分库分表:

  • 使用 ShardingSphere 做水平分片
  • 按照商品 ID 或用户 ID 进行 Hash 分片
  • 保证同一个用户的操作落在同一个分片上

踩过的坑:那些深夜里的调试记忆

1. Redis 分布式锁的问题

最开始我们用了 setnx 来实现分布式锁,但因为没有设置超时时间,一旦某个服务崩溃,锁就会一直存在,导致其他服务无法获取锁,从而死锁。

后来改为:

Boolean success = redisTemplate.opsForValue().setIfAbsent("lock:product:" + productId, requestId, 10, TimeUnit.SECONDS);

并且加上自动续期机制,才解决了这个问题。

2. RabbitMQ 消费慢导致堆积

我们一开始设置了默认的消费者线程数,但随着订单量飙升,队列越堆越多。后来通过动态扩容(Kubernetes + HPA)实现了自动增加消费者数量。

3. 不合理的索引设计拖慢 MySQL

有一个查询语句原本没有索引:

SELECT * FROM orders WHERE product_id = ? AND user_id = ?

因为没加联合索引,导致全表扫描,严重影响性能。加索引之后:

ALTER TABLE orders ADD INDEX idx_product_user (product_id, user_id);

查询效率提升了几十倍。

4. 忘记熔断和服务降级

有一段时间外部支付服务不稳定,我们没有做服务降级,导致整个下单流程都被拖慢。后来引入了 Hystrix(现在换成了 Resilience4j),设置熔断阈值和 fallback 逻辑:

@HystrixCommand(fallbackMethod = "fallbackPay")
public boolean payOrder(String orderId) {
    // 调用远程支付服务
}

效果总结:从崩溃到稳定运行

经过以上一系列改造后,我们成功将系统抗压能力从原来的 QPS 2k 提升到了 20k+,主要指标如下:

指标 改造前 改造后
QPS ~2,000 ~20,000
响应时间 3s+ < 200ms
秒杀成功率 <30% >95%
错误率 <1%

而且最关键的是,再也没有出现过超卖或者数据混乱。这次经历让我彻底明白:高并发系统的稳定性,不是靠一两个组件撑起来的,而是整体架构合理、细节到位、层层兜底的结果。


经验分享:给正在踩坑的你几点建议

  1. 别迷信压测数据
    压测只是模拟场景,真实流量远比你想的复杂。务必预留安全系数,比如预期 QPS 是 1w,你的系统至少要能抗住 3w。

  2. 先缓存后 DB
    面对高并发写入,不要盲目信任数据库。先用缓存挡一下,再定时批量落库。

  3. 关注服务间的依赖关系
    任何一个服务挂掉,可能都会影响整个链路。所以服务必须要有熔断、降级、重试、兜底策略。

  4. 监控必须贯穿整个生命周期
    包括:接口响应时间、慢 SQL、队列堆积、JVM 状态、Redis 命中率等,只有掌握实时数据,才能快速定位问题。

  5. 不要忽视运维视角
    高并发系统上线后,运维才是最关键的环节。务必制定完善的报警机制、预案流程、故障恢复策略。


写在最后:高并发背后是系统思维的成长

高并发从来不是一个技术点,它是一个系统工程。从最初的代码设计,到后来的服务架构,再到上线后的运维保障,每一个环节都必须考虑“并发”的可能性。

在我这几年的工作中,遇到过不少比我厉害的人,也踩过很多坑。但我发现,真正优秀的后端工程师,不是会多少新框架,也不是懂多复杂的算法,而是能在关键时刻冷静地思考问题,快速判断瓶颈所在,并做出合理的架构决策。

如果你也在做高并发相关的系统,或者即将面临类似的挑战,希望这篇文章能给你一些启发。愿我们一起在这个不确定的世界里,写出更稳健、更具韧性的系统。


作者简介:五年Java后端经验,热爱架构设计与性能调优,曾在电商、社交、教育等多个领域主导高并发系统建设。欢迎交流:[邮箱/公众号](略)

评论 0

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