我正在golang中开发一个Web应用程序,并使用单个MongoDB实例作为数据存储。我有应该独占执行的代码。由于我的Web应用程序在两个不同的服务器上运行,我无法使用golang同步工具。
想法是通过锁定文档来使用MongoDB,但我不知道是否可能,如果可能,该怎么做?
答案 0 :(得分:4)
事先注意:使用Redis将是分布式锁定的更好,更有效的选择。
但如果您仍想使用MongoDB,请继续阅读。
以下解决方案的一些注释:
以下所有解决方案都是安全的,即使您拥有多个MongoDB服务器(共享群集)也能正常工作,因为以下解决方案都不依赖于简单读取;并且所有写入(例如insert
或update
)都会转到主实例。
如果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。
如果您可以进一步详细说明您的流程,社区可能会为您提供更具体的答案。