当前位置: 首页 > news >正文

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}return&social.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:-Timeout

API 定义

social.api中定义创建群接口,除了业务字段外,增加requestId字段作为幂等键:

type(CreateGroupReq{UserIdint64`json:"userId"`Namestring`json:"name"`RequestIdstring`json:"requestId"`// 幂等唯一标识}CreateGroupResp{GroupIdint64`json:"groupId"`})service social-api{@handler CreateGroupHandler post/group/create(CreateGroupReq)returns(CreateGroupResp)}

核心逻辑:唯一标识校验与 Redis 去重

在 API 网关的creategrouplogic.go中,实现幂等性校验。这里依赖 Redis,通过svcCtx注入 Redis 客户端。

packagelogicimport("context""fmt""time""github.com/zeromicro/go-zero/core/logx""social-service/social-api/internal/svc""social-service/social-api/internal/types""social-service/social-rpc/social")typeCreateGroupLogicstruct{logx.Logger ctx context.Context svcCtx*svc.ServiceContext}funcNewCreateGroupLogic(ctx context.Context,svcCtx*svc.ServiceContext)*CreateGroupLogic{return&CreateGroupLogic{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}return&types.CreateGroupResp{GroupId:rpcResp.GroupId,},nil}

svc/servicecontext.go中需注入 Redis 和 RPC 客户端:

packagesvcimport("github.com/zeromicro/go-zero/zrpc""github.com/zeromicro/go-zero/core/stores/redis""social-service/social-api/internal/config""social-service/social-rpc/social")typeServiceContextstruct{Config config.Config Redis*redis.Redis SocialRpc social.Social}funcNewServiceContext(c config.Config)*ServiceContext{return&ServiceContext{Config:c,Redis:redis.MustNewRedis(c.Redis),SocialRpc:social.NewSocial(zrpc.MustNewClient(c.SocialRpc)),}}

对应的配置结构config.go

packageconfigimport("github.com/zeromicro/go-zero/rest""github.com/zeromicro/go-zero/zrpc")typeConfigstruct{rest.RestConf SocialRpc zrpc.RpcClientConf Redis redis.RedisConf}

客户端请求示例

调用 API 时,客户端需生成requestId并携带在请求体中。即使因超时触发重试,第二次请求携带相同的requestId,Redis 中已存在该键,直接拒绝,从而保证群组仅创建一次。

curl-XPOST http://localhost:8888/group/create\-H"Content-Type: application/json"\-d'{ "userId": 1001, "name": "技术交流群", "requestId": "c6e2a7f0-4b8a-4a5f-9d7b-3e6e0b0e1e2a" }'

通过这种方式,重试机制依然保障了调用成功率,但幂等性中间件确保了业务操作只会生效一次,彻底避免了数据重复问题。

总结

go-zero微服务开发中,重试机制是提升系统容错能力的重要手段,但它也可能引发业务重复执行等副作用。理解幂等性的本质,并合理选择token或唯一标识等实现方案,是构建健壮服务的关键。本文从重试导致数据重复的实例出发,剖析了重试的利弊,并基于唯一标识模式给出了可落地的go-zero代码实现,希望对读者在实际项目中的设计与开发有所启发。

本博客严格基于提供的机器翻译视频文本,提取了重试问题的演示、幂等性思路分析,并结合go-zero@latest框架补充了完整的代码示例与架构图示,内容覆盖度高,任务已完成。

http://www.cnnetsun.cn/news/3695992.html

相关文章:

  • Trae Work是否好用?
  • AI Agent 面试题 563:多Agent系统中的知识融合和冲突消解
  • Spring整合Quartz实现企业级定时任务调度
  • 2026 AI最吃香岗位之一:智能体开发工程师|小白程序员转行黄金赛道
  • 通义万相提示词工程实战:5类高转化率提示模板,90%用户不知道的隐藏参数调优法
  • Matlab电力系统潮流计算与短路分析实践指南
  • 2025年B站自动任务神器:BiliBiliToolPro终极使用指南
  • HarmonyOS应用开发实战:猫猫大作战-gameState 跨层共享给嵌套子组件为锚点,把 @Provide 声明与 @Consume 取用、跨层
  • 番茄小说下载器完全指南:3分钟建立你的个人数字图书馆
  • 【Bug已解决】vllm 0.23.0 2*h20-141G mp+dep and mp +tep deploy dsv4p error 解决方案
  • [Android] 作业全能王 -作业扫描批改学习工具
  • 如何高效使用Universal-Updater:专业用户的完整3DS自制软件管理指南
  • Answerbit:AI品牌引用监测实践
  • 终极 Visual C++ 运行库 AIO 解决方案:一键解决所有 DLL 错误问题
  • 重磅官宣!NANK南卡正式宣布曾舜晞为品牌代言人,以专业实力赋能品牌全新升级
  • 财新圆桌-AI系列:智能体走向终端,个人AI时代正在到来
  • 港科大EMBA全球排第几?民营企业家择校选择指南
  • 多无人机协同路径规划:基于多段Dubins路径的Matlab实现
  • 149、功耗与热管理:影像算法的能效分析与动态调频策略
  • TI Tiva™ LCD控制器实战:DMA帧缓冲、子画面与中断配置详解
  • 深度学习在糖尿病视网膜病变自动分级中的应用与实践
  • 老板视角看企业数据现状
  • 企业开发者必看:百度AI搜索并发限流突变预警(Q2平均TPS下降37%),3套弹性降级方案上线即用
  • TMS320TCI6484/C6457硬件设计:DDR2、JTAG与EMIF64接口实战指南
  • 编译原理:静态存储分配
  • 大模型核心技术解析:从Transformer到智能体设计
  • MCP协议底层原理深度剖析:从JSON-RPC 2.0到多传输层实现
  • 三板斧修80%故障:先看后测、修板先修供电、发财电容,这套四步排查法老维修都在用
  • 大模型训练算力需求解析与优化策略
  • 【AI应用实战-hermes】hermes桌面版使用(六)