高并发不是玄学,是我被逼出来的实战经验
去年双11前两周,我们组临时接到任务:把一个原本日活几千的小众工具,硬生生扩容到支撑百万级 QPS。产品经理拍着胸脯说“就加个按钮,逻辑不变”,结果我盯着凌晨三点的 Grafana 监控图,看着 CPU 爆表、Redis 拒绝连接、数据库死锁满屏,差点把键盘扔出窗外。
我是谁?一个在北京挤地铁一小时、耳机里永远放着 lo-fi beats 写 Java 的后端码农。试过 GitHub Copilot、CodeWhisperer,甚至拿 Llama 和 Gemini 在本地跑过代码生成实验——但最后还是乖乖切回 Cursor,毕竟它对上下文理解够深,能在我写一半的 CompletableFuture 链式调用里准确补全异常处理逻辑。言归正传,今天不聊工具,聊聊那次差点让我秃头的高并发改造。
一开始,我们都以为加机器就行
项目是个商品秒杀服务。旧架构简单粗暴:Spring Boot + MySQL 单库单表 + 同步 HTTP 接口。平时跑得挺欢,直到运营小哥发了个微博抽奖链接,瞬间涌入 50w 请求。系统当场瘫痪,用户看到的全是“服务器开小差了”。
运维兄弟甩锅:“你这代码太烂,连缓存都没上!”
测试妹子冷笑:“压测报告早就说了扛不住,你们后端当耳旁风?”
我默默喝了口冰美式,心想:行吧,这锅我背,但得背得明白。
高并发不是堆资源,而是削峰、异步、缓存、限流四件套的组合拳。下面说说我怎么一步步把系统从“脆皮鸡”变成“铁布衫”。
第一步:缓存穿透?先给 Redis 加道门神
最开始的问题是缓存穿透——大量请求查不存在的商品 ID,直接打穿到 DB。MySQL 的 InnoDB Buffer Pool 被无效查询冲垮,CPU 打满。
解决方案很简单:布隆过滤器(Bloom Filter)前置拦截。但别急着写代码,先想清楚部署位置。我一开始图省事,在应用层用 Guava 实现,结果发现 JVM 内存暴涨,而且多实例间状态不一致。
后来改成 Redis 原生的 RedisBloom 模块(需要额外加载),或者退而求其次用 SETNX + 短 TTL 标记“已查过但不存在”的 key。后者虽然有点 dirty,但在资源紧张时够用。
// 简化版:用 Redis 标记空值,防止穿透
public Product getProduct(Long id) {
String cacheKey = "product:" + id;
String json = redis.get(cacheKey);
if (json != null) {
return json.equals("null") ? null : JSON.parse(json);
}
// 查 DB
Product product = productDao.findById(id);
if (product == null) {
// 设置短 TTL 的 null 标记
redis.setex(cacheKey, 60, "null"); // 只缓存 60 秒
} else {
redis.setex(cacheKey, 3600, JSON.toJson(product));
}
return product;
}
💡 经验:不要迷信“完美方案”。在 deadline 压顶时,能快速上线且有效的 hack 往往比理论最优解更珍贵。
第二步:同步变异步,消息队列来兜底
秒杀的核心瓶颈其实是库存扣减的一致性。最初我们用数据库行锁(SELECT ... FOR UPDATE),结果高并发下锁竞争激烈,TPS 卡在 200 不到。
怎么办?异步化 + 最终一致性。思路是:用户点击“抢购” → 系统校验资格(库存、用户限购等)→ 发送消息到 Kafka/RocketMQ → 后台消费者真正扣库存。
这里的关键是前置校验必须精准。我们用了三级校验:
- Redis 缓存库存(预热时加载)
- 用户限购计数(用 Redis Hash 存 userId → count)
- 请求合法性(防刷、token 校验)
// 秒杀入口:只做轻量校验,快速响应
@PostMapping("/seckill/{productId}")
public Result seckill(@PathVariable Long productId, @RequestHeader String token) {
if (!validateToken(token)) return fail("非法请求");
// 1. 检查 Redis 库存
Long stock = redis.decr("stock:" + productId);
if (stock < 0) {
redis.incr("stock:" + productId); // 回滚
return fail("手慢了");
}
// 2. 检查用户限购
String userKey = "user_limit:" + getCurrentUserId();
Long count = redis.hincr(userKey, productId.toString(), 1);
if (count > MAX_PER_USER) {
redis.hincr(userKey, productId.toString(), -1);
redis.incr("stock:" + productId);
return fail("超过限购数量");
}
// 3. 发消息,异步扣 DB 库存
mq.send(new SeckillEvent(productId, getCurrentUserId()));
return success("排队中,请稍后查看结果");
}
消费者那边再做最终一致性校验,失败则补偿(比如回滚 Redis 库存)。虽然牺牲了强一致性,但用户体验提升巨大——接口响应从 800ms 降到 30ms。
第三步:限流熔断,别让系统裸奔
光有缓存和异步还不够。如果恶意刷单或突发流量超出预期,系统还是会崩。这时候就得靠限流(Rate Limiting)和熔断(Circuit Breaker)。
我们选了 Sentinel(阿里开源),而不是 Hystrix,因为它的控制台更直观,规则动态可配。关键配置如下:
| 规则类型 | 配置项 | 值 | 说明 |
|---|---|---|---|
| QPS 限流 | /seckill/** |
5000 | 全局限流 |
| 关联限流 | /seckill/{id} → /order/create |
1000 | 防止订单服务被打垮 |
| 熔断规则 | DB 查询接口 | 异常比例 > 50% | 10s 内 |
Sentinel 的好处是支持热点参数限流——比如按 productId 维度限流,热门商品单独控速,冷门商品不限制。这招在双11救了我们好几次。
别忘了数据库:分库分表不是万能药
很多人一提高并发就喊“赶紧分库分表!”。但说实话,90% 的场景优化 SQL + 加索引 + 读写分离就够了。
我们的商品表只有千万级数据,根本不需要 ShardingSphere。真正的问题是:
- 没有覆盖索引,导致回表查询
- 订单状态更新频繁,InnoDB 行锁冲突
优化后:
- 给
(user_id, status)加复合索引,避免全表扫 - 把订单状态变更拆成独立小表,减少主表写压力
- 读写分离:查询走从库,写操作走主库(通过 ShardingSphere 自动路由)
🚨 血泪教训:有一次我把一个大事务拆成多个小事务,结果因为没加
@Transactional,导致库存超卖。凌晨两点被 call 起来修数据,从此再也不敢轻视事务边界。
Llama 和 Gemini?它们帮不上生产事故
说到这儿,可能有人问:你不是试过 Llama 和 Gemini 吗?它们能生成高并发代码吗?
实话实说:不能。Llama 本地跑起来吃光我 32G 内存,Gemini 给的示例代码连线程安全都不考虑。比如它建议用 ConcurrentHashMap 做库存计数,却忽略了原子递减的 CAS 失败重试逻辑——这种代码上线就是事故。
AI 工具适合帮你写 CRUD 或单元测试,但高并发场景下的边界条件、竞态处理、监控埋点,还得靠人脑+经验。这也是为什么我最后回归 Cursor——它至少能理解我项目里的领域模型,不会把 SeckillService 和 ShoppingCartService 的逻辑搞混。
效果如何?数据说话
改造后,我们在压测环境模拟了 2w 并发用户:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 平均响应时间 | 780 ms | 42 ms |
| 错误率 | 38% | 0.2% |
| DB CPU 使用率 | 95% | 35% |
| 系统吞吐量 | 180 TPS | 5200 TPS |
双11当天,系统平稳扛住峰值 8000 QPS,零重大故障。运维兄弟请我喝了杯瑞幸,算是对我们后端最大的认可。
最后几句真心话
高并发系统设计,没有银弹。它是一堆细节的叠加:一个合理的缓存策略、一条高效的 SQL、一个恰到好处的异步流程、一套完备的监控告警。
别被“百万并发”这种词吓到。我见过太多团队盲目上 Kafka、上 Redis Cluster,结果因为配置错误反而拖慢系统。先定位瓶颈,再对症下药,才是工程师该干的事。
现在每天通勤路上,我还会想想:如果再来一次秒杀,能不能做得更好?也许下次,我会试试用 GraalVM 编译成 native image 减少 GC 停顿,或者用 ZGC 进一步压低延迟。
但不管技术怎么变,核心不变:敬畏流量,尊重数据,别让用户的热情变成系统的灾难。
(完)

评论 0