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

时间:2018-05-23 12:07:46

标签: mongodb go mgo

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

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

2 个答案:

答案 0 :(得分:4)

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

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

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

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

  • 如果goroutine无法获得锁定,它可能会决定睡一会儿(例如1秒),然后重试获取锁定。在放弃之前应该有最大重试次数。

使用文档的存在作为锁

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

因此,要获取锁定,只需将文档插入具有锁定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)尝试获取锁定,则只有一个会成功,因为其余的选择器将返回mgo.ErrNotFound,因为占优势的选择器设置{{1 }}字段到locked

一旦你拿着锁的东西,你必须释放锁:

true

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

使用超时保证锁定释放的示例:

锁定文件:

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

获取锁定:

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
}

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

缺点:稍微复杂一些。

注意,为了“延长锁定的生命周期”,可以使用上面描述的相同技术,也就是说,如果锁定到期并且goroutine需要更多时间,它可能会更新{{1}锁文档的字段。

答案 1 :(得分:0)

对于单个文档,更新Mongo are atomic中的操作。您的Web应用程序将同时接收文档的一致视图,因为"写"请求将立即更新所有字段或根本不更新。以上链接中的示例。

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

上述链接适用于商家,但需要全面了解,here's the mongo documentation page

如果您可以进一步详细说明您的流程,社区可能会为您提供更具体的答案。

相关问题