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

王庆华△
2025-12-18 15:52
阅读 1673

大家好,我是小李,一个刚入职新公司还在试用期的后端开发。说“刚入职”可能有点误导——其实我在上一家公司干了三年多,眼看着项目越来越臃肿、技术栈越来越老,再加上去年双11期间系统崩了三次,我终于下定决心换个环境。现在这家新公司主打AI+电商,节奏快得飞起,但氛围意外地不错,至少没人半夜三点在群里@你“这个需求很简单”。

最近团队在搞一个高并发商品秒杀模块,产品经理拍着胸脯说“就几千人抢,稳得很”,结果压力测试一跑,直接502了。领导看我简历上写了点Go经验(其实是自学的),就让我牵头搞架构设计。说实话,当时心里慌得一批——我哪懂什么高并发啊,顶多在B站看了几个教程,连Redis都没正经上过生产。

但转念一想:这不就是跳槽前最该掌握的核心技能吗?于是硬着头皮上了,边学边干,还厚着脸皮蹭了隔壁组的技术分享会。今天这篇博客,就是把踩过的坑、熬过的夜、查过的文档,全倒出来,希望能帮到和我一样“被赶鸭子上架”的兄弟们。


事情是怎么搞砸的?

事情起源于上周五下午四点。产品同学突然拉了个会:“下周三上线新品首发,预计峰值QPS 5000。”我一听,心都凉了半截——我们现在的服务是单体架构,MySQL直连,缓存?不存在的。更离谱的是,他们居然打算用原来的下单接口直接改!我当场就想问:“你是觉得服务器电费太便宜了吗?”

果然,第一次压测(JMeter模拟2000并发)不到30秒,数据库CPU飙到100%,API响应时间从200ms直接干到8秒,最后整个Pod OOM killed。运维大哥在群里发了个“😅”,我知道,他在憋笑。

那一刻我真想砸电脑。但冷静下来想想,问题其实很典型:

  • 热点数据集中:所有请求都打到同一个商品ID
  • 数据库成为瓶颈:每次都要查库存、写订单
  • 缺乏流量控制:恶意刷单或突发流量直接冲垮系统

于是,我给自己定了个小目标:用Go重写核心链路,扛住5000 QPS,不宕机、不超卖、不背锅。


架构怎么搭?别整那些花里胡哨的

很多人一提高并发,张口就是“微服务+Kafka+ES+Redis Cluster”。拜托,我们是个初创团队,人力有限,能跑起来再说。我画了个极简版架构图(脑子里的),核心就三点:

  1. 前置缓存:把库存放到Redis,用原子操作扣减
  2. 异步削峰:下单请求先入队列,后端慢慢消费
  3. 限流熔断:防止雪崩,保护下游

说白了,就是“能缓存的绝不查库,能异步的绝不同步,能拒绝的绝不硬扛”。

缓存层:Redis + Lua 脚本防超卖

库存扣减是最容易出问题的地方。如果用“读库存 → 判断 >0 → 扣减”这种三步走,高并发下必超卖。解决方案?Lua脚本!

// stock.lua
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
    redis.call('DECR', KEYS[1])
    return 1
end
return 0

在Go里调用:

func DeductStock(ctx context.Context, client *redis.Client, skuId string) (bool, error) {
    script := redis.NewScript(`
        local stock = redis.call('GET', KEYS[1])
        if tonumber(stock) > 0 then
            redis.call('DECR', KEYS[1])
            return 1
        end
        return 0
    `)
    result, err := script.Run(ctx, client, []string{"stock:" + skuId}).Result()
    if err != nil {
        return false, err
    }
    return result.(int64) == 1, nil
}

这段脚本保证了原子性,哪怕1000个请求同时进来,也只会扣成功一次。亲测有效——上次压测到8000 QPS,库存一分不差。

小贴士:记得给Redis key设置过期时间,不然活动结束库存锁死,运营小姐姐会追着你砍。


异步处理:用Channel当消息队列?

一开始我想直接上Kafka,但运维说“集群还没申请下来”。情急之下,我灵机一动:Go不是有goroutine和channel吗?临时拿它当内存队列用用?

type OrderRequest struct {
    UserID  string
    SkuID   string
    // ...
}

var orderChan = make(chan OrderRequest, 10000) // 有缓冲channel,防爆

func init() {
    // 启动消费者
    go func() {
        for req := range orderChan {
            // 真正写DB、发MQ、记日志...
            processOrder(req)
        }
    }()
}

func PlaceOrderHandler(w http.ResponseWriter, r *http.Request) {
    var req OrderRequest
    json.NewDecoder(r.Body).Decode(&req)

    // 先扣库存(Redis Lua)
    success, _ := DeductStock(r.Context(), redisClient, req.SkuID)
    if !success {
        http.Error(w, "库存不足", http.StatusTooManyRequests)
        return
    }

    // 入队
    select {
    case orderChan <- req:
        w.WriteHeader(http.StatusAccepted)
    default:
        // 队列满,直接拒绝
        http.Error(w, "系统繁忙,请稍后再试", http.StatusServiceUnavailable)
    }
}

看起来挺美?但上线第二天就翻车了——服务重启,channel里的请求全丢了!用户付了钱没下单,客服电话被打爆。

教训:内存队列不能用于关键业务!赶紧切到RabbitMQ(运维终于批了)。不过这段代码帮我撑过了最危险的48小时,也算立功了。


限流:别让流量把你家门踹烂

再好的系统也怕洪水猛兽。我们用了golang.org/x/time/rate做令牌桶限流:

var limiter = rate.NewLimiter(rate.Every(time.Second/100), 200) // 每秒100个,突发200

func RateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "请求太频繁", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}

配合Nginx层的limit_req,双重保险。实测效果:哪怕压测工具疯狂打,后端服务稳如老狗。


数据库和接口设计:少即是多

很多人喜欢在接口里返回一堆字段,前端说“以后可能用”。结果呢?网络传输慢、序列化耗时、DB索引臃肿。这次我学乖了:

  • 接口只返回必要字段:下单成功就返{"code":200, "msg":"ok"}
  • 数据库拆表:订单主表 + 订单扩展表,热点字段单独索引
  • 写操作去关联:不连表、不触发器、不外键(别骂,生产环境真香)

另外,所有写接口都加了幂等性校验——用request_id做唯一键,防止重复提交。产品经理说“用户不会点两次”,但现实是:他点了十次,因为页面卡住了。


效果如何?数据说话

经过两周折腾(包括三个通宵),最终压测结果如下:

指标 优化前 优化后
P99 延迟 8200 ms 120 ms
错误率 35% 0.2%
DB CPU 100% 25%
最大QPS ~800 6500+

上线当天,虽然流量只来了3000 QPS(产品又高估了),但系统稳得一批。运维大哥破天荒在群里夸了一句:“这次没半夜叫我,不错。”


最后一点碎碎念

写这篇文章的时候,我试用期还剩两周。说实话,要不是这次项目逼我深入高并发,我可能还在写CRUD。现在不仅对系统设计有了手感,连跳槽简历都能多写一行“主导高并发秒杀系统重构”。

如果你也在试用期焦虑、想跳槽但技术卡壳,我的建议是:主动接难题,哪怕是硬着头皮。公司愿意让你碰核心系统,说明信任你;搞砸了,最多背锅(但通常不会开除);搞成了,就是你的资本。

顺便安利下我们公司的技术分享会——每周五下午茶时间,有人讲AI模型部署,有人聊Go性能调优,还有人吐槽产品需求。氛围真的好,不像某些公司,技术分享=领导讲话。

好了,就写这么多。代码细节我放GitHub了(私信我拿链接),欢迎交流。下次要是系统再崩,可能我就真砸电脑了 😅

评论 0

最热最新
暂无评论
王庆华△Lv.1
0
影响力
0
文章
0
粉丝