高并发系统设计:从理论到实践
引言:一场压测事故引发的思考

我第一次意识到高并发系统设计有多重要,是在一次线上压测事故中。那天我们刚上线了一个新的订单查询服务,原本以为只是一个简单的接口。但当测试团队开启JMeter模拟1万并发请求时,数据库瞬间被打爆了,连接池满了、响应超时、CPU飙升……最终整个系统崩溃,被迫回滚。
这件事让我深刻反思:原来不是所有系统都吃得消大流量,也不是所有的架构都能扛得住压力。
从那时起,我开始系统学习和实践高并发系统的架构设计。今天,我想结合这几年在互联网公司的实际项目经验,尤其是参与某电商秒杀系统重构的过程,和大家分享一下我们在“高并发”这条路上踩过的坑,以及积累下来的一些经验和教训。
项目背景:秒杀系统的挑战


我们当时正在重构一个电商平台的秒杀系统,目标是支持每天上百万用户的高频访问,高峰期每秒要处理5000+的请求,并且要在短时间内完成下单、扣库存、生成订单等一系列操作。
这个系统的难点在于:
- 时间集中性强:用户集中在某个时间段涌入
- 数据一致性要求高:库存必须保证不超卖
- 响应速度快:不能让用户等待
- 流量突增不可预测:随时可能被热点事件引爆
我们接手这个项目时,旧系统采用的是典型的单体架构 + MySQL直接读写,在活动期间经常出现卡顿、订单重复、数据库死锁等问题。
这显然无法满足日益增长的业务需求,也根本经不起真正的“秒杀”考验。
于是,我们决定彻底重构系统架构,引入一系列高并发场景下的最佳实践。
面临的核心挑战

挑战一:秒杀商品库存竞争严重
用户抢购同一商品导致大量线程并发修改库存,造成数据库锁表、慢SQL频发,甚至出现脏数据。
挑战二:网络层瓶颈明显
接入层使用Nginx做负载均衡,但QPS到达一定阈值后,就开始出现丢包、超时等现象,严重影响用户体验。
挑战三:缓存雪崩、穿透和击穿
旧系统使用Redis作为一级缓存,但在某些时刻,比如缓存过期与高并发重合,导致大量请求直打MySQL,进一步加剧DB负担。
挑战四:限流降级机制缺失
没有有效的限流策略,系统一旦被打垮就只能重启或回滚,缺乏容错能力。
这些问题促使我们必须重新审视整个系统的架构,并引入分布式设计思想。
我们的解决方案:分层设计 + 分布式演进

为了应对高并发挑战,我们的系统整体采用了分层架构,从客户端、接入层、应用层、存储层逐步优化。下面我重点讲几个关键点:
一、异步化 + 消息队列解耦(Kafka)
针对库存强一致的需求,我们没有选择直接更新数据库,而是通过消息队列将秒杀请求异步化处理。
流程如下:
用户下单 → 写入消息队列 → 独立消费端处理库存、订单等逻辑
这样做的好处是:
- 解除了业务逻辑间的强依赖
- 提高了系统的吞吐量
- 为后续扩容提供了灵活性
我们选用了Kafka,因为它具备高吞吐、持久化能力强的特点,而且天然支持横向扩展。
代码片段参考:
// 发送消息到 Kafka
kafkaTemplate.send("seckill_order", JSON.toJSONString(orderRequest));
// 消费端监听处理
@KafkaListener(topics = "seckill_order")
public void processOrder(String message) {
SeckillOrder order = JSON.parseObject(message, SeckillOrder.class);
// 先查Redis缓存是否还有库存
Long stock = redisTemplate.opsForValue().get("stock:" + order.getProductId());
if (stock == null || stock <= 0) {
return;
}
// 扣库存 + 写入数据库
productService.reduceStock(order.getProductId());
orderService.createOrder(order);
}
当然这只是个简化版本,生产环境下会更复杂一些,包括失败重试、幂等性控制等。
二、本地缓存 + Redis多级缓存设计
为了避免Redis宕机或缓存失效导致的数据库冲击,我们采用“本地缓存+远程缓存” 的组合方式。
例如用Caffeine作为本地缓存,Redis作为全局共享缓存,两者形成互补。
结构如下:
请求进来 → 查本地缓存 → 命中则返回
↓
未命中 → 查Redis缓存 → 命中返回
↓
未命中 → 查数据库(并回填至两级缓存)
这样的缓存结构能显著降低数据库压力,同时提高访问速度。
配置示例:
caffeine:
spec: maximumSize=500, expireAfterWrite=5m
redis:
host: redis-cluster.prod
timeout: 3000ms
Java 示例:
// 查询商品信息
public Product getProductDetail(Long productId) {
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
String cacheKey = "product:" + productId;
String json = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNotEmpty(json)) {
product = JSON.parseObject(json, Product.class);
localCache.put(productId, product); // 回种本地缓存
return product;
}
// 最终落到数据库查询
product = productDao.selectById(productId);
if (product != null) {
String serialized = JSON.toJSONString(product);
redisTemplate.opsForValue().set(cacheKey, serialized, 5, TimeUnit.MINUTES);
localCache.put(productId, product);
}
return product;
}
另外还加了空值缓存防止缓存穿透,TTL随机化避免缓存雪崩。
三、熔断限流设计(Sentinel / Hystrix)
为了防止突发流量压垮系统,我们使用了阿里开源的 Sentinel 来进行限流和熔断管理。
配置样例:
sentinel:
transport:
dashboard: sentinel-dashboard.prod:8080 # 控制台地址
datasource:
ds1:
nacos:
server-addr: nacos.prod:8848
dataId: ${spring.application.name}-flow-rules
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
效果展示:我们在控制台上可以动态设置 QPS 限制、线程数限制、异常比率熔断规则等。
举个例子,我们设置了对 seckill 接口的每秒最多允许 2000 次调用,超过部分自动拒绝,避免后端雪崩。
四、分布式锁控制库存(Redis Lua 脚本)
库存扣减是个经典的并发问题。为了避免多个线程/节点同时修改数据库导致超卖,我们使用了 Redis + Lua 的方式实现分布式锁:
-- Lua脚本
local key = KEYS[1]
local decrement = tonumber(ARGV[1])
local current_stock = redis.call('GET', key)
if current_stock == false then
return -1 -- 不存在该商品
end
current_stock = tonumber(current_stock)
if current_stock < decrement then
return 0 -- 库存不足
end
return redis.call('DECRBY', key, decrement)
Java 调用方式:
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptText(luaScript);
Long result = redisTemplate.execute(script, Arrays.asList("stock:" + productId), 1);
if (result == null || result < 0) {
// 抢购失败
} else {
// 成功下单
}
这样可以保证原子性操作,有效防止超卖问题。
实践中的坑和解决方法
坑1:Redis内存暴增
由于一开始没有限制缓存数量,导致Redis占用内存持续上涨,最终OOM。
解决办法:
- 设置最大内存限制
- 使用淘汰策略(allkeys-lru)
- 定期清理无效缓存
- 加监控告警
坑2:Kafka消息堆积
初期消费端处理效率低,导致大量消息积压在Kafka中,延迟越来越高。
解决办法:
- 增加消费者实例
- 优化消费逻辑(拆分耗时操作)
- 异步落库
- 监控offset lag指标
坑3:数据库索引设计不合理
在并发下单过程中,发现有SQL频繁扫描全表,导致CPU飙红。
解决办法:
- 对高频字段建立联合索引
- 使用Explain分析执行计划
- 对长事务进行优化
坑4:压测工具没真实模拟场景
压测时只用了简单GET请求,结果上线后发现POST请求比GET慢很多。
解决办法:
- 使用JMeter/LoadRunner真实模拟业务逻辑
- 复用登录Cookie、token
- 动态参数处理
效果总结:上线后稳定运行一年
经过这次重构,新系统的性能有了质的飞跃:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| TPS | ~300 | 3500+ |
| 平均响应时间 | ~600ms | ~80ms |
| DB CPU峰值 | >90% | ~30% |
| 秒杀成功率 | ~70% | ~95% |
更重要的是,系统稳定性大幅提升,活动期间再没有出现大规模故障,运维同学的夜终于睡得踏实了 😅。
我的经验建议:给同行的几点忠告
别盲目追求技术炫技,解决问题才是第一位
很多人一提到高并发就想上MQTT、RocketMQ、gRPC什么的,但其实很多时候先做好基础架构的优化反而收益更大。性能优化不要等到上线前才来做
性能问题应该贯穿整个开发周期,前期压测不到位,后期花再多人力也救不回来。监控真的很重要!日志要清晰,报警要及时
不管你用Prometheus还是Zabbix,都要确保能第一时间发现问题。保持敬畏之心,系统永远不够健壮
即使现在做得再好,也不能松懈。每年双11我们都会演练、压测、优化,才能稳住局面。多借鉴社区经验,但也别照搬
很多方案适合别人不一定适合自己,最好是在自己业务场景的基础上灵活变通。
结语:高并发之路没有终点
这篇文章写了近4000字,内容不算太轻松,但我希望你们能从中感受到一点实战的气息 —— 就像我在深夜调试Redis配置时的那种焦灼;像看到监控大盘恢复正常时的那一点成就感。
高并发系统设计从来不是纸上谈兵的事,它是无数开发者在实践中不断摸索出来的智慧结晶。如果你也在走这条路,不妨坚持下去,因为你不知道下一个凌晨三点,你会遇到怎样的一次突破。
最后,也希望这篇文章能成为你在高并发路上的一个小灯塔,愿你在风雨中也能稳步前行 🚀
如果有任何疑问或者想交流探讨的,欢迎留言或者私信我~

评论 0