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

UI还原大师
2025-12-18 13:38
阅读 2490

上海某互联网公司,996福报享受者一枚。白天写CRUD,晚上啃书学K8s,周末还要参加技术分享会——不是我卷,是房租太贵,不敢躺平。

大家好,我是小张(化名),坐标上海张江,目前在一家“准一线”互联网公司做后端开发。说白了就是那种每天被产品经理追着问“这个需求今天能上线吗?”、被运维吐槽“你们服务又把Node搞挂了”、被测试甩一堆Jira单子的普通Go仔。

去年双11前两周,我们组突然接到一个“史诗级”任务:把用户下单接口从QPS 2000 提升到 20000。领导原话是:“别管怎么搞,反正大促那天不能崩。”当时我盯着屏幕上的监控图,CPU飙到90%,数据库连接池爆满,心里只有一个念头:这届双11,要么升职加薪,要么提桶跑路

于是,我被迫(其实是被逼)深入研究高并发系统设计。这篇文章,就是我在无数个加班深夜、凌晨三点泡面配咖啡、以及GitHub疯狂Ctrl+C/V之后,总结出来的一点实战经验。如果你和我一样,是个没时间但又想进步的打工人,希望这篇能帮你少踩几个坑。


问题来了:为啥我们的系统扛不住?

先说背景。我们原来的下单流程是这样的:

  1. 用户点击“立即购买”
  2. 后端调用库存服务扣减
  3. 调用优惠券服务核销
  4. 写订单到MySQL
  5. 发消息到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、重试、死信队列。再也不用担心因为优惠券服务抖动导致整个下单失败了

第三步:限流 & 熔断

再牛的系统也有极限。所以我们加了两层防护:

  1. 网关层限流:用Nginx+Lua做令牌桶,单IP每秒最多10次请求
  2. 服务层熔断:用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

最热最新
暂无评论
UI还原大师Lv.1
0
影响力
0
文章
0
粉丝