MongoDB UpdateOne客户端超时但服务端已执行引发重复更新问题求解
问题本质说明
这是分布式系统下非常典型的非幂等操作重试导致的重复执行问题,核心原因是客户端无法仅凭错误信息判断「未收到响应的写请求」是否已经在服务端执行完成,该问题不属于MongoDB驱动缺陷,驱动无法在缺省情况下感知服务端的实际执行状态,因此不会默认做写操作的重试兼容。
可落地的规避方案
方案1:给自增操作增加幂等校验,从根本上避免重复执行
每次触发自增前生成一个全局唯一的请求标识(比如UUID、雪花ID),在自增的更新条件中增加对该标识的判断。
原自增逻辑示例:updateOne({_id: 业务记录ID}, {$inc: {count: 1}})
改造后的幂等逻辑:updateOne({_id: 业务记录ID, last_req_id: {$ne: 当前请求ID}}, {$inc: {count: 1}, $set: {last_req_id: 当前请求ID}})
改造后同一个请求就算重试多次,只有第一次会匹配到文档执行自增,后续请求都不会命中更新条件,不会出现重复自增问题。方案2:细化重试逻辑,只对确定未执行的请求做重试
可以通过Go Mongo驱动提供的错误判断方法,区分错误类型决定是否重试:- 如果是连接拒绝、握手失败、连接未就绪类的错误,可以安全重试,这类错误说明请求根本没有发送到MongoDB服务端
- 如果是
incomplete read of message header: context deadline exceeded这类写完请求、读响应阶段才出现的超时错误,不要直接重试,可先主动查询一次当前自增字段的值,或者查询对应的请求标识是否已经存在,确认操作未执行后再重试。
方案3:调整超时和连接策略,降低异常出现概率
你已经通过提前握手验证解决了握手挤占超时的问题,还可以补充两个优化点:- 适当拉长单次操作的超时时间,避免正常请求因为超时阈值设置过紧出现误判,100ms的阈值对于包含网络IO的数据库操作来说偏短,可以调整为300~500ms
- 重试间隔不要用固定的100ms,改用指数退避策略,避免数据库刚恢复时大量重试请求把数据库打挂。
方案4:使用MongoDB官方驱动的重试写配置(辅助方案)
官方Go Mongo驱动支持开启retryWrites配置,但是该配置仅能重试驱动确定未执行成功的写请求,对于你遇到的已发请求未收到响应的场景依旧无法处理,仅可作为辅助方案使用。
内容的提问来源于stack exchange,提问作者Michael Benford

