用pymongo连接副本集+唯一索引+insert_one是实现高可用分布式锁最轻量可靠的方式;必须使用含全部节点的uri并指定replicaset参数,写操作走primary,释放锁需校验owner,超时清理由应用层主动执行。

直接用 PyMongo 连接副本集 + 唯一索引 + insert_one 尝试写入,是实现高可用分布式锁最轻量且可靠的方式。它不依赖额外服务(如 Redis),也不要求 MongoDB 开启事务——只要副本集能自动选主、客户端用正确 URI 连接,锁逻辑就能在主备切换后继续工作。
为什么不能用单节点直连地址写分布式锁
用 mongodb://host:port 这种单地址连接,一旦主节点宕机或发生主备切换,你的应用会卡在写操作上,insert_one 报 NotPrimaryError 或超时,锁获取失败。更糟的是,如果锁已写入旧主但未同步到新主,重启后可能产生双写冲突。
必须使用包含全部节点的 ConnectionStringURI,并显式指定 replicaSet 参数:
- PyMongo 会自动发现当前 Primary 节点,并把写请求路由过去
- 主备切换后,客户端在下一次操作前会重新探测拓扑,后续
insert_one自动发往新主 - 读操作(如检查锁状态)可配合
readPreference=secondaryPreferred分流,但写锁必须走 Primary
insert_one + 唯一索引是锁的核心机制
分布式锁本质是“抢写一个唯一键”。MongoDB 的原子性写入 + 唯一约束,天然适合这个场景。不要用 find_and_modify 或两阶段更新——它们在副本集里可能因网络分区导致状态不一致。
关键实操点:
- 集合必须建唯一索引:
collection.create_index("key", unique=True) - 加锁就是尝试插入:
collection.insert_one({"key": "order_123", "ts": time.time()}) - 成功返回说明抢锁成功;抛
DuplicateKeyError说明已被占用 - 不要在 catch 里 sleep 后重试——应由业务层控制重试策略和超时
释放锁必须用 delete_one,且带校验
只删 {"key": "order_123"} 是危险的。如果锁过期被别人续占,你删掉的可能是别人的锁。
安全释放需要附带持有者标识(比如进程 ID + 时间戳):
- 加锁时存入
{"key": "order_123", "owner": "pid-12345", "ts": 1725716580} - 释放时用
delete_one({"key": "order_123", "owner": "pid-12345"}) - 检查
result.deleted_count == 1,否则说明锁已失效或被抢占
注意:不要用 update_one(..., {"$unset": {...}}) 来“释放”,这会让锁记录残留,影响后续唯一性判断。
超时自动清理靠应用层,不是 MongoDB TTL
虽然 MongoDB 支持 TTL 索引自动删文档,但它只在后台线程触发,延迟不可控(默认 60 秒检查一次)。对分布式锁来说,这意味着锁可能滞留远超预期时间,阻塞业务。
正确做法是:
- 加锁时写入
"expire_at"字段(datetime类型) - 每次抢锁前,先用
delete_many({"key": "order_123", "expire_at": {"$lt": datetime.now()}})清理过期锁 - 这个清理操作本身也要防并发——建议用带条件的
delete_many,而非先查再删
副本集环境下,这个清理动作必须在 Primary 上执行,且要容忍 NotPrimaryError 并重试,否则可能漏清。











