高并发不是玄学,是我被逼出来的实战经验

慢查询猎人
2026-04-08 10:36
阅读 2393

去年双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 → 后台消费者真正扣库存。

这里的关键是前置校验必须精准。我们用了三级校验:

  1. Redis 缓存库存(预热时加载)
  2. 用户限购计数(用 Redis Hash 存 userId → count)
  3. 请求合法性(防刷、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——它至少能理解我项目里的领域模型,不会把 SeckillServiceShoppingCartService 的逻辑搞混。


效果如何?数据说话

改造后,我们在压测环境模拟了 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

最热最新
暂无评论
慢查询猎人Lv.1
0
影响力
0
文章
0
粉丝