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

Raf*_*eev 4 go mongodb mgo

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

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

icz*_*cza 5

Note beforehand: Using Redis would be a better and more efficient choice for distributed locking.

But if you still want to use MongoDB for this, read on.

Some notes to the solutions below:

  • All solutions below are safe and work even if you have multiple MongoDB servers (a shared cluster), because none of the solutions below rely on simple reads; and all writes (e.g. insert or update) go to the master instance.

  • If a goroutine fails to obtain a lock, it may decide to sleep a little (e.g. 1 second), then retry obtaining the lock. There should be a max retry count before giving up.


Using the existence of a document as the lock

Simplest would be to rely on MongoDB not allowing 2 documents with the same ID to exist (in the same collection).

So to acquire a lock, simply insert a document into a designated collection (e.g. locks) with the lock ID. If insertion succeeds, you successfully acquired the lock. If insertion fails, you did not. To release the lock, simply delete (remove) the document.

Some things to note: you MUST release the lock, because if you fail to do so, all code that attempts to acquire this lock will never succeed. So releasing the lock should be done using a deferred function (defer). Unfortunately this won't ensure the release in case of some communication error (network failure).

To have guarantee about lock release, you may create an index that specifies document expiration, so the locks would be deleted automatically after some time, should any problems arise in the Go app while it holds the lock.

Example:

Lock documents are not inserted prior. But an index is required:

db.locks.createIndex({lockedAt: 1}, {expireAfterSeconds: 30})
Run Code Online (Sandbox Code Playgroud)

Acquiring the lock:

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
}
Run Code Online (Sandbox Code Playgroud)

Releasing the lock:

err := c.RemoveId("l1")
Run Code Online (Sandbox Code Playgroud)

Pros: Simplest solution.

Cons: You can only specify the same timeout for all locks, harder to change it later (must drop and recreate the index).

Note that this last statement is not entirely true, because you are not forced to set the current time to the lockedAt field. E.g. if you set a timestamp pointing 5 seconds in the past, the lock will auto-expire after 25 seconds. If you set it 5 seconds to the future, the lock will expire after 35 seconds.

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

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

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

之前创建锁:

db.locks.insert({_id:"l1", locked:false})
Run Code Online (Sandbox Code Playgroud)

要获得锁定,请使用该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
}
Run Code Online (Sandbox Code Playgroud)

这是如何运作的?如果多个 Go 实例(甚至同一个 Go 应用程序中的多个 goroutine)尝试获取锁,则只有一个会成功,因为其余的选择器将返回mgo.ErrNotFound,因为占上风的那个将locked字段设置为true

一旦你完成了持有锁的工作,你必须释放锁:

err := c.UpdateId("l1", bson.M{
    "$set": bson.M{"locked": false},
})
Run Code Online (Sandbox Code Playgroud)

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

保证超时锁释放的示例:

锁定文档:

db.locks.insert({_id:"l1", locked:false})
Run Code Online (Sandbox Code Playgroud)

获取锁:

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
}
Run Code Online (Sandbox Code Playgroud)

释放锁:

err := c.UpdateId("l1", bson.M{
    "$set": bson.M{ "locked": false},
})
Run Code Online (Sandbox Code Playgroud)

优点:您可以对不同的锁使用不同的超时,甚至可以对不同位置的相同锁使用不同的超时(尽管这是不好的做法)。

缺点:稍微复杂一些。

请注意,为了“延长锁的生命周期”,可以使用与上面描述的相同的技术,即,如果锁到期临近并且 Goroutine 需要更多时间,它可能会更新lockedAt锁文档的字段。