开放问答
gRPC client-streaming 客户端中途断连,服务端收尾写库该不该继续,重试如何幂等
该问题的正文、回答与评论。
提问者 谢若衡-958 · 状态 open
gRPCclient-streaming幂等事务重试Go
【背景】 我用 gRPC-Go v1.62 + Go 1.21 提供一个 client-streaming 接口:客户端分批 send 一批「明细行」(约 200~2000 行),CloseSend 后服务端才开始真正处理——逐行校验 → 批量写 MySQL(单事务)→ 返回汇总结果。典型场景是「批量导入 / 批量下单」这类收尾才落库的接口。日均约 30 万次调用,P99 正常 <800ms。
【现象 / 卡点】 上线后偶发两类情况,我都想不通该怎么定规则:
- 客户端在 send 阶段就断连(网络抖动、客户端超时、用户取消),服务端
Recv()返回context.Canceled。此时我已经Recv到一半的行,内存里攒着部分数据——该不该继续校验 + 写库? 继续写会产生「半份数据」的脏事务;不写又可能丢了客户端其实「以为发完了」的请求。 - 更糟的是:服务端其实已经写库成功,正准备
Send汇总结果时客户端断了(Send报错 / context 已取消)。客户端那边没拿到结果,于是按失败重试,把同一批明细又发了一次 → 重复落库 / 重复下单。
【已尝试的排查与结果】
- 在 handler 里监听
ctx.Done(),一旦取消就return,结果发现「已经写了一半事务」的分支处理得很乱,容易漏回滚。 - 客户端重试时带了业务
requestId,但服务端目前没有用它做去重键,导致重试重复写。 - 查过 807 号那篇 GOAWAY 连接复用的文章,但那是客户端连接层的问题,没讲服务端收尾阶段遇到取消的取舍。
【期望得到的答案形态】 希望有人能给一套可落地的规则(最好带代码片段或配置):
- 收尾阶段检测到 ctx 取消时,「已落库 / 未落库 / 落了一半」三种状态分别该怎么处理,判据是什么;
- 如何用
requestId在服务端做幂等去重(落哪张表、唯一键怎么设、TTL 怎么定、和事务怎么配合),让客户端重试不再重复写; - 「客户端会重试」和「客户端彻底放弃」在服务端能不能区分,如果不能,默认该按哪种假设处理。
已采纳
回答者
回答正文
评论
暂无评论。