预先注意:使用Redis https://redis.io/将是分布式锁定更好、更有效的选择。
但如果您仍然想使用 MongoDB 来实现此目的,请继续阅读。
以下解决方案的一些注释:
使用文档的存在作为锁
最简单的方法是依赖 MongoDB,不允许存在 2 个具有相同 ID 的文档(在同一个集合中)。
因此,要获取锁,只需将文档插入指定的集合(例如locks
)与锁 ID。如果插入成功,则说明您成功获取了锁。如果插入失败,则说明您没有插入。要释放锁定,只需删除(移除)文档即可。
需要注意的一些事情:您必须释放锁,因为如果您不这样做,则尝试获取此锁的所有代码都将永远不会成功。因此释放锁应该使用延迟函数来完成(defer
)。不幸的是,这不能确保在出现某些通信错误(网络故障)时发布。
为了保证锁释放,您可以创建一个指定的索引文件过期 https://docs.mongodb.com/manual/tutorial/expire-data/,因此,如果 Go 应用程序在持有锁时出现任何问题,一段时间后锁会自动删除。
Example:
锁定文档不会事先插入。但需要一个索引:
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")
Pros:最简单的解决方案。
Cons:您只能为所有锁指定相同的超时,以后很难更改它(必须删除并重新创建索引)。
请注意,最后一个陈述并不完全正确,因为您不会被迫将当前时间设置为lockedAt
场地。例如。如果您设置的时间戳指向过去 5 秒,则锁定将在 25 秒后自动过期。如果将其设置为未来 5 秒,则锁定将在 35 秒后过期。
另请注意,如果一个 goroutine 获得了锁,并且没有任何问题,它需要将其保持超过 30 秒,则可以通过更新lockedAt
锁定文档的字段。例如。 20秒后,如果goroutine没有遇到任何问题,但需要更多时间来完成持有锁的工作,它可能会更新lockedAt
字段设置为当前时间,防止它被自动删除(从而为等待该锁的其他 goroutine 亮绿灯)。
使用预先创建的锁定文档和update()
另一种解决方案可能是拥有一个包含预先创建的锁定文档的集合。锁可以有一个 ID(_id
),以及一个状态,告诉它是否被锁定(locked
).
之前创建锁:
db.locks.insert({_id:"l1", locked:false})
要获取锁,请使用Collection.Update() https://godoc.org/github.com/globalsign/mgo#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
,因为占主导地位的决定了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},
})
Pros:您可以对不同的锁使用不同的超时,甚至可以对不同位置的相同锁使用不同的超时(尽管这是不好的做法)。
Cons:稍微复杂一点。
请注意,为了“延长锁的生命周期”,可以使用与上面描述的相同的技术,即,如果锁过期即将到来并且 goroutine 需要更多时间,它可能会更新lockedAt
锁定文档的字段。