如何通过 MongoDB 在两个不同的服务器上同步两个应用程序 运行

How to synchronize two apps running in two different servers via MongoDB

我正在用 golang 开发一个 Web 应用程序并使用单个 MongoDB 实例作为数据存储。我有应该专门执行的代码。由于我的 Web 应用程序在两台不同的服务器上运行,因此我不能为此使用 golang 同步工具。

想法是通过锁定文档来使用MongoDB,但我不知道是否可行,如果可行,该怎么做?

预先注意:使用 Redis 将是分布式锁定更好、更有效的选择。

但如果您仍想为此使用 MongoDB,请继续阅读。

以下解决方案的一些注释:

  • 即使您有多个 MongoDB 服务器(共享集群),以下所有解决方案都是安全且有效的,因为以下解决方案中的 none 依赖于简单读取;并且所有写入(例如 insertupdate)都转到主实例。

  • 如果一个goroutine获取锁失败,它可能会决定休眠一点(比如1秒),然后重试获取锁。放弃前应该有一个最大重试次数。


以文档的存在为锁

最简单的方法是 MongoDB 不允许存在具有相同 ID 的 2 个文档(在同一集合中)。

因此,要获取锁,只需将文档插入具有锁 ID 的指定集合(例如 locks)。如果插入成功,则成功获得锁。如果插入失败,则说明您没有。要解除锁定,只需删除(移除)文档即可。

一些注意事项:你必须释放锁,因为如果你不这样做,所有试图获取这个锁的代码都不会成功。所以释放锁应该使用延迟函数(defer)来完成。不幸的是,这不能确保在某些通信错误(网络故障)的情况下发布。

为了保证锁的释放,你可以创建一个索引指定document expiration,这样锁会在一段时间后自动删除,如果Go应用程序在持有锁时出现任何问题。

示例:

事先没有插入锁定文档。但需要索引:

db.locks.createIndex({lockedAt: 1}, {expireAfterSeconds: 30})

获取锁:

sess := ... // Obtain a MongoDB session
c := sess.DB("").C("locks")

err := c.Insert(bson.M{
    "_id":      "l1",
    "lockedAt": time.Now(),
})
if err == nil {
    // LOCK OBTAINED! DO YOUR STUFF
}

解除锁定:

err := c.RemoveId("l1")

优点:最简单的解决方案。

缺点:您只能为所有锁指定相同的超时时间,以后很难更改它(必须删除并重新创建索引)。

请注意,最后一个陈述并不完全正确,因为您没有被强制将当前时间设置为 lockedAt 字段。例如。如果您将时间戳设置为过去 5 秒,则锁将在 25 秒后自动过期。如果你将它设置为未来 5 秒,锁将在 35 秒后过期。

另请注意,如果一个 goroutine 获得了一个锁,并且没有任何问题需要持有它超过 30 秒,这可以通过更新锁文档的 lockedAt 字段来完成。例如。 20 秒后,如果 goroutine 没有遇到任何问题,但需要更多时间来完成持有锁的工作,它可能会将 lockedAt 字段更新为当前时间,以防止它被自动删除(并因此亮起绿灯到等待该锁的其他 goroutines)。

使用预先创建的锁文件和update()

另一种解决方案可能是拥有一个包含预先创建的锁定文档的集合。锁可以有一个 ID (_id),以及一个表明它是否被锁定的状态 (locked)。

之前创建一个锁:

db.locks.insert({_id:"l1", locked:false})

要获得锁,请使用Collection.Update() 方法,在选择器中,您必须按 ID 和锁定状态进行过滤,其中状态必须是解锁状态。而更新值应该是一个$set操作,设置锁定状态为true.

err := c.Update(bson.M{
    "_id":    "l1",
    "locked": false,
}, bson.M{
    "$set": bson.M{"locked": true},
})
if err == nil {
    // LOCK OBTAINED! DO YOUR STUFF
}

这是如何工作的?如果多个 Go 实例(甚至同一个 Go 应用程序中的多个 goroutine)尝试获取锁,只有一个会成功,因为其余的选择器将 return mgo.ErrNotFound,因为占优势的集合locked 字段到 true.

一旦你拿着锁做了你的事情,你必须释放锁:

err := c.UpdateId("l1", bson.M{
    "$set": bson.M{"locked": false},
})

为了保证锁定释放,您可以在锁定时在锁定文档中包含一个时间戳。并且在尝试获取锁时,选择器还应该接受已锁定但早于给定超时(例如 30 秒)的锁。在这种情况下,更新还应设置锁定时间戳。

超时锁定释放保证示例:

锁定文件:

db.locks.insert({_id:"l1", locked:false})

获取锁:

err := c.Update(bson.M{
    "_id": "l1",
    "$or": []interface{}{
        bson.M{"locked": false},
        bson.M{"lockedAt": bson.M{"$lt": time.Now().Add(-30 * time.Second)}},
    },
}, bson.M{
    "$set": bson.M{
        "locked":   true,
        "lockedAt": time.Now(),
    },
})
if err == nil {
    // LOCK OBTAINED! DO YOUR STUFF
}

解除锁定:

err := c.UpdateId("l1", bson.M{
    "$set": bson.M{ "locked": false},
})

优点:您可以对不同的锁使用不同的超时时间,甚至可以在不同的地方对相同的锁使用不同的超时时间(尽管这将是一种不好的做法)。

缺点:稍微复杂一些。

注意,对于"extend the lifetime"的锁,可以使用与上面描述的相同的技术,即如果锁到期临近并且goroutine需要更多时间,它可能会更新lockedAt 锁定文档的字段。

对于单个文档,更新操作在Mongo are atomic。您的 Web 应用程序都将收到一致的文档视图,因为 "write" 请求将立即更新所有字段或 none 全部更新。以上示例 link.

如果网络应用程序实例在单个查询中更新多个文档(如果您使用 updateMany 对它们进行批处理),则原子操作不可用。您可以使用嵌入式文档来解决这个问题(link 以上)或 document, collection, or database locking 提供不同的读写锁。

以上link说到正题,但综合来看,here's the mongo documentation page

如果你能进一步详细说明你的流程,社区可能会给你一个更具体的答案。