从日活百万到扛住秒杀:我的高并发实战复盘
上个月刚从老东家离职,准备拉几个兄弟搞点自己的事情。创业前总得给自己攒点“技术弹药”,于是我回头整理了在上家公司做高并发系统时踩过的坑、熬过的夜、掉过的头发——特别是去年双11大促期间,我们那个差点把服务器干冒烟的订单服务。
说实话,当时作为技术总监,压力不是一般的大。产品经理在群里@我:“老板说今年GMV要翻三倍,系统撑不住就换人。”运维兄弟幽幽回了一句:“再加机器预算超了,财务已经把我拉黑了。”测试妹子直接甩过来一个JIRA链接:“压测5000 QPS就崩,你看着办。”
那会儿真想砸电脑。但转念一想,不就是高并发嘛?无非是缓存、队列、限流、降级那一套。可真上手才发现,理论和实践之间差了十个凌晨三点的咖啡因剂量。
为什么高并发不是“多开几台服务器”那么简单?
很多人(包括我早期)以为高并发就是堆机器。但现实很骨感:资源永远不够,成本永远敏感,而用户永远不会体谅你的架构瓶颈。
我们遇到的第一个问题,是在商品详情页。看似简单的读接口,高峰期每秒请求超过8万次。数据库主库CPU飙到98%,Redis集群也扛不住穿透+击穿的双重暴击。更惨的是,因为没做熔断,下游库存服务被拖垮,整个下单链路雪崩。
这时候我才意识到:高并发不是单一组件的性能问题,而是整个链路的协同设计。
缓存策略:别再用“先查DB再写缓存”了
早期我们用的是最朴素的缓存模式:
def get_product(product_id):
data = redis.get(f"product:{product_id}")
if not data:
data = db.query("SELECT * FROM products WHERE id = ?", product_id)
redis.setex(f"product:{product_id}", 300, json.dumps(data))
return data
看起来没问题?但在高并发下,一旦缓存失效(比如TTL到了),成千上万个请求会同时打到数据库——这就是经典的缓存击穿。
后来我们改成了逻辑过期 + 后台异步刷新:
- 缓存里存两个字段:
data和expire_time - 请求进来先拿缓存,如果
expire_time < now,则返回旧数据,同时触发一个后台任务去更新 - 用 Redis 的
SETNX做分布式锁,避免多个线程同时刷新
这个方案的关键在于牺牲一点数据实时性,换系统稳定性。对商品信息这种非强一致场景,完全够用。
不过写这个逻辑的时候,我差点把自己绕晕。还好有 Aider 这个神器——它能根据我的自然语言描述,直接生成带注释的 Python 代码。比如我输入:“写一个带逻辑过期和异步刷新的商品缓存类,用 Redis 和 threading”,它几秒就给我整出来了,还自动处理了异常和锁释放。
Aider 真是我最近开发效率飞升的秘密武器。以前写这种并发逻辑要反复查文档、调试死锁,现在靠精准的 Prompt 工程,直接让它“扮演资深后端工程师”,输出的代码质量堪比 senior review 过的。
接口设计:防抖、限流、降级一个都不能少
光有缓存还不够。我们发现很多流量其实是无效的——比如用户疯狂点击“立即购买”,或者爬虫扫全站。
于是我们在网关层加了三层防护:
- 请求防抖:同一用户 500ms 内重复请求直接返回缓存结果
- 令牌桶限流:基于用户 ID + 接口维度,用 Guava RateLimiter(Java)或
token-bucket(Python)实现 - 动态降级:当系统负载 > 80%,自动关闭非核心功能(比如个性化推荐、评论加载)
这里有个血泪教训:限流一定要做在最外层!我们一开始把限流放在业务层,结果 Nginx 还是被打满,TCP 连接耗尽。后来直接用 OpenResty 在 Lua 层做 IP 级限流,效果立竿见影。
# nginx.conf 片段
lua_shared_dict limit_req_store 10m;
access_by_lua_block {
local limiter = require "resty.limit.req"
local limit, err = limiter.new("limit_req_store", 10, 100) -- 10 req/sec, burst 100
if not limit then
ngx.log(ngx.ERR, "failed to instantiate a limiter: ", err)
return ngx.exit(500)
end
local key = ngx.var.binary_remote_addr
local delay, err = limit:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(429) -- Too Many Requests
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
}
数据库:分库分表不是万能药
说到数据库,很多人第一反应就是“赶紧分库分表”。但分库分表带来的复杂度是指数级上升的——跨库 JOIN、全局 ID、分布式事务……我们团队试过 ShardingSphere,结果发现维护成本太高,反而拖慢了迭代速度。
后来我们走了另一条路:读写分离 + 热点数据拆分。
- 主库只处理写请求,最多支撑 3000 TPS
- 读请求走多个只读副本,通过 ProxySQL 自动路由
- 对于订单这类写密集型表,我们按用户 ID 哈希,拆成 64 个物理表(但还在同一个库)
关键优化点在于批量写入。原来每次下单要插入 5 张表(订单、明细、日志、流水、优惠券),现在改成:
- 先写 Kafka 消息队列
- 异步消费,批量 insert(一次 100 条)
- 失败的消息进死信队列,人工介入
这样数据库写压力直接降了 70%。虽然牺牲了“下单即可见”的体验,但用户其实根本感知不到——毕竟支付成功页还要转圈 2 秒呢。
队列:用好 Kafka,但别滥用
说到 Kafka,我们曾经犯过一个低级错误:把所有异步任务都塞进去,结果 topic 膨胀到上百个,消费者组管理混乱,消息积压严重。
后来我们做了严格分级:
| 消息类型 | Topic 命名规范 | 保留时间 | 消费者数量 |
|---|---|---|---|
| 核心交易 | order-critical | 7天 | 3 |
| 日志埋点 | log-analytics | 1天 | 10 |
| 通知类 | notify-push | 3天 | 5 |
| 批量任务 | batch-job | 30天 | 2 |
而且每个 topic 都配了监控告警:lag > 10万 或 consumer offline > 5min 就钉钉报警。运维兄弟再也不用半夜爬起来查队列了(他说请我喝了杯奶茶表示感谢)。
性能调优:从 5000 QPS 到 50000 QPS 的秘密
最后说说具体的性能数字。我们订单创建接口最初只能扛 5000 QPS,经过以下优化后达到 50000+:
- 连接池调优:MySQL 连接数从默认 100 调到 2000,配合 HikariCP 的
maximumPoolSize - JVM 参数优化:G1GC +
-XX:MaxGCPauseMillis=200 - 本地缓存:热点商品用 Caffeine 做 L1 缓存,减少 Redis 访问
- 序列化优化:JSON 改用 Protobuf,体积减少 60%
- 异步非阻塞:Spring WebFlux 替代传统 MVC(但注意:不是所有场景都适合!)
这里特别提一下 Prompt 工程的作用。比如我想调优 JVM,直接问 Claude:“给一个高吞吐 Java 服务的 G1GC 参数配置,要求暂停时间 < 200ms,堆内存 8GB”,它立刻给出:
-Xms8g -Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=35
省了我翻官方文档的时间。当然,参数还是要结合实际压测调整——AI 给的是起点,不是终点。
写在最后:高并发的本质是“妥协的艺术”
折腾了半年多,我最大的感悟是:没有完美的架构,只有不断权衡的取舍。
- 要一致性还是可用性?
- 要实时性还是吞吐量?
- 要开发速度还是系统稳定?
创业之后,我打算把这些经验产品化。比如做一个“高并发诊断助手”,用 Aider + RAG 技术,让中小团队也能快速获得架构建议。毕竟不是每个公司都有钱养一群 P8 架构师。
对了,如果你也在搞高并发系统,欢迎交流。我已经不用再背锅了(暂时),但看到同类问题还是会手痒——可能这就是程序员的宿命吧 😅
附:关键指标对比
| 优化阶段 | 订单接口 QPS | 平均延迟 (ms) | 错误率 (%) | DB CPU (%) |
|---|---|---|---|---|
| 初版 | 4800 | 210 | 2.1 | 95 |
| 缓存+限流后 | 18000 | 85 | 0.3 | 65 |
| 异步+队列后 | 35000 | 62 | 0.1 | 45 |
| 全链路优化后 | 52000 | 48 | 0.05 | 38 |
数据不会骗人。有时候你缺的不是技术,而是一套系统性的思考框架——加上一杯续命的冰美式。

评论 0