bigywater.tech
MongoDB · 63

可重试写入:txnNumber 怎么避免重复执行

网络断开后重试一次 $inc 会不会加两次?可重试写入是怎么避免重复执行的?这篇用 failpoint 制造连接断开,再手写 OP_MSG 把同一个请求发两次,看服务端的去重。

n = 1
连接断开后驱动自动重试一次 $inc,最终计数器的值(没有加两次)
retriedStmtIds
服务端识别出重发的语句并返回缓存结果时的响应字段
2 条
两次不同的 txnNumber 对应的 oplog 条目数;重发没有产生新条目

驱动重试

实测 · 本机执行输出
  update_one 的结果:matched 1 modified 1;n = 1;驱动发出的 update 命令(命令名, txnNumber, 带 lsid): [('update', 2, True), ('update', 2, True)]
  (两次 update 的 txnNumber 相同——重试带着同一个事务号,服务端据此判断是不是重复请求)

连接中断时驱动自动重发写命令,带着同一个 lsid 和 txnNumber;retryWrites=False 则直接抛错。

服务端去重

实测 · 本机执行输出
  AutoReconnect: 127.0.0.1:9901: connection closed (configured timeouts: connectTimeoutMS: 20000.
  n = 1(这次故障发生在执行之前,没有加)

同一个 lsid + txnNumber 的重发,服务端不再执行,而是返回之前的结果并带上 retriedStmtIds。

靠什么

实测 · 本机执行输出
  第 1 次(txnNumber 100)          → n=1 nModified=1 retriedStmtIds=None;库里 n = 1
  重发同一个请求(txnNumber 100)        → n=1 nModified=1 retriedStmtIds=[0];库里 n = 1
  新请求(txnNumber 101)            → n=1 nModified=1 retriedStmtIds=None;库里 n = 2

靠 oplog 条目里的 lsid、txnNumber、stmtId 和 config.transactions 里每个会话最后的事务号。

批量

实测 · 本机执行输出
  oplog: op=u txnNumber=100 stmtId=0 o2={'_id': 1} o={'$v': 2, 'diff': {'u': {'n': 1}}}
  oplog: op=u txnNumber=101 stmtId=0 o2={'_id': 1} o={'$v': 2, 'diff': {'u': {'n': 2}}}
  config.transactions 里该会话的记录: txnNum=101,lastWriteOpTime=Timestamp(1791776225, 4)

批量 insert 的每条语句有自己的 stmtId,整批重发只会执行一次。

不能重试的

实测 · 本机执行输出
  第 1 次:n=3;重发:n=3 retriedStmtIds=[0, 1, 2];集合里文档数 3(只有 3 个,没有重复键错误)

multi 的 update 和 delete 不可重试,遇到连接断开直接报错。

独立实例

实测 · 本机执行输出
  update_many 遇到同样的连接断开:不重试,直接抛 AutoReconnect;发出的 update 命令 [('update', None, True)]
  delete_one:被重试后成功;发出的 delete 命令 [('delete', 1, True), ('delete', 1, True)]
  独立实例(非副本集)上 retryWrites=True 的 insert:成功(驱动检测到不支持就自动不用)
  # 直接带 txnNumber 的命令在独立实例上被拒绝
  test> try { db.runCommand({insert: "t", documents: [{x: 2}], lsid: {id: UUID()}, txnNumber: Long(1)}) } catch (e) { e.message }
  Transaction numbers are only allowed on a replica set member or mongos

事务号只允许副本集或 mongos,独立实例上驱动会自动关闭重试写。

分步演示

可重试写入
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。单节点副本集启用 enableTestCommands 以使用 failCommand;去重演示用手写的 OP_MSG 客户端
    • 没有验证:响应在网络上丢失(服务端已执行)的真实场景(failCommand 的 closeConnection 发生在执行之前,执行后的重发用手写客户端模拟);重试次数与 retryReads;故障切换(primary 变化)时的重试;事务号的保留时间(会话过期后去重信息的清理);分片集群里 mongos 的重试
    京ICP备2026067502号-1京公网安备11010502063487号