关键不是堆go关键字,而是控制资源边界、隔离i/o阻塞、避免下游雪崩:goroutine泄漏与失控源于无节制并发,需用worker池限流;grpc替代http/1.1提升通信效率;redis缓存需布隆过滤器+空值缓存防穿透击穿;限流应按下游依赖粒度配置并绑定context。

Go 微服务后端要扛住高并发,关键不是堆 go 关键字,而是控制资源边界、隔离 I/O 阻塞、避免下游雪崩。单纯靠 Goroutine 数量撑并发,不出三天就会在预发环境被 OOM 或 connection refused 打趴。
goroutine 泄漏和失控:为什么批量任务不能直接 for+go
常见错误是把循环里每个业务逻辑都包一层 go process(item),尤其在导入、导出、消息广播等场景。它看似并发快,实则埋下三颗雷:
- 内存持续上涨:每个
goroutine初始栈 2KB,10 万条数据 = ~200MB 栈内存,还不算业务中分配的切片、结构体 - 数据库连接池打满:
sql.DB默认连接池大小通常为 10–30,无节制并发会触发sql: database is closed或超时等待 - 调度器过载:GMP 模型中 P 的数量默认等于 CPU 核数,当 G 远超 P,大量 G 在全局队列排队,延迟不可控
正确做法是用固定 worker 数的 goroutine 池,例如用 semaphore.NewWeighted(10) 控制并发上限,或基于 channel 构建带缓冲的任务队列。
gRPC 替代 HTTP/1.1:为什么服务间通信必须换协议
微服务之间走 http.DefaultClient + JSON,在 QPS > 5k 后延迟陡增,根本原因不在 Go,而在协议本身:
-
HTTP/1.1是文本协议,每次请求都要重复解析 header、序列化 body;gRPC底层用Protobuf,二进制编码体积小、解析快,同等数据量下序列化耗时低 5–8 倍 -
HTTP/1.1每个连接只能串行处理请求,而gRPC基于HTTP/2多路复用,单连接可并发处理数百请求,连接复用率高,减少TIME_WAIT和 TLS 握手开销 -
gRPC原生支持流式调用、截止时间(context.WithTimeout)、状态码语义(codes.Unavailable),比自己封装 HTTP 错误码更可靠
注意:gRPC Server 端需显式配置 MaxConcurrentStreams(默认 100),否则单个连接突发大量 stream 可能压垮服务。
Redis 缓存穿透与击穿:别只加 cache,得配布隆过滤器和空值
缓存层失效时,大量请求直击数据库,典型表现是慢查询飙升、CPU 100%、SELECT ... WHERE id = ? 出现大量 RowsAffected=0。这不是 Redis 不够快,而是没做兜底:
- 缓存穿透(查不存在的 key):用
redisbloom或自建布隆过滤器,在缓存前拦截无效 ID,但要注意布隆有误判率,不能替代业务校验 - 缓存击穿(热点 key 过期瞬间并发重建):用
sync.Once或分布式锁(如redis.SetNX)保证只有一个 goroutine 重建缓存,其他等待而非穿透 - 空值缓存:对查不到的数据也写入
cache.Set(key, nil, time.Minute),TTL 缩短(如 60s),避免恶意刷不存在 ID
特别提醒:redis.Client.Get 返回 nil, nil 表示 key 不存在,不是错误;很多新手误判为连接失败,反复重试,反而加剧穿透。
限流不是“挡流量”,而是保下游:rate.Limiter 要按依赖拆分
全局限流(比如整个服务每秒 1000 请求)意义不大——用户请求可能只读缓存,而订单创建要查库存、扣余额、发 MQ,资源消耗差 10 倍以上。真正有效的是按下游依赖粒度限流:
- 对数据库操作:用
rate.NewLimiter(50, 100)控制每秒最多 50 次写入,防止连接池耗尽 - 对第三方 API:按服务商维度单独配置,比如微信回调接口限流 10qps,避免被对方封禁
- 对内部 gRPC 服务:在 client 端封装限流中间件,而不是等 server 端返回
codes.ResourceExhausted再降级
别忘了:限流器必须和 context 绑定,超时后自动释放令牌,否则在长尾请求里会卡住整个桶。
高并发不是让系统跑得更快,而是让它在压力下不崩溃。Goroutine 是刀,gRPC 是盾,缓存和限流是闸门——用错位置,再锋利的刀也救不了系统。











