高并发系统设计:那些年我们一起踩过的坑
引子:为什么我决定写这篇文章?

说实话,写这篇文章一开始并不是为了“分享经验”,更多是一种“自省”。作为一名经历过几个大型项目的后端开发者,我在实际工作中多次面对高并发系统的挑战。有些是突如其来的访问高峰,有些是数据库瓶颈,还有些是架构不合理导致的雪崩效应。
今天想聊的,是我们团队在为一个电商项目做双十一流量支撑时的真实经历。那时候我们第一次真正意义上面对百万级 QPS 的冲击——那真是一次从理论到实践、再到崩溃边缘的完整洗礼。
希望这篇基于真实工作场景的文章,能给正在或即将走上这条“不归路”的兄弟姐妹们一点启发和共鸣。
项目背景:一场关于“秒杀”的战役

事情得从一个叫做 “闪购频道” 的项目说起。公司要在双十一期间上线一个限时限量秒杀活动,预计单场活动流量可达每分钟上百万请求,并且要求下单响应时间控制在500ms以内。
当时的后端架构是传统的三层架构:
Nginx + Spring Boot + MySQL
缓存用的是Redis,但只用了本地缓存和少量热点数据缓存。整体部署结构也很简单,几台ECS实例加上RDS。
系统初期压力测试暴露出的问题
我们在压测环境进行了一轮基准测试,使用JMeter模拟1000个并发用户持续请求秒杀接口,结果如下:
- 接口平均响应时间超过2秒
- 数据库CPU直接飙红
- Redis连接超时现象严重
- 出现大量重复下单(库存扣减不同步)
更可怕的是,在QPS达到800左右时,整个服务开始出现连锁反应,下游系统也跟着被拖垮。这已经不是简单的性能问题,而是系统架构设计层面的根本性缺陷。
第一波优化:分而治之,拆解瓶颈
技术选型对比与决策
我们迅速拉通了技术负责人开会讨论,大家一致认为必须从以下几个方面入手:
| 问题点 | 解决方案 | 替代方案 | 最终选择理由 |
|---|---|---|---|
| 请求压力集中 | 前置缓存+队列削峰 | 单纯扩容服务器 | 成本低,见效快 |
| 数据库瓶颈 | 读写分离+异步持久化 | 直接切换MongoDB | 已有MySQL体系,改造成本小 |
| 库存超卖 | Redis原子操作+预扣机制 | 分布式锁 | 实现更简单,Redis本身支持 |
| 失败重试风暴 | 限流+熔断 | 消息补偿 | 控制面更大 |
下面我会重点讲其中几个核心点是如何实现的。
解决方案一:前置缓存与队列削峰
我们将原有的直接处理秒杀逻辑调整为两个阶段:
- 排队阶段:用户点击“抢购”进入排队,将请求加入消息队列。
- 执行阶段:由后台worker从队列中消费并执行真正的秒杀操作。
前端页面增加排队提示,降低用户刷新频率,减少无效请求。
// 示例代码:使用RabbitMQ发送秒杀请求
public void sendSeckillRequest(Long userId, Long productId) {
try {
String message = JSON.toJSONString(new SeckillMessage(userId, productId));
rabbitTemplate.convertAndSend("seckill.queue", message);
} catch (Exception e) {
log.error("发送秒杀消息失败", e);
// 失败回调或者降级处理
}
}
同时我们将商品信息缓存在Redis中,包括库存、状态等,并通过定时任务同步数据库状态:
// 初始化库存示例
public void initStockCache(Long productId, int stockCount) {
String key = "product:stock:" + productId;
redisTemplate.opsForValue().set(key, String.valueOf(stockCount), 30, TimeUnit.MINUTES);
}
解决方案二:Redis分布式锁与库存预扣
为了避免并发超卖问题,最初我们尝试用Redis分布式锁:
public boolean lockProduct(String productId, String requestId, long expireTime) {
Boolean success = redisTemplate.opsForValue().setIfAbsent("lock:" + productId, requestId, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(success);
}
但后来发现,在大并发下锁的竞争太激烈,影响效率。于是我们改为使用Redis INCR 和 Lua脚本来保证原子性:
// 使用Lua脚本保障原子性
String script = "local stock = redis.call('GET', KEYS[1])\n" +
"if tonumber(stock) > 0 then\n" +
" redis.call('DECR', KEYS[1])\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript, Arrays.asList("product:stock:" + productId));
return result == 1;
解决方案三:读写分离与数据库优化
随着流量增大,主数据库压力飙升,我们立即引入了数据库读写分离方案:
- 使用MyCat做中间件实现读写分离
- 主库负责写入订单、库存变更
- 从库处理非实时查询类请求
同时针对秒杀表做了以下优化:
- 表结构瘦身,仅保留必要字段
- 增加复合索引
(user_id, product_id) - 将下单事务简化为两段式提交
此外,我们还使用了延迟双删策略来缓解缓存与数据库一致性问题:
// 伪代码:更新数据库后删除缓存
public void updateOrderAndInvalidateCache(Order order) {
// 1. 更新数据库
orderRepository.save(order);
// 2. 删除缓存
deleteFromCache(order.getUserId());
// 3. 延迟二次删除(应对可能的数据库延迟)
taskQueue.addTask(() -> {
Thread.sleep(500);
deleteFromCache(order.getUserId());
});
}
踩坑经验:那些你以为没问题却翻车的地方
坑点一:Redis连接池设置不合理
最开始我们使用默认配置的Jedis连接池,只有几十个连接,远远不够。后来换成了Lettuce + 连接池配置提升吞吐量:
spring:
redis:
lettuce:
pool:
max-active: 512 # 最大连接数
max-idle: 256 # 最大空闲连接
min-idle: 64 # 最小空闲连接
max-wait: 2000ms # 获取连接最大等待时间
坑点二:Kafka分区设置不当
消息队列我们用的是Kafka,初始只设置了3个分区,结果消费者无法并发处理。最终根据预期QPS划分了12个分区,提升了整体吞吐能力。
坑点三:线程池没隔离,导致线程阻塞
我们在处理下单逻辑的时候没有对线程池做隔离,所有任务共用一个线程池,导致某个接口慢了之后整个服务不可用。后来我们引入了Hystrix实现线程池隔离。
虽然Hystrix现在不维护了,但对于当时来说是个救命稻草。
效果总结:从崩溃边缘到稳定运行
经过一轮又一轮优化,最终效果如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均响应时间 | >2s | <300ms |
| 秒级吞吐 | ~800 QPS | >8000 QPS |
| 错误率 | >20% | <0.1% |
| 系统稳定性 | 经常崩溃 | 持续稳定运行 |
更重要的是,这套架构在后续多个促销活动中都得到了验证,具备良好的可扩展性和复用性。
经验分享:写给正在路上的你
1. 提前规划永远比临时救火重要
不要等到大促前夕才想到做压测。日常就应该保持对系统承载力的了解,定期做容量评估。
2. 不要迷信单一技术方案
高并发场景下往往是多种手段结合使用。比如缓存、队列、异步、分布式、限流、熔断……缺一不可。
3. 监控是你的眼睛
一定要尽早接入APM系统(如SkyWalking、Pinpoint),以及基础指标采集(Prometheus + Grafana)。出了问题才能第一时间定位。
4. 日志是你的朋友
建议统一接入ELK,方便事后分析。日志级别要控制好,生产环境避免DEBUG输出。
5. 压测才是唯一真理
无论架构设计得多漂亮,不压测就是纸上谈兵。推荐使用Gatling或Locust做压测工具,模拟真实用户行为。
写在最后:技术之外的一些思考
说真的,做完那次大促,我整个人都瘦了一圈,头发也没少掉。
但最大的收获不是技术上的积累,而是意识到了一个优秀架构的重要性。它不仅仅是技术选型的问题,更是对业务的理解、对细节的关注、对突发状况的预判。
高并发系统设计从来都不是一项孤立的技术,它考验的是一个全栈工程师的综合能力。
愿你们在自己的项目里也能披荆斩棘,写出既优雅又能扛住流量的系统。如果哪天你也遇到了“十万并发进不来一条”的奇景,别慌,坐下来喝杯茶,想想我们这些前辈是如何挺过来的 😊
如有需要交流具体实现细节或相关源码,欢迎留言或私信,我们可以进一步探讨!

评论 0