高并发系统设计:从理论到实践 —— 一个双休不加班的国企程序员的血泪复盘

写码不秃头
2025-12-12 22:18
阅读 575

上周五下午四点半,我正偷偷在工位上刷 LeetCode(别问,问就是准备跳槽),突然钉钉“叮”一声炸响。产品老张发来消息:“兄弟,咱们那个用户积分系统要支持秒杀活动了,预估峰值 5k QPS,下个月上线。” 我手一抖,咖啡差点洒在键盘上——这玩意儿现在连 Redis 缓存都没加,数据库还是单点 MySQL,平时 200 QPS 就开始喘。

但转念一想:反正我是国企程序员,双休不加班,项目 deadline 再紧也得按流程走。不过……既然都准备跳槽了,不如趁机把高并发这块硬骨头啃下来?毕竟简历上写“参与设计支撑 5k QPS 的高可用积分系统”,比“维护老旧 OA 系统”好听多了,对吧?

于是,周末两天,我一边用 Claude 帮我画架构图、生成 Go 模板代码,一边翻《Designing Data-Intensive Applications》补理论,顺便在 B 站刷了个 K8s 实战课。今天这篇技术分享,就是我从“理论懵逼”到“线上稳如狗”的全过程复盘。Go 和 JavaScript 都会涉及,毕竟我们前端用 Vue + Axios,后端是 Go 微服务,典型的技术栈混搭现场。


别被“高并发”吓住,先搞清楚你要扛什么

很多人一听到高并发就想到“百万连接”、“亿级流量”,其实对我们这种中小业务来说,5k QPS 已经算“高”了。关键不是数字本身,而是瓶颈在哪

我拉了运维小王查了下当前系统的监控:

  • CPU 平均 30%,但峰值能飙到 90%
  • DB 连接池经常打满(max_connections=100)
  • 接口 P99 延迟 800ms,偶尔超时

很明显:数据库成了单点瓶颈。每次积分变更都要写 DB,还带事务,锁表是家常便饭。更坑的是,前端为了实时显示积分,每 2 秒轮询一次 /user/points,纯纯的流量浪费。

所以第一步不是上 Kafka、上 Redis Cluster,而是 优化请求路径 + 减少无效负载

前端先背锅:轮询?长连接安排上!

我把前端小李叫过来(其实是企业微信语音):“兄弟,轮询太奢侈了,咱们改 WebSocket 吧,或者至少用 Server-Sent Events(SSE)。”

他一脸苦相:“老大,Vue 里搞 SSE 要额外封装,而且测试说兼容性……”

我说:“兼容性个锤子,IE 都死了。你用 EventSource,5 行代码搞定,我给你示例:”

// 前端 JS:用 SSE 替代轮巡
const eventSource = new EventSource('/api/v1/points/stream?userId=123');
eventSource.onmessage = (event) => {
  const points = JSON.parse(event.data);
  updatePointsUI(points); // 更新 UI
};

后端 Go 用 Gin 写个简单 handler:

func PointsStream(c *gin.Context) {
    userId := c.Query("userId")
    flusher, ok := c.Writer.(http.Flusher)
    if !ok {
        c.AbortWithStatus(500)
        return
    }

    c.Header("Content-Type", "text/event-stream")
    c.Header("Cache-Control", "no-cache")
    c.Header("Connection", "keep-alive")

    for {
        points := getCurrentPoints(userId) // 从缓存读
        fmt.Fprintf(c.Writer, "data: %s\n\n", points)
        flusher.Flush()
        time.Sleep(5 * time.Second) // 比轮询省 4 倍请求
    }
}

光这一招,QPS 直接砍掉 70%。产品老张看到监控曲线平滑了,当场请我喝了杯瑞幸(虽然券是他自己的)。


核心逻辑:异步化 + 缓存双保险

但积分变更还是同步写 DB,高峰期照样崩。这时候就得祭出高并发三板斧:缓存、队列、削峰

第一步:Redis 缓存兜底

我给积分字段加了两级缓存:

  1. 本地缓存(Go 用 bigcache):扛住热点用户(比如运营账号频繁查自己积分)
  2. Redis 分布式缓存:集群部署,TTL 5 分钟,写穿透用互斥锁
func GetPoints(userId string) (int, error) {
    // 先查本地缓存
    if val, ok := localCache.Get(userId); ok {
        return val.(int), nil
    }
    
    // 再查 Redis
    if val, err := redisClient.Get(ctx, "points:"+userId).Result(); err == nil {
        points, _ := strconv.Atoi(val)
        localCache.Set(userId, points) // 回填本地
        return points, nil
    }
    
    // 缓存击穿?加锁重建
    mutex.Lock()
    defer mutex.Unlock()
    // ... 从 DB 加载并回填 Redis 和本地
}

💡 踩坑提醒:别用 Go 自带的 sync.Map 做本地缓存!它在高并发下 GC 压力巨大,bigcacheristretto 才是正道。

第二步:变更操作扔进队列

所有积分增减操作,不再直接写 DB,而是发到 Kafka(我们云原生团队强推的,运维说比 RabbitMQ 稳)。消费者用 Go 写,批量消费 + 事务写入。

// 生产者:积分变动入队
func AddPoints(userId string, delta int) error {
    msg := PointsChangeEvent{UserID: userId, Delta: delta, Timestamp: time.Now()}
    bytes, _ := json.Marshal(msg)
    return kafkaProducer.Send(context.Background(), &kafka.Message{
        Topic: "points-change",
        Value: bytes,
    })
}

// 消费者:批量处理
func consumePointsChange() {
    for msgs := range consumer.Chan() {
        var changes []PointsChangeEvent
        for _, msg := range msgs {
            var change PointsChangeEvent
            json.Unmarshal(msg.Value, &change)
            changes = append(changes, change)
        }
        
        // 批量更新 DB + 清缓存
        batchUpdateDB(changes)
        invalidateCache(changes) // 删除 Redis 和本地缓存
    }
}

这样一来,前端请求只读缓存,写操作异步化,DB 压力骤降。实测 5k QPS 下,MySQL CPU 稳定在 40% 以下。


云原生加持:K8s 让扩容像呼吸一样自然

作为熟悉 K8s 的“老油条”,我当然不会只靠代码优化。基础设施才是高并发的终极护城河

我们在阿里云 ACK 上部署了 Go 服务,配置了 HPA(Horizontal Pod Autoscaler):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: points-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: points-service
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "100"

意思是:CPU 超过 60% 或单 Pod QPS 超 100,就自动扩容。配合阿里云 SLB + Ingress,流量进来自动分发。

上周压测时,模拟 6k QPS,Pod 从 3 个 10 秒内扩到 15 个,错误率始终 < 0.1%。运维小王惊了:“你这服务怎么比我们的 Jenkins 还稳?”

我默默看了眼窗外——双休日,阳光正好,而我的服务正在云上自由伸缩。


数据库:别再裸奔了!

最后说说 DB。很多团队以为上了缓存就万事大吉,但缓存只是加速器,DB 才是底线

我们做了三件事:

  1. 读写分离:主库写,两个只读从库读(Go 用 gorm 的 DB Resolver)
  2. 分库分表:按 user_id hash 分 4 库,每库 16 表(用 ShardingSphere 中间件)
  3. 连接池调优:Go 的 database/sql 默认 MaxOpenConns=0(无限制),必须设上限!
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100)   // 根据 DB 规格调整
sqlDB.SetMaxIdleConns(20)
sqlDB.SetConnMaxLifetime(time.Hour)

压测时发现,连接池打满往往是因为慢查询。于是又加了 SQL 审计:

  • 强制所有 WHERE 条件走索引
  • 禁止 SELECT *
  • 大于 100ms 的查询自动告警

效果对比 & 跳槽底气

改造前后性能对比如下:

指标 改造前 改造后
最大 QPS 300 8000+
P99 延迟 800ms 45ms
DB CPU 峰值 95% 42%
服务可用性 98.5% 99.99%

最关键的是——没加一台物理机,全靠架构优化和云原生弹性。

现在产品老张见我就笑:“下次大促就靠你了!” 我表面点头,心里想着:等我跳槽涨薪成功,你就另请高明吧 😏


写在最后:高并发不是魔法,是工程权衡

很多人觉得高并发是“大厂专属”,其实不然。核心就三点

  1. 识别瓶颈(用监控说话,别猜)
  2. 分层解耦(缓存、队列、异步)
  3. 自动化伸缩(拥抱云原生)

我一个国企程序员,靠 ChatGPT 辅助写样板代码、Claude 帮我理清思路、K8s 做自动扩缩容,也能搞定 5k QPS。你说,这届程序员是不是越来越“懒”了?

但懒归懒,该学的东西一点不能少。毕竟,跳槽市场的残酷,可不会因为你是国企员工就手下留情

好了,LeetCode 还有 200 题没刷,我去卷了。下期分享:《如何用 Go + WASM 把前端性能提升 3 倍》,记得关注!


作者:某国企不加班程序员,白天写 CRUD,晚上刷算法,梦想是入职一家“真·双休”的公司。技术栈:Go / K8s / 一切能让工作变轻松的 AI 工具。

评论 0

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