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

睿智的法师
2025-12-18 19:26
阅读 592

上周五晚上十一点,我正瘫在沙发上刷招聘网站,一边啃着冷掉的外卖披萨,一边纠结要不要跳槽。最近公司项目压得喘不过气,双11刚过,系统又崩了一次——这次是因为有个爬虫团伙疯狂请求我们的商品详情页,QPS 直接飙到 8w+,数据库连接池直接干爆。

说实话,我现在有点迷茫。远程办公虽然自由,但天天在家对着屏幕写代码,连个能吐槽的人都没有。今天这篇博客,算是给自己整理思路,也顺便记录下这段时间踩过的坑。毕竟嘛,万一真跳槽了,简历上也能多写点“高并发系统设计经验”(狗头保命)。


起因:不是我想搞高并发,是爬虫逼的

事情得从去年双11说起。我们团队维护一个电商中台,平时 QPS 也就几千,结果那天凌晨两点,监控报警像疯了一样响。查日志发现,大量请求来自几个固定 IP 段,User-Agent 还是空的——典型的恶意爬虫行为。

更离谱的是,这些请求全打在 /product/detail/{id} 接口上,而且参数 id 是连续递增的,明显是在批量扫商品数据。我们的接口没做任何限流,缓存穿透也没防,结果 Redis 被打穿,MySQL 主库 CPU 直接 100%,整个服务雪崩。

当时我真的想砸电脑。产品经理还发消息问:“用户反馈打不开页面,是不是后端又挂了?” 我回了个“是”,然后默默打开了《Go 并发编程实战》PDF。


架构调整:从单体到分层防御

冷静下来后,我和运维、测试开了个紧急会议(其实就是在 Slack 上扯了两小时)。我们决定重构整个接口链路,核心思路就一条:别让请求轻易碰到数据库

第一层:Nginx + Lua 做 IP 限流

最简单粗暴的方式,先在网关层把坏流量拦住。我们在 Nginx 里加了 Lua 脚本,基于 IP 做令牌桶限流:

http {
    lua_shared_dict limit_req_store 100m;

    init_by_lua_block {
        require "resty.core"
    }

    server {
        location /product/detail/ {
            access_by_lua_block {
                local limit_req = require "resty.limit.req"
                local lim, err = limit_req.new("limit_req_store", 10, 20)
                if not lim then
                    ngx.log(ngx.ERR, "failed to instantiate a limit_req object: ", err)
                    return ngx.exit(500)
                end

                local key = ngx.var.remote_addr
                local delay, err = lim:incoming(key, true)
                if not delay then
                    if err == "rejected" then
                        ngx.log(ngx.WARN, "request rejected by limit_req: ", key)
                        return ngx.exit(429)
                    end
                    ngx.log(ngx.ERR, "failed to limit req: ", err)
                    return ngx.exit(500)
                end
            }
            proxy_pass http://backend;
        }
    }
}

这招对普通爬虫效果拔群。IP 被限后直接返回 429,连 Go 服务都碰不到。

第二层:Go 服务内做熔断 & 缓存

我们用的是 Go 写的微服务,框架选了 Gin。针对缓存穿透问题,我加了两层防护:

  1. 空值缓存:如果查不到商品,就缓存一个 {},TTL 设短一点(比如 60s)
  2. 布隆过滤器:用 Redis 的 Bitmap 实现简易版布隆过滤器,提前拦截无效 ID

关键代码片段:

func GetProductDetail(id string) (*Product, error) {
    // 先查布隆过滤器
    exists, err := bloomFilter.Exists(ctx, id)
    if err != nil {
        log.Warnf("bloom filter check failed: %v", err)
        // 容错:过滤器挂了不能阻塞主流程
    } else if !exists {
        return nil, ErrProductNotFound // 直接返回,不查 DB
    }

    // 查 Redis
    var p Product
    if err := redis.Get(ctx, "product:"+id, &p); err == nil {
        return &p, nil
    }

    // 缓存未命中,查 DB
    p, err := db.QueryProduct(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            // 空值缓存,防穿透
            redis.SetEx(ctx, "product:"+id, "{}", 60*time.Second)
            return nil, ErrProductNotFound
        }
        return nil, err
    }

    // 回填缓存
    redis.SetEx(ctx, "product:"+id, p, 24*time.Hour)
    return &p, nil
}

吐槽一句:测试同学一开始说“布隆过滤器有误判,会影响用户体验”。我说大哥,误判只是把合法请求当成非法,最多多查一次 DB;但不加的话,系统直接崩,哪个更影响体验?

第三层:数据库读写分离 + 分库分表

虽然前面两层已经挡住 99% 的恶意流量,但我们还是做了兜底。MySQL 主库只处理写请求,读请求全部走从库。另外,商品表按 id % 64 分了 64 个物理表,避免单表过大。


关于区块链?别急,它真的有用

你可能会问:标题里提到的“区块链”去哪了?是不是为了凑关键词硬塞?

还真不是。我们最近在调研一个新需求:商品溯源。用户扫描二维码,能看到这个商品从原料到上架的完整流转记录。这种场景下,数据一旦写入就不能篡改,天然适合区块链。

但我们没用公链(太慢太贵),而是基于 Hyperledger Fabric 搭了个私有链。Go 服务在写入商品信息时,会同时向区块链提交一笔交易:

func SubmitToBlockchain(productID string, traceData TraceInfo) error {
    // 序列化数据
    payload, _ := json.Marshal(traceData)
    
    // 调用 Fabric SDK 提交
    _, err := fabricClient.SubmitTransaction(
        "tracecc",      // chaincode 名
        "recordTrace",  // function
        productID,
        string(payload),
    )
    return err
}

有趣的是,区块链在这里反而成了性能瓶颈。Fabric 的吞吐量大概就 3k TPS,远低于我们的业务峰值。所以我们做了异步写入:先落 Kafka,再由消费者慢慢同步到链上。这样既保证了不可篡改性,又不影响主流程性能。


性能对比:优化前后数据说话

折腾了两周,效果立竿见影。下面是压测数据(用 wrk 模拟 1w 并发):

指标 优化前 优化后
P99 延迟 2800ms 85ms
错误率 42% 0.3%
DB CPU 98% 23%
Redis 命中率 61% 98.7%

最爽的是,上周又有爬虫来扫,结果人家自己 IP 被封了,我们系统纹丝不动。运维兄弟终于不用半夜被 PagerDuty 叫醒了。


跳槽?先把手头事做好

写到这里,突然觉得跳槽这事没那么急了。虽然现在公司加班多、需求乱,但至少让我接触到了真实的高并发场景——这玩意光看书是学不会的。上次面试某大厂,面试官问“怎么防缓存穿透”,我能直接掏出线上案例讲半小时,比背八股文强多了。

当然,该学的技术还得学。最近在研究 Go 的 contextsync.Pool 在高并发下的最佳实践,还顺手给团队封装了个通用限流中间件。代码已开源(内部 GitLab),欢迎 PR(虽然可能没人看 😂)。

最后送大家一句我贴在显示器边上的话:

“系统不会因为你写了‘高可用’三个字就真的高可用。”

共勉。

评论 0

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