如何通过MongoDB同步两台服务器上的Golang应用并实现排他执行
用MongoDB文档锁实现跨服务器的Golang代码排他执行
完全可行!MongoDB的原子操作和文档锁机制刚好能解决你这种跨服务器的排他执行需求——毕竟你用的是单实例MongoDB,天然支持强一致性读写,完美适配你的场景。核心思路是利用findOneAndUpdate这个原子操作来“抢锁”:只有成功更新锁文档的实例能获得执行权限,执行完成后主动释放锁,还能通过过期时间防止进程崩溃后锁被永久占用。
具体实现步骤
1. 定义锁文档结构
首先在Golang里定义对应MongoDB的锁文档结构体,包含锁ID、持有者标识(用来区分不同服务器实例)和过期时间:
import ( "time" "go.mongodb.org/mongo-driver/bson" ) type MongoLock struct { ID string `bson:"_id"` // 锁的唯一标识,比如对应你的排他操作名称 Holder string `bson:"holder"` // 实例唯一标识,比如服务器IP/容器ID ExpiresAt time.Time `bson:"expiresAt"` // 锁过期时间,防止进程挂死导致锁永久占用 }
2. 实现抢锁逻辑
用findOneAndUpdate原子操作尝试获取锁:只有当锁不存在、或者锁已经过期时,才能成功更新锁文档并成为持有者。这个操作是原子性的,所以不会出现多个实例同时抢到锁的情况:
import ( "context" "fmt" "os" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func acquireLock(ctx context.Context, coll *mongo.Collection, lockID string, holder string, ttl time.Duration) (bool, error) { now := time.Now() expiresAt := now.Add(ttl) // 过滤条件:锁不存在,或者锁已过期 filter := bson.M{ "_id": lockID, "$or": []bson.M{ {"expiresAt": bson.M{"$lt": now}}, {"expiresAt": bson.M{"$exists": false}}, }, } // 更新操作:设置当前持有者和过期时间 update := bson.M{ "$set": bson.M{ "holder": holder, "expiresAt": expiresAt, }, } // 开启Upsert:如果锁文档不存在就自动创建 opts := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After) var acquiredLock MongoLock err := coll.FindOneAndUpdate(ctx, filter, update, opts).Decode(&acquiredLock) if err != nil { return false, fmt.Errorf("failed to attempt lock acquisition: %w", err) } // 确认当前实例确实抢到了锁(防止刚好过期瞬间被其他实例抢注) return acquiredLock.Holder == holder, nil }
3. 实现释放锁逻辑
执行完排他代码后,主动释放锁——这里选择更新过期时间为当前时间,比直接删除文档更安全(避免并发删除的冲突):
func releaseLock(ctx context.Context, coll *mongo.Collection, lockID string, holder string) error { // 只有当前持有者才能释放锁 filter := bson.M{ "_id": lockID, "holder": holder, } update := bson.M{ "$set": bson.M{"expiresAt": time.Now()}, } _, err := coll.UpdateOne(ctx, filter, update) return err }
4. 实际使用示例
在需要排他执行的代码块前后调用抢锁和释放锁,记得用defer确保锁一定会被释放(即使业务逻辑出错):
import "log" func runExclusiveTask(ctx context.Context, lockColl *mongo.Collection) error { lockID := "user-data-sync-lock" // 给你的排他操作起个唯一锁ID // 用服务器唯一标识作为holder,比如读取环境变量里的服务器ID或IP holder := fmt.Sprintf("server-%s", os.Getenv("SERVER_INSTANCE_ID")) lockTTL := 5 * time.Minute // 设置足够覆盖任务最长执行时间的过期时间 // 尝试抢锁 lockAcquired, err := acquireLock(ctx, lockColl, lockID, holder, lockTTL) if err != nil { return fmt.Errorf("lock acquisition failed: %w", err) } if !lockAcquired { return fmt.Errorf("could not acquire lock: another server is executing the task") } // 确保锁被释放,即使任务执行出错 defer func() { if err := releaseLock(ctx, lockColl, lockID, holder); err != nil { log.Printf("warning: failed to release lock - %v", err) } }() // -------------------------- // 这里写你的排他执行代码 log.Println("Starting exclusive task execution...") // 比如:批量更新用户数据、生成报表等需要排他的操作 // -------------------------- return nil }
关键注意事项
- 设置合理的TTL:TTL要比你的任务最长执行时间长一些,防止任务还在执行就被判定为过期,导致多个实例同时执行。
- Holder必须唯一:每个服务器实例的holder不能重复,否则会出现误释放其他实例锁的情况,建议用服务器IP、容器ID或K8s Pod ID作为holder。
- 处理抢锁失败场景:如果抢锁失败,可以添加重试逻辑(比如指数退避),或者直接返回错误提示用户稍后再试。
- 单实例MongoDB的可靠性:因为你用的是单实例MongoDB,不存在分布式锁的一致性问题,这个方案的可靠性很高,不用担心脑裂等问题。
内容的提问来源于stack exchange,提问作者Rafael Bogaveev
相关产品推荐
相关产品推荐

