高并发系统设计:从理论到实践
引言:为什么高并发值得认真对待?

我第一次真正接触“高并发”这个词是在我工作的第三年,那时候我们公司正经历一次关键的业务转型,从原本的内部系统支撑转向对外提供 SaaS 服务。用户量迅速从几千增长到了几十万,突然之间,那些以前跑得好好的接口开始变得缓慢甚至崩溃。
最让我印象深刻的一次是某个促销活动上线之后,数据库直接被打崩了,整个平台几乎瘫痪。当时的场面很混乱,运维同事在疯狂重启服务,我们在代码层一遍又一遍地查问题,最终才意识到,这不是一个简单的性能优化问题,而是一个整体架构级别的挑战。
从那以后,我就开始系统性地学习高并发系统的构建和优化。这篇文章会基于我在多个项目中踩过的坑、趟过的水,来分享一些关于高并发系统设计的实战经验。
项目背景:电商交易系统改造升级

我们的主要产品是一个 B2B 电商平台,支持供应商入驻、商品管理、订单交易等功能。随着业务发展,原来的系统逐渐暴露出性能瓶颈:
- 高峰期请求量激增,单日 PV 达到百万级别
- 订单生成接口响应延迟严重,经常超过 500ms
- 数据库压力巨大,出现锁等待、慢查询等问题
- 库存服务频繁超时,影响下单成功率
这些症状背后其实暴露了一个老系统普遍存在的通病:架构过于集中化,缺少缓存体系,数据库没有分库分表,也没有限流熔断机制。
于是我们启动了一个为期三个月的系统重构计划,目标很明确:打造一个能支撑千万级 UV 的稳定交易系统。
遇到的问题和挑战:高并发下的“多米诺骨牌”

1. 接口并发瓶颈突显
最初的版本中,所有接口都部署在一个 Spring Boot 应用里,前端请求全部打到这一个服务节点上。结果在压测中发现:
ab -n 5000 -c 100 http://api.example.com/order/create
当并发数达到 100 时,TPS 只有不到 100,CPU 使用率飙升到了 95%,接口响应时间飙到 3s 左右。明显存在线程阻塞或者资源竞争。
2. 数据库连接池吃紧
订单创建过程中需要访问多个表,涉及库存扣减、订单写入、优惠计算等操作。每次请求都要获取数据库连接,导致连接池很快耗尽:
Caused by: java.sql.SQLNonTransientConnectionException: No connection available from pool.
3. 缓存未有效利用
虽然我们加了 Redis 缓存商品信息和库存数据,但因为使用方式不当(如串行批量读取),反而成了新的瓶颈。比如查询一个包含 20 个商品的购物车时,竟然是一个个去 Redis 查,完全没有发挥缓存的优势。
解决方案:分层架构 + 核心能力增强
针对上述问题,我们逐步采取了以下措施:
一、拆分服务 + 增加横向扩展能力
我们将核心服务解耦出来,形成独立微服务:
| 服务模块 | 功能职责 |
|---|---|
| order-service | 处理订单生命周期 |
| inventory-srv | 负责库存读写与预占 |
| product-srv | 商品管理与基本信息查询 |
| user-srv | 用户信息及积分、收货地址 |
每个服务都部署了多个实例,并配置 Nginx 做负载均衡:
upstream order_servers {
least_conn;
server 192.168.0.10:8080;
server 192.168.0.11:8080;
}
这样不仅提高了系统可用性,也方便后续做灰度发布。
二、引入缓存策略:Redis + 本地缓存双层结构
我们在 Redis 中缓存热点数据,同时引入 Guava Cache 做本地缓存:
// 示例:本地缓存商品详情
LoadingCache<Long, ProductDetail> productCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(this::loadProductFromDB);
对于库存这种变化频率较低、一致性要求高的数据,采用 Redis 持久化 + 预留机制:
-- Redis Lua 脚本处理库存扣减
local key = KEYS[1]
local num = tonumber(ARGV[1])
local stock = redis.call('GET', key)
if not stock then
return -1
end
if tonumber(stock) < num then
return 0
end
redis.call('DECRBY', key, num)
return 1
三、数据库拆分与分库分表
原来的所有业务都集中在一张订单表中,订单数突破千万后,查询非常慢。我们采用按用户 ID 进行水平分表:
- 分片键:user_id % 4,分成四个物理表 order_0 ~ order_3
- 查询逻辑封装在 MyBatis Interceptor 中自动路由
同时做了主从复制,把读写流量分离:
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://mysql-master:3306/shop
username: root
password: xxx
slave1:
url: jdbc:mysql://mysql-slave1:3306/shop
username: root
password: xxx
四、引入限流与熔断机制

为了防止雪崩效应,我们在网关层接入了 Sentinel:
@Bean
public GlobalFilter sentinelGlobalFilter() {
return (exchange, chain) -> {
try {
Entry entry = SphU.entry("order_create_api");
return chain.filter(exchange).doOnTerminate(entry::exit);
} catch (BlockException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return response.setComplete();
}
};
}

Sentinel dashboard 设置 QPS=2000,在接口过载时自动降级。
代码实践:关键点展示
异步下单 + RocketMQ 解耦
为了减少同步调用的等待时间,我们将部分非实时操作异步化:
// 下单流程核心逻辑
OrderDTO createOrder(CreateOrderReq req) {
OrderDTO order = prepareOrder(req);
// 同步:库存扣减
reduceInventory(req.getItems());
// 异步:消息通知、积分增加、风控检查
rocketMQTemplate.convertAndSend("ORDER_CREATED", order);
return order;
}
消费者监听订单创建事件,进行后续处理:
@RocketMQMessageListener(topic = "ORDER_CREATED", consumerGroup = "order-consumer")
public class OrderCreatedConsumer implements RocketMQListener<OrderDTO> {
@Override
public void onMessage(OrderDTO order) {
sendNotification(order.getUserId(), "您的订单已创建");
addUserPoint(order.getUserId(), order.getPoint());
riskControlService.check(order);
}
}
使用线程池实现并行调用
例如:在结算页加载多个子服务时,并行发起请求:
@Autowired
private ProductClient productClient;
@Autowired
private CartClient cartClient;
public OrderCheckout checkout(long userId) {
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<ProductInfo> productFuture = executor.submit(() -> productClient.getProduct(userId));
Future<CartItems> cartFuture = executor.submit(() -> cartClient.getCart(userId));
// 合并结果
OrderCheckout checkout = new OrderCheckout();
checkout.setProduct(productFuture.get());
checkout.setCart(cartFuture.get());
return checkout;
}
当然这里也可以使用 CompletableFuture 来更优雅地处理组合式异步调用。
踩坑经验:那些被教训打醒的日子
1. 忽视数据库索引优化的惨痛代价
有一次我们新增了一个订单状态变更的日志功能,表建好了但是没加索引。过了几天,一个 SQL 查询卡住整个数据库,导致全站瘫痪。最后才发现是因为有一个 WHERE order_id = ? 的查询没走索引!
所以后来我们立下规矩:
凡是涉及到 WHERE 或 JOIN 的字段,必须添加索引;每张新表上线前必须进行 Explain 审核
2. Redis 穿透、击穿、雪崩三兄弟
一开始我们对缓存异常情况没有足够的防护意识。某次大促期间,大量缓存失效,导致数据库瞬间承受几倍的压力。
后面我们采用了以下几个方案来应对:
- 布隆过滤器防穿透
- 互斥锁更新缓存防击穿
- 设置随机过期时间防雪崩
// 防击穿示例
String getFromCache(String key) {
String value = redis.get(key);
if (value == null) {
synchronized (this) {
value = redis.get(key);
if (value == null) {
value = loadFromDB();
redis.setex(key, randomExpireTime(), value);
}
}
}
return value;
}
3. 线程池配置错误引起的“伪高并发”
我们曾为异步任务配置了一个无界队列的线程池,结果在突发请求到来时,任务越堆越多,内存被撑满,GC 疯狂回收,服务器彻底挂掉。
后来改为有界队列 + 拒绝策略:
new ThreadPoolTaskExecutor()
.setCorePoolSize(10)
.setMaxPoolSize(20)
.setQueueCapacity(500)
.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
效果总结:改造后的收益
经过这次大规模重构,系统有了明显提升:
| 指标 | 改造前 | 改造后 | 提升幅度 |
|---|---|---|---|
| 并发能力 | 500 TPS | 7500 TPS | 15X |
| 订单接口平均响应时间 | 800ms | 120ms | ✅降低85% |
| DB QPS | 1500 | 400 | ✅下降73% |
| 故障恢复时间 | 数小时 | 10分钟内 | ✅大幅缩短 |
| 系统扩展性 | 单体难以扩容 | 微服务易扩缩容 | ✅增强显著 |
最重要的是,现在遇到高流量冲击时,我们已经有一套完整的应急手段和监控报警体系,而不是像过去那样临时抱佛脚。
我的经验和建议
给新手的几个忠告:
不要一开始就追求极致性能
很多人一上来就要分库分表、搞集群、上缓存,但其实很多业务初期完全没必要。先把架构做清晰,技术债后期更容易还。关注链路追踪与监控体系建设
真正的高并发系统不是靠肉眼判断性能瓶颈的,你必须得能快速定位哪个环节出问题。推荐使用 SkyWalking 或 Zipkin 做分布式追踪。学会使用压测工具摸清系统边界
Apache Bench、JMeter、Locust 都要掌握,只有你知道自己的系统上限在哪,才敢谈扛压能力。别忽视线上日志和报警机制
线上出问题时往往是你最慌的时候。提前配好 Prometheus + Grafana 监控指标,配合企业微信/钉钉报警,才能做到第一时间响应。
当前趋势中的参考方向:
- 云原生架构:K8s + Service Mesh 成为标配,弹性伸缩更容易。
- Serverless 架构:阿里云函数计算、AWS Lambda 在部分场景可节省资源成本。
- 多活架构:异地多活正在成为大型企业的标准配置。
- AI辅助诊断:部分团队已经在尝试用 AI 来分析日志、预测异常。
结语:技术是一场修行
回想这几年,从刚入行时只会埋头改 bug,到现在可以主导整个系统的设计与优化,最大的成长不是学会了更多工具,而是建立了一种系统性的思考方式。
高并发从来不是一个单一的技术点,它考验着你对网络、操作系统、数据库、中间件、编程语言乃至组织协作的理解和把控。它就像一场马拉松,比拼的不是爆发力,而是耐力和韧性。
如果你正站在这个路口上,请相信一句话:
每一个高并发系统,都是由无数个深夜和一次次失败打磨出来的。
愿你在路上不惧风浪,勇往直前。
如果喜欢这篇内容,欢迎点赞、转发,也欢迎留言交流你的高并发实战经验~

评论 0