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