高并发系统设计:从理论到实践
作者:某985大三狗,秋招备战中,边听Lo-fi beats边敲代码的重度患者。在组里干了快两年,代码洁癖晚期,看到屎山就手痒。今天这篇不是教科书,是我被现实毒打后吐出来的血泪经验。
去年双11前两周,我正躺在宿舍床上刷LeetCode,突然接到组长电话:“小张,你那个商品详情页接口,QPS刚被压测打爆了,现在前端天天催,运营说大促挂了要背锅,PM已经在会议室拍桌子了。”
我:???
那一刻,我真的想砸电脑。明明本地跑得好好的,怎么一上测试环境就崩?更离谱的是,运维甩给我一行日志:
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3000ms.
好家伙,连接池都炸了。而距离上线只剩11天。
事情是怎么走到这一步的?
我们组做的其实是个电商中间件产品,给公司内部多个业务线提供统一的商品展示能力。前端是React + 微前端,后端SpringBoot单体架构(别笑,很多公司还在用),数据库MySQL + Redis缓存。
问题出在流量预估严重失真。产品经理拍脑袋说“这次活动最多5k QPS”,结果灰度放量第一天就冲到了2w+。更要命的是,我们的接口里居然有三次串行DB查询——查商品基础信息、查库存、查促销规则,每个都走主库,没加任何缓存兜底。
前端同学直接在群里@我:“兄弟,用户点一下页面转圈3秒,再点直接502,运营已经在老板面前哭诉了……”
行吧,背锅侠就是我了。
第一步:稳住别浪,先做熔断降级
高并发的第一课不是优化,是别让系统雪崩。
我立刻在SpringBoot里集成Sentinel(比Hystrix轻量,配置也简单)。核心思想就一条:快速失败,保护系统。
// 商品服务入口处加流控注解
@SentinelResource(value = "productDetail",
blockHandler = "handleProductDetailBlock",
fallback = "fallbackProductDetail")
public ProductVO getProductDetail(Long skuId) {
// 原有业务逻辑
}
同时,和前端约定了降级策略:
- 如果缓存命中但DB异常,返回“库存未知”;
- 如果Redis也挂了,直接返回静态兜底数据(提前从DB dump一份JSON放在Nginx);
- 接口超时一律设为800ms,超过直接fail fast。
这一招立竿见影。第二天压测,即使DB扛不住,前端至少能展示个“商品信息加载中…稍后再试”,不至于白屏或者502。运营终于闭嘴了。
第二步:缓存,缓存,还是缓存!
我承认,之前的设计太理想主义了。以为“读多写少”就不用缓存,结果被打脸打得啪啪响。
多级缓存架构
我们现在用的是 本地缓存(Caffeine) + Redis + DB 三级结构:
请求 → Caffeine(毫秒级) → Redis(网络RTT ~1ms) → MySQL(~10ms)
关键点在于缓存穿透/击穿/雪崩的防御:
- 穿透:查不存在的skuId。解决方案:布隆过滤器 + 空值缓存(带短TTL)
- 击驰:热点商品缓存过期瞬间大量请求打到DB。解决方案:逻辑过期 + 后台异步刷新
- 雪崩:大量key同时失效。解决方案:随机TTL偏移
// 示例:带逻辑过期的缓存读取
public ProductVO getWithLogicalExpire(Long skuId) {
String key = "product:" + skuId;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
RedisData redisData = JSON.parseObject(json, RedisData.class);
ProductVO product = redisData.getData(ProductVO.class);
// 判断是否逻辑过期
if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
return product; // 未过期,直接返回
}
// 已过期,开启后台线程刷新(只允许一个线程刷新)
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
this.saveProductToRedis(skuId, 20 * 60); // 重建缓存
} catch (Exception e) {
log.error("缓存重建失败", e);
}
});
return product; // 先返回旧数据
}
// 缓存未命中,查DB并回填
return queryAndCache(skuId);
}
注:
CACHE_REBUILD_EXECUTOR是一个单线程线程池,避免并发重建。
第三步:数据库扛不住?那就别让它扛
我们的MySQL主库在高峰期CPU飙到90%,慢查询日志里全是 SELECT * FROM product_info WHERE sku_id = ?。
读写分离 + 分库分表(轻量版)
由于时间紧,没上ShardingSphere这种重型武器,而是用 MyBatis-Plus + 多数据源 实现读写分离:
spring:
datasource:
master:
url: jdbc:mysql://master-db:3306/product
slave:
url: jdbc:mysql://slave-db:3306/product
然后自定义注解路由:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
AOP切面判断,带 @ReadOnly 的走从库。商品详情这种纯读接口全标上,主库压力瞬间下降40%。
索引优化:别再 SELECT * 了!
我把所有接口的SQL都捞出来review了一遍,发现几个致命问题:
- 没有为
sku_id加唯一索引(虽然它是主键,但有些表用的是自增id,sku_id只是普通字段) - 大量
SELECT *,其实前端只需要5个字段 - 连表查询没限制,一次查出上千条记录
改完后,单次查询从12ms降到1.8ms。有时候性能瓶颈不在架构,在SQL本身。
第四步:和前端、运营对齐“体验优先级”
技术人容易陷入“我要做到完美”的陷阱,但高并发场景下,用户体验 > 数据强一致。
我们和前端、产品开了个紧急会,达成共识:
| 场景 | 技术方案 | 用户感知 |
|---|---|---|
| 库存显示 | 最终一致性(MQ异步更新缓存) | “仅剩XX件”可能延迟1-2秒 |
| 价格计算 | 缓存促销规则,每5分钟刷新 | 大促期间价格不变,可接受 |
| 商品图片 | CDN + 静态资源预加载 | 首屏秒开 |
甚至运营同意:大促期间关闭“实时销量”展示。因为那个功能每次都要count(*),简直是DB杀手。
第五步:压测!监控!复盘!
光改代码不够,必须验证。
我们用 JMeter + Grafana + Prometheus 搭了一套简易监控:
- JVM内存、GC频率
- Redis命中率(目标 > 98%)
- 接口P99延迟(目标 < 500ms)
- DB连接池使用率
压测策略也很野:模拟真实用户行为,不是单纯发请求。比如:
- 70% 用户只看商品页不下单
- 20% 加购但不支付
- 10% 完成下单
结果发现,加购接口的Redis写操作成了新瓶颈。于是把“购物车”从Redis List改成 Hash 结构,内存占用降了60%,TPS翻倍。
效果如何?
双11当天,峰值QPS达到3.2w,系统稳如老狗:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| P99延迟 | 2800ms | 320ms |
| DB CPU | 92% | 45% |
| 错误率 | 12% | 0.03% |
| 缓存命中率 | 76% | 98.7% |
前端同学请我喝了杯瑞幸,PM在周会上夸我“有大局观”(虽然我知道他根本不懂Sentinel是啥)。
给秋招同学的真心话
作为正在投简历的大三狗,我深刻体会到:面试官不关心你会多少框架,只关心你有没有解决过真实问题。
高并发不是背八股文,而是一套系统性思维:
- 先保命(熔断降级)
- 再提速(缓存+异步)
- 最后抠细节(SQL、索引、连接池)
我在简历里写“主导高并发商品服务优化,支撑3w+ QPS”,比写“熟悉SpringCloud”有用十倍。
顺便吐槽一句:千万别信产品经理说的“流量不大”。他们眼中的“不大”,可能是你服务器的十倍。
最后的小Tips
- 连接池配置别照抄网上的:HikariCP的
maximumPoolSize要根据DB的max_connections和应用实例数计算,公式:(DB_max_conn - 保留连接) / 实例数 - 日志别打太多:高并发下logger.info() 都可能成为瓶颈,用异步日志(logback async)
- 上线前必做:混沌工程!哪怕只是手动 kill 一个Redis节点,看系统会不会雪崩
好了,写完这篇已经凌晨两点,耳机里的Lo-fi还在循环。明天还要改另一个“简单需求”——据说只要加个按钮就行。
唉,程序员的命,都是产品经理给的(狗头保命)。
共勉。

评论 0