Go-Zero项目开发35: 微服务重试与幂等性实践

📅 2026/7/28 1:26:21
Go-Zero项目开发35: 微服务重试与幂等性实践
纲要重试机制引发的数据重复问题问题复现与根因分析社交服务“创建群”场景超时与重试配置重复创建的数据库表现重试的“双刃剑”收益与风险优点可能引入的问题幂等性解决重复执行的关键什么是幂等性幂等性的应用场景幂等性常见实现方案token方式唯一标识方式其他方案Go-Zero 中基于唯一标识的幂等性实现项目结构概览超时与重试配置API 定义核心逻辑唯一标识校验与 Redis 去重客户端请求示例总结重试机制引发的数据重复问题在微服务架构中为了保证服务调用的可靠性我们通常会在 RPC 客户端加入重试机制。当调用因网络抖动、服务暂时不可用等原因失败时自动重试可以提升请求的成功率。但如果被调用的业务接口不具备幂等性重试就会带来数据重复的风险。下面通过一个“社交服务-创建群”的场景来演示这一现象。问题复现与根因分析我们以go-zero框架构建的社交服务为例。服务分为social-rpc提供群组创建等内部 RPC和social-api对外 HTTP 网关。正常情况下用户通过 API 发起创建群请求API 会调用 RPC 完成持久化。为了模拟偶发延迟我们在 RPC 的创建群业务逻辑中人为加入 2 秒延迟并将 API 调用 RPC 的超时时间从默认 2 秒调整为 1 秒同时为 RPC 客户端启用重试超时时重试一次。超时与重试配置API 服务的配置文件etc/social-api.yaml中设置 RPC 客户端超时与重试Name:social-apiHost:0.0.0.0Port:8888SocialRpc:Etcd:Hosts:-127.0.0.1:2379Key:social.rpcTimeout:1000# 1 秒超时Retry:Max:1# 最多重试 1 次Conditions:-Timeout# 超时异常时重试RPC 服务端etc/social-rpc.yaml同样可以配置自身超时这里为 2 秒以上但实际被 API 端 1 秒超时覆盖。服务逻辑模拟延迟RPC 端的创建群逻辑internal/logic/creategrouplogic.go中模拟耗时操作func(l*CreateGroupLogic)CreateGroup(in*social.CreateGroupReq)(*social.CreateGroupResp,error){// 模拟偶发延迟time.Sleep(2*time.Second)// 正常业务创建群写入数据库group:model.Group{Name:in.Name,OwnerId:in.UserId,}_,err:l.svcCtx.GroupModel.Insert(l.ctx,group)iferr!nil{returnnil,err}returnsocial.CreateGroupResp{GroupId:group.Id},nil}重试导致的重复创建启动服务后调用创建群 API。由于 API 端 1 秒超时小于 RPC 实际耗时 2 秒第一次调用触发超时重试机制立即发起第二次调用。此时第一次调用其实还在执行中最终两次调用都成功写入了数据库。观察数据库group表会发现同一条请求产生了多条记录。这说明重试虽然提升了调用成功率但对非幂等操作会造成业务数据重复。重试的“双刃剑”收益与风险优点提高系统容错能力应对瞬时网络故障。结合熔断、降级可以防止级联雪崩。可能引入的问题业务重复执行如上述示例重试导致创建多个群。服务压力增大多次重试加重被调用方负担可能加剧延迟。并发冲突多个服务实例同时重试可能引发锁竞争或数据不一致。延迟累积不当的重试次数和间隔会使请求响应时间显著增加。因此重试并非万能必须与幂等性设计结合使用。对于重复提交敏感的业务需要保证“多次调用一次生效”。幂等性解决重复执行的关键什么是幂等性幂等性是指对同一操作执行多次所产生的结果与执行一次完全相同不会因重复调用而产生副作用。例如一次抢购请求用户可能因卡顿多次点击按钮但系统最终只会生成一笔有效订单。幂等性的价值避免重复操作造成的数据污染。提高系统容错性允许安全重试。增强系统可靠性简化异常处理逻辑。典型应用场景表单重复提交如订单、评论。秒杀/抢购中的重复请求。超时重试场景下的资源创建、状态变更。幂等性常见实现方案token方式客户端在进入业务表单前先向服务端申请一个token服务端将其存入 Redis 等缓存并返回。客户端提交业务请求时携带该token服务端校验 Redis 中是否存在存在则删除token并执行业务不存在则直接忽略请求。这种方式适合表单提交等场景需要一次额外的token获取步骤。RedisServerClientRedisServerClientalt[token 存在][token 不存在]1. 获取 token生成并存储 token返回 token2. 提交业务请求 (携带 token)校验并删除 token执行业务逻辑返回成功忽略请求 (幂等)唯一标识方式由客户端为每次请求生成一个全局唯一的标识如 UUID、雪花 ID该标识随请求一起发送。服务端接收到请求后以该唯一标识为键查询缓存如 Redis如果不存在则继续执行业务并将标识写入缓存如果已存在则判定为重复请求直接返回已有结果或忽略。这种方案无需额外的token获取步骤更适合微服务间 RPC 调用的幂等控制。RedisServerClientRedisServerClientalt[requestId 不存在][requestId 已存在]生成唯一请求 ID请求业务 (携带 requestId)查询 requestId 是否存在执行业务逻辑存储 requestId (设置过期时间)返回成功忽略请求 (幂等)其他方案MVCC多版本并发控制通过版本号或时间戳实现乐观锁如更新库存时携带版本号。去重表在数据库中建立唯一索引利用数据库约束拒绝重复数据。分布式锁以业务唯一键加锁保证同一时刻只有一个请求执行。在go-zero实战中我们选择唯一标识方案实现幂等性因为它对业务侵入小且无需额外前置请求。Go-Zero 中基于唯一标识的幂等性实现项目结构概览示例项目基于go-zero框架采用 API 网关 RPC 服务分层。结构如下social-service/ ├── social-api/ # HTTP API 网关 │ ├── etc/ │ │ └── social-api.yaml # 网关配置 │ ├── internal/ │ │ ├── config/ │ │ │ └── config.go │ │ ├── handler/ │ │ │ └── creategrouphandler.go │ │ ├── logic/ │ │ │ └── creategrouplogic.go │ │ └── svc/ │ │ └── servicecontext.go │ └── social.api # API 描述文件 ├── social-rpc/ # RPC 服务 │ ├── etc/ │ │ └── social-rpc.yaml │ ├── internal/ │ │ ├── config/ │ │ │ └── config.go │ │ ├── logic/ │ │ │ └── creategrouplogic.go │ │ ├── server/ │ │ │ └── socialrpcserver.go │ │ └── svc/ │ │ └── servicecontext.go │ └── social.proto # Proto 定义 └── go.mod超时与重试配置在 API 网关的配置文件social-api.yaml中为 RPC 客户端配置超时和重试条件。当调用超时时go-zero客户端将根据配置自动重试此处仅为演示问题后续通过幂等性规避重复写入。Name:social-apiHost:0.0.0.0Port:8888SocialRpc:Etcd:Hosts:-127.0.0.1:2379Key:social.rpcTimeout:1000Retry:Max:1Conditions:-TimeoutAPI 定义在social.api中定义创建群接口除了业务字段外增加requestId字段作为幂等键type(CreateGroupReq{UserIdint64json:userIdNamestringjson:nameRequestIdstringjson:requestId// 幂等唯一标识}CreateGroupResp{GroupIdint64json:groupId})service social-api{handler CreateGroupHandler post/group/create(CreateGroupReq)returns(CreateGroupResp)}核心逻辑唯一标识校验与 Redis 去重在 API 网关的creategrouplogic.go中实现幂等性校验。这里依赖 Redis通过svcCtx注入 Redis 客户端。packagelogicimport(contextfmttimegithub.com/zeromicro/go-zero/core/logxsocial-service/social-api/internal/svcsocial-service/social-api/internal/typessocial-service/social-rpc/social)typeCreateGroupLogicstruct{logx.Logger ctx context.Context svcCtx*svc.ServiceContext}funcNewCreateGroupLogic(ctx context.Context,svcCtx*svc.ServiceContext)*CreateGroupLogic{returnCreateGroupLogic{Logger:logx.WithContext(ctx),ctx:ctx,svcCtx:svcCtx,}}func(l*CreateGroupLogic)CreateGroup(req*types.CreateGroupReq)(resp*types.CreateGroupResp,errerror){// 1. 幂等性校验以 requestId 为键ifreq.RequestId{returnnil,fmt.Errorf(requestId 不能为空)}key:fmt.Sprintf(idempotent:create_group:%s,req.RequestId)// 尝试设置 Redis 键NX 表示仅当不存在时设置EX 设置过期时间避免长期占用ok,err:l.svcCtx.Redis.SetNXEx(l.ctx,key,1,60*time.Second)iferr!nil{returnnil,fmt.Errorf(redis 操作失败: %w,err)}if!ok{// requestId 已存在说明是重复请求直接返回或可查询上次结果返回l.Logger.Infof(幂等拦截: requestId%s 已处理,req.RequestId)returnnil,fmt.Errorf(请求正在处理或已处理完毕请勿重复提交)}// 2. 调用 RPC 创建群rpcResp,err:l.svcCtx.SocialRpc.CreateGroup(l.ctx,social.CreateGroupReq{UserId:req.UserId,Name:req.Name,})iferr!nil{// 业务失败时可删除 Redis 键允许客户端修改后重新提交// l.svcCtx.Redis.Del(l.ctx, key)returnnil,err}returntypes.CreateGroupResp{GroupId:rpcResp.GroupId,},nil}在svc/servicecontext.go中需注入 Redis 和 RPC 客户端packagesvcimport(github.com/zeromicro/go-zero/zrpcgithub.com/zeromicro/go-zero/core/stores/redissocial-service/social-api/internal/configsocial-service/social-rpc/social)typeServiceContextstruct{Config config.Config Redis*redis.Redis SocialRpc social.Social}funcNewServiceContext(c config.Config)*ServiceContext{returnServiceContext{Config:c,Redis:redis.MustNewRedis(c.Redis),SocialRpc:social.NewSocial(zrpc.MustNewClient(c.SocialRpc)),}}对应的配置结构config.gopackageconfigimport(github.com/zeromicro/go-zero/restgithub.com/zeromicro/go-zero/zrpc)typeConfigstruct{rest.RestConf SocialRpc zrpc.RpcClientConf Redis redis.RedisConf}客户端请求示例调用 API 时客户端需生成requestId并携带在请求体中。即使因超时触发重试第二次请求携带相同的requestIdRedis 中已存在该键直接拒绝从而保证群组仅创建一次。curl-XPOST http://localhost:8888/group/create\-HContent-Type: application/json\-d{ userId: 1001, name: 技术交流群, requestId: c6e2a7f0-4b8a-4a5f-9d7b-3e6e0b0e1e2a }通过这种方式重试机制依然保障了调用成功率但幂等性中间件确保了业务操作只会生效一次彻底避免了数据重复问题。总结在go-zero微服务开发中重试机制是提升系统容错能力的重要手段但它也可能引发业务重复执行等副作用。理解幂等性的本质并合理选择token或唯一标识等实现方案是构建健壮服务的关键。本文从重试导致数据重复的实例出发剖析了重试的利弊并基于唯一标识模式给出了可落地的go-zero代码实现希望对读者在实际项目中的设计与开发有所启发。本博客严格基于提供的机器翻译视频文本提取了重试问题的演示、幂等性思路分析并结合go-zerolatest框架补充了完整的代码示例与架构图示内容覆盖度高任务已完成。