grpc客户端重试需显式配置retrypolicy且仅对特定状态码生效,net/rpc重试须用goroutine+channel+context超时封装;二者均需错误分类、超时控制与服务端幂等性协同,否则重试加剧系统脆弱性。

Go微服务间RPC重试不是加个for循环就能跑通的事——它必须和错误分类、上下文超时、服务端幂等性对齐,否则重试次数越多,系统越脆弱。
gRPC客户端重试必须显式配置retryPolicy且只对特定状态码生效
gRPC Go客户端默认完全关闭重试,即使服务端返回UNAVAILABLE也不会自动重发。要启用,必须在grpc.Dial时传入grpc.WithDefaultServiceConfig,且配置内容是JSON字符串(不是struct)。常见失效原因包括:
- 把retry策略写成Go struct直接传给
grpc.WithServiceConfig——后者只影响后续NewClient调用,不生效于当前连接 - 服务端proto没声明
retryable: true,导致即使客户端配了,gRPC runtime也不认为该方法可重试 -
RetryableStatusCodes里漏掉RESOURCE_EXHAUSTED(限流场景),或误加INVALID_ARGUMENT(业务错误不该重试)
示例有效配置(注意转义和字符串格式):
{"methodConfig": [{"name": [{"service":"payment.PaymentService","method":"Charge"}], "retryPolicy": {"MaxAttempts": 4, "InitialBackoff":"0.1s", "MaxBackoff":"1s", "BackoffMultiplier": 2, "RetryableStatusCodes": ["UNAVAILABLE","DEADLINE_EXCEEDED","RESOURCE_EXHAUSTED"]}}]}
net/rpc重试必须用goroutine+channel+context超时封装
net/rpc.Client.Call不接收context.Context,无法原生取消阻塞调用。硬套for循环会堆积goroutine、耗尽连接、放大超时。安全做法是:
- 每次调用启动独立goroutine,结果写入带缓冲的
chan error - 外层用
select监听ctx.Done()或doneChan,超时即退出,不等待调用返回 - 建连阶段也设超时:
net.DialTimeout("tcp", addr, 2*time.Second),避免卡在TCP握手 - 别用
time.Sleep硬等——第1次失败后等100ms,第2次200ms,第3次400ms,上限建议1s
关键代码片段:
ch := make(chan error, 1)<br>go func() {<br> ch }()<br>select {<br>case err := return err<br>case return ctx.Err()<br>}
自研重试逻辑优先用backoff.Retry而非手写循环
手写重试容易漏掉退避、jitter、context中断、错误分类这四件事。用github.com/cenkalti/backoff/v4能规避大部分坑:
- 传入的函数必须返回
error,且backoff.Retry内部会自动判断是否重试——你只需在函数里做一次调用并返回原始err - 必须用
backoff.WithContext(..., ctx)包装,否则ctx.Done()触发时重试不会中止 - 错误判断不能靠
strings.Contains(err.Error(), "timeout")——gRPC错误信息版本间不一致,应解包:s, ok := status.FromError(err); ok && (s.Code() == codes.Unavailable) - 不要在重试函数里重建请求体(如new proto struct),避免含
sync.Mutex等不可序列化字段
典型用法:
err := backoff.Retry(func() error {<br> resp, err := client.Charge(ctx, req)<br> if err != nil {<br> s, ok := status.FromError(err)<br> if ok && (s.Code() == codes.Unavailable || s.Code() == codes.DeadlineExceeded) {<br> return err<br> }<br> return backoff.Permanent(err)<br> }<br> return nil<br>}, backoff.WithContext(backoff.NewExponentialBackOff(), ctx))
重试前必须和服务端幂等性机制联动
客户端再严谨的重试逻辑,若服务端没做幂等校验,就是危险操作。比如转账被重试两次,服务端没查idempotency_key就执行扣款,钱就没了。落地时:
- HTTP场景:客户端在Header加
X-Idempotency-Key: uuid,服务端用Redis缓存该key对应的结果(带TTL) - gRPC场景:proto message里定义
string idempotency_key = 1;字段,服务端方法开头校验该key是否已存在 - 别依赖“服务端说幂等就真幂等”——要验证:同一
idempotency_key连续发两次请求,响应是否完全一致(含status、body、headers) - 幂等键生成不能只用UUID——需包含业务维度,比如
"charge_" + userID + "_" + orderID,避免不同用户撞key
重试本身解决不了重复提交,它只是把压力从“一次失败”转移到“多次成功”,而这个转移是否安全,全看服务端有没有守住幂等这一道门。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











