MongoDB Retryable Writes 是什么?和幂等设计有什么关系?
简化版
Retryable Writes 允许驱动在网络抖动等可重试错误下自动重试某些写操作,降低“写成功但响应丢失”的不确定性。但它不能替代业务幂等,涉及扣款、下单等场景仍要设计业务唯一键和幂等表。
详细版
分布式系统里,客户端发出写请求后可能遇到网络断开、主节点切换等情况。客户端不知道写入到底失败了,还是已经成功但响应没回来。Retryable Writes 通过会话和操作标识,让支持的写操作可以安全重试。
- 它主要解决瞬时故障下的写入重试问题。
- 支持范围有限,不是所有写操作和部署形态都支持。
- 它依赖驱动、服务端版本、复制集或分片集群等条件。
- 业务层仍要考虑幂等,比如订单号唯一、请求号唯一、状态机防重复。
- 面试时要区分“数据库协议层重试”和“业务语义层幂等”。
完整版教学
一、为什么写操作最怕“结果未知”
读请求失败通常可以再读一次,最坏是延迟增加;写请求失败更麻烦,因为客户端可能不知道数据库到底有没有执行成功。比如应用发起插入订单,网络在服务端提交后断开,客户端没收到确认。如果应用直接重试,可能插入两笔订单;如果不重试,可能告诉用户失败但数据库里其实成功了。Retryable Writes 的目标就是缓解这种“成功与否未知”的灰色状态。
客户端 -> MongoDB:insert order
MongoDB:写入成功
网络断开:ACK 丢失
客户端:到底该不该重试?
记忆钩子:Retryable Writes 解决“同一次数据库写能不能安全再发”,幂等解决“同一个业务动作能不能安全再做”。
二、Retryable Writes 的基本思路
驱动会为可重试写操作带上会话信息和操作编号。服务端看到重试请求时,可以识别它是否属于同一个逻辑写操作,从而避免重复执行或返回之前的结果。这个机制需要服务端保存一定的重试历史,也需要部署形态支持复制集或分片集群。它不是简单粗暴地“失败就再发一次”,而是“带身份的再发一次”。
// 连接串中通常默认或显式启用
mongodb://host1,host2/app?retryWrites=true&w=majority
三、它能处理哪些典型故障
Retryable Writes 常见处理的是短暂网络错误、主节点选举导致的临时不可写、响应丢失等情况。比如主节点刚完成写入但响应没回到客户端,驱动重试后服务端能识别同一操作。再比如写入发到旧主节点时发生选举,驱动可以刷新拓扑并向新主节点重试。它提升的是客户端面对瞬时故障时的成功率和确定性,但不保证所有失败都能恢复。
| 场景 | Retryable Writes 是否有帮助 | 说明 |
|---|---|---|
| ACK 丢失 | 有帮助 | 可识别同一操作 |
| 短暂网络抖动 | 有帮助 | 驱动可自动重试 |
| 业务重复提交 | 不足够 | 要业务幂等 |
| 逻辑校验失败 | 无帮助 | 重试仍会失败 |
四、为什么不能替代业务幂等
数据库只知道“这是同一个写操作的重试”,但不一定知道“这是同一个业务动作”。用户连续点击两次提交订单,可能是两个不同 HTTP 请求,驱动层看不到它们是同一业务意图。支付回调重复发送、消息队列重复投递也是同理。业务幂等要靠业务唯一键,比如 requestId、orderNo、paymentNo,并用唯一索引或状态机保证重复请求不会产生重复结果。
db.orders.createIndex({ orderNo: 1 }, { unique: true })
// 重复请求用同一个 orderNo,第二次插入会被唯一索引挡住
db.orders.insertOne({ orderNo: "O202607300001", amount: 199, status: "CREATED" })
五、带数字看重复写入风险
假设一个下单接口每天 100 万次请求,网络异常率只有 0.01%,也意味着每天可能有 100 次处在“客户端不知道写入结果”的状态。如果没有重试和幂等设计,这 100 次里一部分会变成重复订单,一部分会变成用户看到失败但后台成功。数量看起来小,但出现在支付、库存、优惠券上就是事故。因此可靠写入设计不能只看平均成功率,要看异常路径是否可控。
1000000 次/天 × 0.01% = 100 次/天结果未知
每次结果未知都需要可重试 + 可幂等的处理路径
六、实战设计:数据库重试 + 业务唯一键
比较稳的设计通常是两层:驱动层启用 Retryable Writes,应对基础设施抖动;业务层使用唯一请求号或业务单号,应对重复提交。对于状态更新,还要用条件更新保护状态机,例如只允许 CREATED -> PAID,重复支付回调不会把状态乱改。这样即使请求被网关重试、消息被重复投递、驱动自动重试,最终业务结果仍然只发生一次。
db.payments.updateOne(
{ paymentNo: "P001", status: "CREATED" },
{ $set: { status: "PAID", paidAt: new Date() } }
)
七、常见误区与追问
- 误区:开启 Retryable Writes 就不需要幂等。 它处理数据库写操作重试,不处理用户重复提交和消息重复投递。
- 误区:失败重试一定安全。 没有操作标识和业务唯一键时,重试可能制造重复数据。
- 误区:所有写操作都能自动重试。 支持范围受操作类型、驱动版本和部署形态限制,要查具体能力。
- 追问:订单接口怎么防重复? 使用业务订单号或请求号唯一索引,并用状态机条件更新保护重复回调。
- 追问:
w=majority和 retry 有什么关系? majority 提升写确认可靠性,retry 处理可重试错误,两者关注点不同但常一起使用。
八、加强记忆
这题用“两层安全网”记:第一层是 MongoDB Retryable Writes,解决网络抖动、主从切换、ACK 丢失时同一次写操作的安全重试;第二层是业务幂等,解决同一个业务动作被重复提交。面试里不要把两者混成一个东西,先讲结果未知问题,再讲会话和操作编号,最后落到订单号唯一、支付状态机这些业务设计。