高并发系统设计:从理论到实践 —— 一位后端工程师的实战经验分享
引言:一次双十一压测让我意识到高并发不是玄学

记得刚成为项目负责人那年,公司准备在双十一期间上线一个限时秒杀活动。作为核心接口之一的“下单”服务,承载着整个活动中最核心的交易逻辑。我原本信心满满地部署了一套基于Spring Boot + MyBatis的微服务架构,然而,在预演压力测试中,系统竟然在200并发时就开始出现大面积超时和线程阻塞。
那次失败让我彻底清醒:高并发系统远不只是简单地加几个线程或者堆几台服务器。它涉及到方方面面的设计与权衡,是技术深度与业务理解的综合考验。
后来我花了几个月时间从架构调整、缓存策略、数据库优化等多个层面重构了这个系统,最终在实际双十一当天扛住了3500+ QPS的瞬时峰值,成功率保持在99.6%以上。这篇文章,我会以那次项目为背景,聊聊我在高并发系统设计中的真实经历与思考。
项目背景:一场限时秒杀背后的挑战

我们这次项目的背景是一个电商限时秒杀平台的下单服务模块,核心功能包括:
- 商品库存预扣减
- 用户限购控制
- 下单流水生成
- 支付状态更新(异步处理)
关键业务指标:
| 指标 | 目标值 |
|---|---|
| 峰值QPS | ≥3000 |
| 平均响应时间 | ≤100ms |
| 接口成功率 | ≥99.5% |
当时我们的服务部署结构如下:
Nginx + Keepalived (软负载)
├── Gateway -> Spring Cloud Gateway
│ └── 认证 & 权限校验 & 熔断降级
└── 下单服务集群 (4台 ECS, Spring Boot 2.x)
├── MyBatis + MySQL(主从)
└── Redis(集群)
整体看起来还算标准的微服务架构,但在压测环节暴露了多个致命问题。
我们遇到的真实挑战

1. MySQL连接池被打爆,DB成为瓶颈
在JMeter模拟300并发用户请求下单时,数据库连接数迅速飙升至最大值(max_connections默认150),后续所有SQL都进入等待状态,进而引发线程池打满。
2. 热点商品锁竞争严重,大量事务回滚
由于使用的是UPDATE stock SET count = count - 1 WHERE goods_id = ? AND count > 0的经典方式,导致同一商品ID在并发情况下产生大量行锁争用。
3. Redis分布式锁性能不够理想,增加延迟
我们最初采用了Redlock算法实现的分布式锁,用来防止超卖。但实际场景下发现Redlock在网络不稳定或节点故障时,不仅性能不佳,还容易导致重复扣减。
4. 线程阻塞严重,TPS上不去
服务层没有做异步化处理,所有操作都是同步执行,主线程长时间等待DB/Redis响应,无法充分利用CPU资源。
5. 缺乏熔断与降级机制
当某个依赖服务(如库存服务)异常时,整个下单流程都会阻塞,用户体验极差,甚至导致雪崩效应。
解决方案:逐步打磨出一套轻量稳定的高并发系统

面对这些问题,我们并没有盲目换架构或引入新组件,而是从最根本的问题出发,逐个击破。
一、流量前置拦截:在接入层做好分流和限流
我们在Nginx层做了两件事情:
IP限流:防止刷单或恶意攻击
http { limit_req_zone $binary_remote_addr zone=one:10m rate=50r/s; server { location /order/create { limit_req zone=one burst=10 nodelay; proxy_pass http://gateway; } } }根据商品ID hash分发请求:将同一个商品的请求始终分配到同一台后端实例,减少跨实例竞争。
upstream backend { hash $request_header_goodsid consistent; server instance1 weight=1; server instance2 weight=1; keepalive 32; }
这样做不仅提高了命中本地缓存的概率,还能降低Redis分布式锁的复杂度。
二、缓存与异步:把耗时动作尽可能挪出主路径
使用多级缓存体系
我们将库存数据从数据库提前加载进Redis,并通过定时任务做预热:
// 定时任务每日凌晨加载库存到Redis
@Scheduled(cron = "0 0 3 * * ?")
public void preheatStock() {
List<Goods> goodsList = goodsMapper.selectAll();
for (Goods goods : goodsList) {
String key = "stock:" + goods.getId();
redisTemplate.opsForValue().setIfAbsent(key, goods.getStock(), 1, TimeUnit.DAYS);
}
}
下单时优先扣减Redis,仅在库存充足时再提交数据库更新。
异步落库 + 消费补偿
我们将真正写订单的动作异步化,通过消息队列解耦:
// 异步下单流程
public String createOrder(OrderDTO dto) {
// 1. 扣减Redis库存
Long decrResult = redisService.decr("stock:" + dto.getGoodsId());
if (decrResult < 0) {
throw new StockNotEnoughException();
}
// 2. 异步写入MQ
orderProducer.sendAsyncOrder(dto);
// 返回预下单ID供前端查询
return UUID.randomUUID().toString();
}
然后消费者监听订单消息并持久化到数据库:
@KafkaListener(topics = "order_create")
public void processOrder(OrderDTO dto) {
try {
orderService.saveOrder(dto);
} catch (Exception e) {
log.error("订单入库失败,重试中...", e);
retryQueue.offer(dto); // 加入重试队列
}
}
这样既保证了实时性,又避免了数据库瓶颈。
三、数据库设计与调优:稳住核心存储
合理分表 + 冷热分离
我们按照用户ID进行水平切分,每个用户的订单被均匀分布到不同的子表中。同时,对历史订单进行冷热分离,单独迁移至另外一张表用于统计分析。
分布式事务改用TCC模式
为了替代原生的分布式事务,我们引入了TCC(Try-Confirm-Cancel)模式:
- Try阶段:冻结库存、锁定用户额度
- Confirm阶段:正式扣减库存、生成订单
- Cancel阶段:回滚冻结资源
这种方式虽然增加了代码复杂度,但极大地提升了系统的扩展性和容错能力。
四、真正的“分布式锁”,不需要复杂的实现
经过几次尝试,我们放弃了Redlock和Redisson的分布式锁实现,最终选择了基于Redis Lua脚本的原子扣减来实现库存控制:
public boolean deductStock(String goodsId) {
String script = "if redis.call('get', KEYS[1]) >= tonumber(ARGV[1]) then return redis.call('decrby', KEYS[1], ARGV[1]) else return 0 end";
Object result = redisTemplate.execute(script, Arrays.asList("stock:" + goodsId), "1");
return ((Long) result) > 0;
}
这段Lua脚本保证了“检查+扣减”的原子性,比任何第三方封装都更高效、更安全。
五、熔断降级保障可用性:Hystrix?不,现在我们用Resilience4j
考虑到Spring Cloud Netflix Hystrix已经停止维护,我们切换到了社区活跃的Resilience4j,并结合Gateway实现了优雅的熔断和降级:
resilience4j.circuitbreaker:
instances:
orderService:
failure-rate-threshold: 50
wait-duration-in-open-state: 5s
ring-buffer-size-in-closed-state: 10
ring-buffer-size-in-open-state: 2
并在Feign Client中配置 fallback:
@FeignClient(name = "inventory-service", fallback = InventoryFallback.class)
public interface InventoryClient {
@GetMapping("/check_stock/{goodsId}")
Boolean checkStock(@PathVariable String goodsId);
}
踩坑经验:那些深夜debug的经历教会我的事
Redis连接泄漏导致服务不可用 早期未正确关闭Redis连接,导致连接池资源被吃光。后来统一采用try-with-resources的方式,并设置合理的超时时间:
try (Jedis jedis = pool.getResource()) { ... }Nginx长连接配置不当引发连接数过高 刚开始没加keepalive参数,每次请求都重新创建TCP连接,导致TIME_WAIT激增。添加
proxy_http_version 1.1; proxy_set_header Connection "";和 keepalive 设置后缓解。日志打太多拖垮磁盘IO 日志级别一开始设成了DEBUG,每秒写几千条日志,结果导致磁盘满载。后面改成INFO为主,关键路径保留TRACE即可。
线程池配置不合理引发拒绝策略 最初Tomcat最大线程数设置太小,且未启用队列。后来合理配置线程池,并监控RejectedExecutionException。
实施后的效果与收益
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 最大QPS | 200 | 3800 |
| 平均RT | ~600ms | ~80ms |
| DB连接数 | 200+ | 30~40 |
| 接口成功率 | 85% | 99.7% |
| 线程利用率 | 不足40% | 提升至80% |
而且整个系统在双十一当天表现稳定,即使个别节点挂掉也能自动恢复,几乎没有影响业务。
给后端开发者的几点建议

别迷信架构,先看清业务本质 没有银弹,高并发一定是围绕你的业务特点去优化,而不是照搬别人的“秒杀架构”。
缓存不是万能的,但确实是最重要的手段之一 一级缓存命中率提上去,性能会提升一个数量级。多级缓存要组合使用,注意失效一致性。
异步思维贯穿始终 任何可以异步处理的流程都尽量异步,哪怕只是延迟几十毫秒。不要让主线程等你干活!
重视监控和报警机制 Prometheus + Grafana是我们的好朋友。一定要在生产环境搭建好监控大盘,早发现问题比事后救火重要得多。
做好容量评估和压测准备 上线前一定要进行压测,摸清楚各个组件的吞吐极限,预留扩容空间,尤其在做促销前。
结语:高并发不是终点,而是日常修炼的一部分
回顾整个过程,我觉得最大的收获不是技术本身,而是学会了如何在复杂系统中寻找平衡点:既要保证高可用,又要兼顾业务需求;既不能过早优化,也不能临阵磨枪。
如果你也正处在类似场景中,不妨从以下几个点着手:
- 梳理核心链路的瓶颈位置
- 先做压测,不要猜测性能瓶颈
- 小步迭代,持续改进
- 把错误当成学习的机会
高并发这条路不好走,但我们终将在风雨中成长。
作者是一位拥有五年一线后端开发经验的Java程序员,热爱架构设计与性能优化,目前专注于电商系统高并发方向。欢迎交流微信:XXXXXXX,一起成长!

评论 0