高并发系统设计:我是怎么从“撑不住”到“扛得住”的
背景介绍

去年年底,我们团队接手了一个电商平台的秒杀系统重构项目。说是重构,其实更像是重建——老系统已经不堪重负,每逢大促,服务器就疯狂报警,数据库主从延迟严重,接口超时率飙升,用户抱怨不断。
我作为技术负责人,第一次看到压测报告的时候,整个人都不好了。QPS 两三百?连双十一的零头都不到!更糟的是,每次促销活动一开,订单系统就开始排队,甚至出现数据不一致的问题。
这事儿不能再拖了。我们决定全面重构秒杀模块的核心架构,目标只有一个:能抗住上万 QPS,保证数据一致性,并且在极端压力下还能稳得住。
今天就来分享一下,我们在设计和落地这个高并发系统的整个过程中所踩过的坑、学到的经验,以及最终交出的成绩单。
真实挑战:我们到底遇到了什么问题?


刚开始分析老系统的问题时,发现并不是单一瓶颈,而是多个方面都在拖后腿:
1. 接口无限制,随便抢
老系统没有做任何限流措施,用户只要疯狂刷新页面或用脚本,就能瞬间打爆服务端。一个简单的请求,直接穿透到了数据库层,导致 DB 压力暴增。
2. 数据库扛不住写操作
所有下单逻辑都走 MySQL 写入流程,而且是同步的。一旦库存扣减失败,还会触发事务回滚,进一步影响性能。尤其是在并发高的时候,经常出现死锁。
3. Redis 缓存穿透频繁发生
缓存设计不合理,大量无效请求穿透到 DB。虽然用了 Redis,但没设置合理的失效策略和空值处理机制。
4. 异步处理能力薄弱
订单生成、短信发送等非核心功能都是同步执行的,结果导致主线程被阻塞,进一步加剧了响应延迟。
5. 没有降级机制
当系统快崩溃的时候没有任何自动降级手段,只能靠人工关掉接口或者重启服务。
面对这些问题,我们深知不能只修修补补,必须从架构层面重新设计一套稳定高效的系统。
架构设计方案与选型对比
我们做了几轮内部技术评审,最终确定采用如下架构方案:
整体结构:
- 接入层:Nginx + Lua
- 服务层:Spring Boot + Netty(部分异步处理)
- 缓存层:Redis Cluster + Guava 本地缓存
- 消息队列:RocketMQ 解耦关键链路
- 数据库:MySQL 分库分表 + TDDL 中间件
- 限流熔断:Sentinel + Nginx 限流
- 监控告警:Prometheus + Grafana
我们为什么这么选?
1. 为什么要用 Nginx+Lua 做前置控制?
一开始我们考虑用 Java 做限流,比如 RateLimiter 或者 Sentine,但这些毕竟还是运行在 JVM 层。而 Nginx 天然具备高性能的流量控制能力,再配合 OpenResty 的 Lua 脚本,可以快速完成请求拦截、黑白名单、频次控制等功能。
举个例子,在 Lua 里我们做了这样的限流逻辑:
location /seckill {
access_by_lua_block {
local limit = require "limit"
local key = ngx.var.remote_addr -- 或者 token
if not limit.check(key) then
return ngx.exit(503)
end
}
}
这段代码在 Nginx 层面就把很多恶意请求挡住了,对 DB 的冲击小了很多。
2. Redis 和 MySQL 如何协同作战?
我们采取了经典的“双缓存 + 预减库存”策略:
- 商品信息预加载到 Redis
- 抢购开始前,通过 Lua 脚本原子性地扣减 Redis 中库存
- 成功扣减后再将订单写入数据库(异步)
这样设计的好处是尽可能减少 MySQL 的写入次数,同时又保障了数据准确性。
3. RocketMQ 为什么必不可少?
把订单生成、物流通知、积分变更等非核心链路解耦出来,使用消息队列异步处理,让主流程尽可能快地返回。比如:
// 下单成功后发消息到 MQ
rocketMQTemplate.convertAndSend("ORDER_CREATED_TOPIC", orderDTO);
消费方根据消息逐步执行后续动作,即使某一步失败也不会影响用户下单体验。
核心代码实现和关键配置
1. Redis 扣减库存的 Lua 脚本
-- stock.lua
local key = KEYS[1]
local count = tonumber(ARGV[1])
if redis.call('exists', key) == 1 then
local current_stock = tonumber(redis.call('get', key))
if current_stock >= count then
redis.call('decrby', key, count)
return true
else
return false
end
else
return false
end
Java 调用示例:
public boolean deductStock(String productId, int quantity) {
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(luaScript, Boolean.class);
Boolean result = redisTemplate.execute(script, Arrays.asList(productId), quantity);
return Boolean.TRUE.equals(result);
}

2. 使用 Sentinel 实现限流
配置一个全局的限流规则:
rules:
- resource: "/seckill/submit"
grade: 1
count: 100
strategy: 0
并在 Controller 上添加注解即可:
@GetMapping("/submit")
@SentinelResource(value = "/seckill/submit", blockHandlerClass = BlockHandler.class, blockHandler = "handleBlock")
public ResponseEntity<?> submitOrder() {
// 扣库存 + 下单逻辑
}
踩过的坑和解决思路
坑1:Redis 减库存之后 MySQL 插入失败怎么办?
这个问题我们初期也没想到。假设 Redis 库存减了 1,但 MySQL 因为唯一索引冲突(比如重复提交同一订单)插入失败,这时候库存就没法恢复。
解决方案: 我们引入了一个“待定订单池”,Redis 中记录当前用户是否有待定订单,如果 MySQL 插入失败,则释放 Redis 锁并触发补偿机制。
坑2:MQ 消费顺序被打乱
某个促销活动中,由于 RocketMQ 的默认并发消费机制,导致用户的订单状态更新和短信通知先后顺序错乱。
解决方法:
- 对同一个用户的消息按照 user_id hash 到同一个队列中
- 设置消费者线程数为 1,确保顺序消费
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
String userId = new String(msg.getBody()).split(":")[0];
processInOrder(userId, msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
坑3:压测时 CPU 100%,接口无响应
后来才发现是因为我们用了一个同步日志组件,每个请求都刷盘一次,导致 I/O 线程阻塞。
对策:
- 改成异步日志打印(Logback AsyncAppender)
- 控制日志级别,关闭不必要的 debug 日志
最终效果和收益
经过三个月的开发和多轮压测,最终上线后的表现超出预期:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| QPS | 300 | 11,000 |
| 平均响应时间 | 800ms | 60ms |
| 数据一致性错误 | 0.5% | <0.01% |
| 大促期间故障率 | 时常宕机 | 全程平稳 |
最让我欣慰的是,今年双十一大促当天,整个秒杀系统顶住了峰值流量,没有出现一起用户投诉。
经验总结:给你的几点建议
如果你也在做类似的高并发系统开发,这里是我踩过坑之后总结的一些实用经验:
✅ 1. 永远不要相信前端会帮你限流
很多研发前期会想:“反正前端会限制点击频率,不会有问题。”但实际生产环境里,总有聪明人拿着脚本模拟请求。所以,限流必须从网关做起。
✅ 2. Redis 是好帮手,但不是万能钥匙
缓存能提升性能,但也要考虑缓存击穿、穿透等问题。记得结合本地缓存(如 Guava)、布隆过滤器、以及后台定时刷新策略。
✅ 3. 异步不是万能,但几乎总是需要的
把非核心链路抽离出来,通过 MQ 或线程池处理,主流程才能更快响应,用户体验也更好。
✅ 4. 数据库必须提前规划扩容方案
分库分表、读写分离、连接池大小设置、慢 SQL 监控,这些数据库相关的优化工作必须在项目早期做好架构设计。
✅ 5. 监控比编码更重要
上线之前一定要集成 Prometheus + Grafana + ELK,监控每一个服务节点的状态、请求成功率、JVM 内存变化等指标,出了问题可以第一时间定位。
✅ 6. 测试要真实,压力测试更要贴近现实
别只测 HTTP 请求,要模拟真实业务场景,包括异常情况、网络抖动、MQ 延迟等情况下的系统反应。
写在最后
回头看,这套高并发系统的设计和实现过程并不轻松,中间也遇到不少挫折,但我们始终坚持两个原则:
- 以用户为中心:任何技术决策都不能牺牲用户体验。
- 工程化思维先行:先设计架构、再写代码,避免盲目堆代码导致不可维护。
现在每当用户反馈“这次秒杀体验很棒”的时候,我就特别感慨。那些深夜调试、线上查日志、反复修改限流配置的日子,终于有了回报。
希望这篇文章,不只是讲了一套技术方案,也能带给你一些真实的启发。愿我们一起写出更多“扛得住”的系统。
如有问题欢迎留言交流,共同成长 💪

评论 0