高并发系统设计:从理论到实践
上海某互联网公司,996福报享受者一枚。白天写CRUD,晚上啃书学K8s,周末还要参加技术分享会——不是我卷,是房租太贵,不敢躺平。
大家好,我是小张(化名),坐标上海张江,目前在一家“准一线”互联网公司做后端开发。说白了就是那种每天被产品经理追着问“这个需求今天能上线吗?”、被运维吐槽“你们服务又把Node搞挂了”、被测试甩一堆Jira单子的普通Go仔。
去年双11前两周,我们组突然接到一个“史诗级”任务:把用户下单接口从QPS 2000 提升到 20000。领导原话是:“别管怎么搞,反正大促那天不能崩。”当时我盯着屏幕上的监控图,CPU飙到90%,数据库连接池爆满,心里只有一个念头:这届双11,要么升职加薪,要么提桶跑路。
于是,我被迫(其实是被逼)深入研究高并发系统设计。这篇文章,就是我在无数个加班深夜、凌晨三点泡面配咖啡、以及GitHub疯狂Ctrl+C/V之后,总结出来的一点实战经验。如果你和我一样,是个没时间但又想进步的打工人,希望这篇能帮你少踩几个坑。
问题来了:为啥我们的系统扛不住?
先说背景。我们原来的下单流程是这样的:
- 用户点击“立即购买”
- 后端调用库存服务扣减
- 调用优惠券服务核销
- 写订单到MySQL
- 发消息到Kafka通知物流、积分等下游
看起来没啥毛病?但一压测就炸。原因很简单:所有操作都是同步串行的,而且数据库成了瓶颈。更惨的是,当时用的还是单体架构,一个接口拖垮整个服务。
最离谱的是,有次测试同学模拟了5000并发,结果数据库主从延迟直接飙到30秒,订单状态不一致,客服电话被打爆。我当时真的想砸电脑——这哪是写代码,这是在给公司埋雷啊!
重构思路:异步 + 缓存 + 削峰
被教训之后,我们痛定思痛,决定重构。核心原则就三条:
- 能异步的绝不同步
- 能缓存的绝不查库
- 流量太大就削峰填谷
第一步:引入Redis做库存预扣
库存是最大的性能杀手。以前每次下单都要SELECT FOR UPDATE锁行,高并发下直接死锁。后来我们改成库存预热+Redis原子操作。
简单说,就是大促前先把商品库存加载到Redis,用DECR命令扣减。如果Redis库存不够,直接返回“售罄”,连数据库都不碰。
// Go代码示例:Redis扣库存
func DeductStock(ctx context.Context, client *redis.Client, skuID string, num int64) (bool, error) {
key := fmt.Sprintf("stock:%s", skuID)
res, err := client.Eval(ctx, `
local current = tonumber(redis.call('GET', KEYS[1]))
if current >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
`, []string{key}, num).Result()
if err != nil {
return false, err
}
return res.(int64) >= 0, nil
}
小贴士:记得设置合理的过期时间,防止缓存雪崩。我们用了随机TTL + 定时任务兜底。
第二步:下单流程异步化
原来的5步流程,现在拆成两阶段:
- 阶段一(同步):校验参数、扣Redis库存、生成订单号、写入Kafka
- 阶段二(异步):消费Kafka消息,处理优惠券、写DB、发通知
这样,用户点击下单后50ms内就能看到“下单成功”,后面的事慢慢干。即使下游服务挂了,消息还在Kafka里,重试就行。
我们用Go写的消费者,配合nsq(公司内部封装的Kafka客户端),支持自动ack、重试、死信队列。再也不用担心因为优惠券服务抖动导致整个下单失败了。
第三步:限流 & 熔断
再牛的系统也有极限。所以我们加了两层防护:
- 网关层限流:用Nginx+Lua做令牌桶,单IP每秒最多10次请求
- 服务层熔断:用
gobreaker库,当依赖服务错误率超过50%,自动熔断5秒
// Go熔断器示例
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "coupon-service",
MaxRequests: 10, // 熔断期间允许的探测请求数
Timeout: 5 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
// 调用时包裹
_, err := cb.Execute(func() (interface{}, error) {
return callCouponService(order)
})
数据库怎么扛住?
光靠应用层优化还不够,DB也得跟上。
我们做了几件事:
| 优化项 | 具体做法 | 效果 |
|---|---|---|
| 读写分离 | 主库写,从库读(除了下单这种强一致场景) | 读QPS提升3倍 |
| 分库分表 | 订单表按user_id hash分16库16表 | 单表数据<500万 |
| 连接池调优 | SetMaxOpenConns(100), SetMaxIdleConns(20) |
连接数稳定,不再OOM |
| SQL优化 | 所有查询走索引,避免SELECT * |
平均响应<10ms |
特别吐槽一句:千万别在循环里查数据库! 我们之前有个同事写了个for循环查用户信息,结果压测时直接把DB打挂。后来被全组“亲切关怀”了一周。
上线 & 监控:别让事故半夜叫醒你
代码写完只是开始,上线才是真正的考验。
我们用蓝绿发布 + K8s滚动更新,配合Prometheus + Grafana监控关键指标:
- 接口QPS / 延迟 / 错误率
- Redis命中率
- Kafka堆积量
- DB连接数 / 慢查询
还写了个简单的告警规则:如果下单成功率低于99.5%,自动钉钉@值班人。
双11当天,我坐在工位上手心冒汗,盯着Grafana看。结果?峰值QPS 22000,平均延迟80ms,零故障。那一刻,我觉得所有的加班都值了——虽然第二天还是得9点准时打卡。
开源项目推荐(打工人必备)
如果你也在搞高并发,这几个GitHub项目强烈推荐:
- go-kratos/kratos:B站开源的Go微服务框架,内置限流、熔断、链路追踪
- go-zero:自带缓存、防重、幂等的Go框架,适合快速开发
- kubebuilder:如果你玩K8s Operator,这个必须会
我自己也把部分工具代码整理到了GitHub(私有仓库,老板不让开源 😭),但核心思路都是从这些开源项目学来的。
最后一点真心话
说实话,高并发不是靠一两个“黑科技”就能搞定的。它是一套系统性工程:从代码设计、缓存策略、数据库优化,到部署架构、监控告警,每个环节都不能掉链子。
作为996打工人,我没时间去读大部头论文,只能在实战中边踩坑边学。但正是这些“线上事故”逼着我成长。现在回头看,那些凌晨三点改配置的日子,反而成了最宝贵的经验。
如果你也在为高并发头疼,别慌。先从缓存和异步入手,再逐步优化数据库和架构。记住:能跑起来的系统,比完美的设计更重要。
对了,下周五晚上公司又要搞技术分享会,我打算讲讲K8s网络策略怎么防DDoS……如果你在上海,欢迎来听(顺便请我喝杯瑞幸,加班续命用)。
本文所有代码和配置均已脱敏,如有雷同,纯属打工人共鸣。
Go version: 1.21, K8s cluster: v1.25, 双11已过,但需求永不停歇。

评论 0