高并发系统设计:从理论到实践

浏览器兼容师
2025-12-16 07:39
阅读 746

上周五晚上 10 点半,我一边戴着耳机听周杰伦的《晴天》,一边在工位上盯着屏幕上疯狂刷屏的 GC 日志——我们新上线的秒杀活动接口又崩了。产品经理小王在企业微信群里疯狂@我:“线上挂了!用户都炸锅了!”,运维老李也在旁边幽幽地说:“兄弟,你这 Java 应用吃内存比我们家狗啃骨头还快。”

那一刻,我真的想砸电脑。

但不行啊,毕竟我才入职这家新公司俩月,副业接的外包单子还没结款,房贷还在等着还。只能深吸一口气,默默打开了 IDEA,开始排查问题。

为什么突然要搞高并发?

事情得从一个月前说起。我们产品组接到老板的“圣旨”:要在下个月双 11 前上线一个限量商品抢购功能,预计峰值 QPS 要扛住 5w+。我一听就头皮发麻——我们现在的系统架构还是单体应用,数据库是 MySQL 单点,缓存用得也不规范,连 Redis 都没做集群。

说实话,刚入职时我以为就是个 CRUD 项目,结果一上来就要搞高并发,属实有点懵。为了不被祭天,我赶紧翻出压箱底的几本书籍:《高性能 MySQL》、《Redis 设计与实现》,还有那本被无数人吹爆的《大型网站技术架构》。边看边做笔记,咖啡当水喝,头发一把一把掉。

先别急着写代码,想想架构

很多新人(包括曾经的我)一听到高并发,第一反应就是:“上 Redis!”、“加线程池!”、“用消息队列削峰!”。但其实,高并发不是靠某个中间件堆出来的,而是靠合理的架构设计和对业务场景的深入理解

比如我们的秒杀场景,核心就三个字:快、稳、准

  • :用户点击“立即抢购”后,响应必须快,最好 100ms 内返回结果;
  • :哪怕 10w 人同时点,系统也不能崩;
  • :不能超卖,也不能少卖,库存数字必须精确。

基于这个目标,我画了一张草图(其实就是用 draw.io 搞的简陋架构图),主要分三层:

  1. 接入层:Nginx + Lua 做限流,防刷;
  2. 服务层:Java 微服务,用 Spring Boot + Redis + RocketMQ;
  3. 数据层:MySQL 分库分表 + Redis 缓存预热。

别看现在说起来头头是道,当初设计时可是被架构师怼了三次。他说:“你这 Redis 直接写库存,万一宕机了怎么办?” 我当场愣住,心想:完蛋,又要改方案。

代码人生:从“能跑就行”到“优雅可靠”

以前接外包的时候,我写代码信奉“能跑就行主义”——只要客户验收通过,管它内存泄漏还是死锁。但现在不行了,公司有 Code Review,有 Sonar 扫描,还有那个眼神犀利的测试妹子天天拿着 JMeter 压我接口。

所以这次,我逼着自己把代码质量提上来。比如库存扣减,我一开始写了个简单的:

// 初版:千万别学!
public boolean deductStock(Long skuId, int num) {
    Integer stock = redisTemplate.opsForValue().get("stock:" + skuId);
    if (stock >= num) {
        redisTemplate.opsForValue().set("stock:" + skuId, stock - num);
        return true;
    }
    return false;
}

结果上线压测时,1000 并发就出现超卖。为啥?因为 getset 不是原子操作啊!两个线程同时读到库存是 10,都以为自己能买,结果扣成负数。

后来改成 Lua 脚本:

-- Lua 脚本保证原子性
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
    redis.call('DECRBY', KEYS[1], ARGV[1])
    return 1
end
return 0

再配合 Java 里的 execute 调用,终于稳了。那一刻,我感觉自己像个真正的程序员了——不再是那个“能跑就行”的外包仔。

数据库:别让 MySQL 成为瓶颈

很多人以为上了 Redis 就万事大吉,但别忘了,最终数据还是要落到 MySQL。如果直接让高并发请求打到 DB,分分钟给你干趴。

我们的做法是:

  1. 库存预热:活动开始前 10 分钟,把所有 SKU 的库存加载到 Redis;
  2. 异步落库:用户下单成功后,只写 Redis,然后发消息到 RocketMQ,由消费者异步更新 MySQL;
  3. 分库分表:订单表按 user_id 哈希分 16 库,每库 16 表,避免单表过大。

当然,异步也有风险。比如 MQ 消息丢了怎么办?我们加了补偿机制:每天凌晨跑一个对账任务,对比 Redis 和 MySQL 的库存差异,自动修复。

有一次凌晨三点,报警邮件把我吵醒:“库存不一致!SKU_12345 差 2 件!” 我爬起来一看,原来是 MQ 消费者挂了,消息堆积了。赶紧重启 + 手动重推,折腾到天亮。从此以后,我把监控告警配得更细了——CPU、内存、队列长度、消费延迟,一个都不能少。

性能调优:那些深夜踩过的坑

光有架构还不够,还得调优。以下是我踩过的几个经典坑:

1. 连接池不够用

Spring Boot 默认的 HikariCP 连接池只有 10 个连接。压测时,大量请求卡在“waiting for connection”,TPS 上不去。后来调整为:

spring:
  datasource:
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10

2. Redis 大 Key 导致阻塞

我们一开始把整个商品信息 JSON 存 Redis,结果一个 Key 几十 KB。高并发时,GET 操作阻塞主线程,Redis 延迟飙升到 50ms+。后来拆分成多个字段,用 Hash 存储:

HSET product:123 name "iPhone" price 5999 stock 100

3. JVM 参数没调

默认的 JVM 参数在高并发下 GC 频繁。我们换成 G1 垃圾回收器,并调整堆大小:

-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200

调完后,Full GC 从每分钟一次降到一天不到一次。

效果如何?数据说话

经过三周的迭代 + 压测 + 修复,我们在预演环境做了全链路压测。结果如下:

指标 优化前 优化后
QPS 1200 58000
平均响应时间 850ms 65ms
错误率 12% 0.02%
CPU 使用率 95% 60%

双 11 当天,虽然流量冲到 6w QPS,但系统稳如老狗。运维老李拍了拍我肩膀:“小伙子,可以啊。” 产品经理小王也发了个红包:“辛苦了!”

那一刻,我感觉所有的熬夜、掉发、焦虑都值了。

写在最后:高并发不是终点,而是起点

很多人觉得高并发是“高级技能”,只有大厂才需要。但其实,只要你做的产品有用户、有流量,迟早会遇到性能瓶颈。而作为开发者,我们的代码人生不应该止步于“能跑”,而要追求“优雅、高效、可靠”。

回过头看,这次经历让我明白:高并发系统设计,70% 是架构思维,20% 是工程细节,10% 是工具使用。那些书里的理论,只有在真实业务中摔打过,才能真正内化成自己的能力。

顺便说一句,我现在副业接单子,也会主动问客户:“你们有没有高并发需求?” 如果有,我就按这套思路来——毕竟,谁不想做个靠谱的斜杠程序员呢?

哦对了,如果你也在搞高并发,欢迎留言交流。说不定下次双 11,我们还能一起“肝”到天亮,听着《晴天》,看着监控曲线平稳如初。

(完)


P.S. 最近在读《Designing Data-Intensive Applications》,强烈推荐!比国内某些“高并发秘籍”靠谱多了。别再信什么“三天掌握高并发”了,那都是割韭菜的。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝