高并发系统设计:一次真实的“扛住百万请求”的旅程
开篇:为什么我会想写这篇技术文章?

记得几年前,我刚加入一家快速发展的电商平台公司。当时我们正在筹备一年一度的“双十一大促”,作为一个后端架构师的新手,我第一次真正感受到高并发系统的威力。
那天晚上,我们在压测环境模拟每秒10万次请求时,数据库直接崩溃了三次。Redis被打满,接口延迟飙升到1.5秒以上,服务之间频繁超时,整个系统像一锅煮沸的开水——热闹但失控。
这场灾难让我深刻认识到:纸上谈兵的架构永远撑不起真正的流量洪峰,只有在实践中不断试错、打磨,才能构建出可靠的高并发系统。
所以今天我想和你分享这段真实经历,讲一讲我是怎么从一个只会用Spring Boot搭个简单API的开发,一步步设计出能够承载百万级QPS的系统的全过程。
问题描述:百万级别的用户请求,系统随时可能崩盘

我们当时负责的是用户登录和订单提交两个核心链路。这两部分在大促当天会集中爆发访问压力:
- 登录模块:单点登录(SSO),用户访问量集中在开售前30分钟
- 订单模块:抢购开始后的前几分钟内,会有大量用户同时下单
当时的系统存在几个严重问题:
1. 数据库瓶颈严重
- 所有数据都直连MySQL主库,没有读写分离
- 用户信息查询全表扫描,执行计划糟糕
- 没有做连接池管理,应用频繁创建数据库连接
2. 系统调用链过长
- 多层业务校验和服务调用串行化
- 超时时间设置混乱,导致雪崩效应
- 缓存命中率不到40%,大多数请求穿透到了数据库
3. 容灾机制基本缺失
- 没有服务降级策略,出现异常直接抛错误给前端
- 单节点部署,某台服务器宕机直接影响全局可用性
- 没有监控告警,只能靠人工发现故障
那段时间,每天都在改代码、调参数、看日志,团队加班成常态。最崩溃的一次是预热期间数据库主从同步延迟高达20分钟,导致用户看到的数据严重不一致,投诉电话差点打爆客服。
解决方案:从架构调整到细节优化的全流程改造
面对这些挑战,我们采取了一系列技术和架构层面的措施。下面我就按照实际落地顺序来聊聊。
1. 架构拆分与服务化改造
第一步是对系统进行解耦和模块化:
旧结构:
Client -> Gateway -> OrderService + AuthService -> MySQL
新结构:
Client
├── AuthGateway (登录流程独立)
│ └── AuthCache → AuthDB
└── OrderGateway
├── Cache Layer(本地缓存+Redis)
├── InventoryService(库存微服务)
├── UserService(用户微服务)
└── OrderDB
将原来的大单体拆分成多个微服务模块,通过Gateway统一接入。每个服务都有独立的部署环境和资源配额,避免彼此干扰。
2. 缓存层建设
我们引入了多级缓存体系:
- 客户端缓存:浏览器端设置LocalStorage保存静态数据
- Nginx本地缓存:对于完全静态的内容(如商品详情页)缓存在CDN或Nginx上
- Redis集群:用户身份、优惠券状态等热点数据由Redis承担
- 本地缓存(Caffeine):用于存储频繁访问的小对象,减少跨网络IO
Redis设计关键点:
- 使用二级Key设计:
{business}:{key}方式便于排查 - 合理设置TTL:不同类别的数据生命周期不同
- 对热点Key单独配置,防止穿透、击穿
代码示例(Spring Data Redis):
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, User> redisTemplate;
public UserService(UserRepository repo, RedisTemplate<String, User> redis) {
this.userRepository = repo;
this.redisTemplate = redis;
}
public User getUserById(String userId) {
String key = "user:" + userId;
// 先查缓存
User user = redisTemplate.opsForValue().get(key);
if (user != null) return user;
// 缓存未命中,查数据库
user = userRepository.findById(userId).orElse(null);
// 设置缓存(随机过期时间,防止集体失效)
int expirationTime = (int)(Math.random() * 600 + 300);
if (user != null) {
redisTemplate.opsForValue().set(key, user, expirationTime, TimeUnit.SECONDS);
}
return user;
}
}

3. 异步化与削峰填谷
为了缓解数据库压力,我们将一些非即时性操作异步化:
- 下单动作中库存扣减使用MQ消息队列(Kafka)
- 支付状态更新采用事件驱动方式处理
- 日志记录转为批量入库
这样做的好处是能有效控制系统的吞吐量峰值,提升整体稳定性。
4. 数据库优化
数据库是最后一道防线,我们也做了很多优化工作:
- 建立合理的索引,尤其是组合索引的设计要符合高频查询场景
- 分库分表:根据用户ID哈希分8库,订单按时间分片
- 实施读写分离:写走主库,读走从库(延迟容忍度高的操作可接受几秒同步滞后)
分库配置示例(MyCat中间件):
<schema name="ORDER_DB" checkSQLschema="false" sqlMaxLimit="100">
<table name="orders" dataNode="dn1,dn2,dn3,dn4,dn5,dn6,dn7,dn8"
rule="mod-userid-8"/>
</schema>
5. 接口性能优化
在接口层面我们也做了不少细节优化:
- 统一返回格式,尽量精简字段输出
- 将多个RPC调用合并为一个(Batch API)
- 使用CompletableFuture实现异步编排
比如原本需要3个接口串联调用:
// 同步调用
public OrderInfo getOrderDetailSync(String orderId) {
UserInfo userInfo = userService.getUser(orderId);
Product product = productService.getProduct(orderId);
Coupon coupon = couponService.getCoupon(orderId);
return new OrderInfo(userInfo, product, coupon);
}
// 异步并行调用优化
public OrderInfo getOrderDetailAsync(String orderId) {
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(orderId));
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productService.getProduct(orderId));
CompletableFuture<Coupon> couponFuture = CompletableFuture.supplyAsync(() -> couponService.getCoupon(orderId));
return userFuture.thenCombine(productFuture, (u, p) -> new OrderInfo(u, p))
.thenApply(orderInfo -> {
try {
orderInfo.setCoupon(couponFuture.get());
} catch (Exception e) {
// handle exception
}
return orderInfo;
}).join();
}
踩坑经验:那些深夜调试教会我的事
1. 不合理限流引发连锁反应
一开始我们使用Guava RateLimiter来做限流,但在实际压测中发现它并不是线程安全的,在并发环境下会导致计数不准。后来换成了Sentinel,结合网关实现了更精细的速率控制。
教训:选型不能只看文档,一定要测试!
2. 缓存穿透问题
曾经有个接口因为查询不存在的ID,导致大量请求穿透到MySQL,进而触发慢查询报警。我们后来加了一层布隆过滤器来拦截非法请求,效果非常明显。
代码片段(使用RedisBloom扩展):
BF.ADD itemExistsFilter abc123
BF.EXISTS itemExistsFilter abc123 --> 1
BF.EXISTS itemExistsFilter invalidKey --> 0
3. 服务雪崩的惨痛教训
有一个促销活动当天,第三方配送系统挂掉,我们的服务也因为未设置超时熔断机制而跟着阻塞。这次事故让我们意识到断路器的重要性,之后在所有外部服务调用上都加上了Hystrix熔断逻辑。
代码示例(Resilience4j + Spring Cloud Sleuth):
@CircuitBreaker(name = "delivery-service", fallbackMethod = "fallbackDelivery")
public DeliveryStatus getDeliveryStatus(String orderId) {
return restTemplate.getForObject("/delivery/status?orderId=" + orderId, DeliveryStatus.class);
}
private DeliveryStatus fallbackDelivery(Throwable t) {
return DeliveryStatus.UNKNOWN;
}
效果总结:从十万到百万QPS的突破
经过三个多月的改造和测试,我们的系统最终支撑住了双十一大促的实际流量峰值:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 最大并发量 | 8k QPS | 120k QPS |
| 接口平均响应时间 | 900ms | 120ms |
| 数据库连接数 | 超过1500 | 控制在300以内 |
| 服务可用性 | 98% | 99.99% |
更关键的是:系统具备了弹性扩缩容能力。我们可以根据实时流量动态增加计算资源,这在过去是不可想象的。
经验分享:给你几点真诚的建议
如果你正准备做高并发系统的优化,下面是我亲身经历总结出来的几点实用建议:
1. 架构必须早于需求设计阶段就介入
不要等出了问题再想着补救。在项目初期就要考虑并发问题,哪怕当前需求还没达到很高并发。否则将来重构的成本将是现在的5倍以上。
2. 技术方案要“接地气”
别上来就搞什么Pulsar/Kafka/ElasticSearch全家桶,先想想你的系统真的需要吗?我见过太多堆了很多高端组件却没人会调优的系统。适合的才是最好的。
3. 监控先行,预防为主
搭建一套完整的监控体系比任何临时修复手段更重要。我当时用的是Prometheus + Grafana + ELK这套组合拳,效果很好。提前预警远胜事后补救。
4. 性能优化要“有取舍”
不是每个模块都要追求极致性能,优先优化高频路径。比如我们把支付环节做到毫秒级响应,但退货退款这种低频功能就不值得花那么大力气。
5. 团队协作至关重要
一个人的能力终究有限,特别是遇到复杂分布式问题时,团队的配合和分工特别重要。定期做Code Review和压力测试演练,也是发现问题的好方法。
写在最后:高并发没有银弹,只有不断进化

这些年下来,我越来越觉得:高并发系统设计从来不是一个“完成品”,它更像是一个持续演进的过程。技术在变、业务在变、用户也在变,唯一不变的就是变化本身。
希望这篇文章能为你带来一些启发和思考。也许你现在还在纠结要不要引入Redisson做分布式锁,或者是不是该上Kubernetes集群,没关系,慢慢来。只要坚持站在实际需求的基础上解决问题,终有一天,你会感谢那个不断学习、不怕失败的自己。
如果你也经历过类似的挑战,欢迎留言交流;如果你还在路上,请相信:每一个半夜1点修改配置文件的你,终将换来白天用户流畅体验的笑容 🎉

评论 0