我需要使用MongoDB实现一些锁定机制,以防止数据不一致,但允许脏读。
只有在WRITE
锁定并没有READ
锁定的情况下才能获得WRITE
锁定。
只有在没有READ
锁定的情况下才能获得WRITE
锁定。
单个文档上可能存在多个并行READ
锁。
必须有某种超时机制:如果(无论出于何种原因)某个进程未释放其锁定,则应用程序必须能够恢复。
只需忽略查询中的所有锁即可进行脏读。
(WRITE
进程的饥饿不是本主题的一部分)
READ
和WRITE
锁定/为什么不仅仅使用WRITE
锁:我们假设有2个集合:contacts
和categories
。它是一种n-m关系,每个联系人都有一个类别ID数组。
READ
锁定:在向联系人添加类别时,我们必须确保此类别目前未被删除(这需要{{1}锁定,见下文)。并且由于同一文档中可能存在多个WRITE
锁,因此多个进程可以将此单个类别添加到多个联系人。
READ
锁定:删除类别时,我们必须先从所有联系人中删除类别ID。在此操作正在运行时,我们必须确保无法将此类别添加到任何联系人(此操作需要WRITE
锁定)。之后我们可以安全地删除类别文档。
这样,总会有一致的状态。
这是最难的部分。我已经尝试过两次实现它,但总是发现一些问题,这些问题似乎太难解决了。
基本思路:每次获取的锁都带有时间戳,直到此锁有效。如果此时间戳是过去的,我们可以忽略该锁定。当一个进程完成其任务时,它应该删除它的锁。
最大的挑战是,有多个READ
锁,其中每个READ
锁具有自己的超时,但多个READ
锁可以具有相同的超时值。释放READ
锁时,它必须只释放自己,所有其他READ
锁必须保留。
READ
{
_id: 1234,
lock: {
read: [
ISODate("2015-06-26T12:00:00Z")
],
write: null
}
}
可以包含或 lock.read
可以设置的元素。必须永远不可能同时设置!
对此的查询是可以的,有些可能会更容易一些(特别是"释放读锁和#34;)。但向他们展示它们的主要原因是我仍然不确定我是否还没有错过任何东西。
前言
lock.write
是当前时间。它曾用于忽略所有过期的锁。它还用于删除所有过期的读锁。ISODate("now")
用于指示此锁定何时到期,可以忽略/删除。 (例如ISODate("lock expiration")
)
获取now + 5 seconds
锁定
如果没有有效的写锁定,则插入读锁定。
READ
获取update(
{
_id: 1234,
$or: [
{ 'lock.write': null },
{ 'lock.write': { $lt: ISODate("now") } }
]
},
{
$set: { 'lock.write': null },
$push: { 'lock.read': ISODate("lock expiration") }
}
)
锁定
如果没有有效的读锁且没有有效的写锁定,则设置写锁定。
WRITE
发布update(
{
_id: 1234,
$and: [
$or: [
{ 'lock.read':{ $size: 0 } },
{ 'lock.read':{ $not: { $gte: ISODate("now") } } }
],
$or: [
{ 'lock.write': null },
{ 'lock.write': { $lt: ISODate("now") } }
]
]
},
{
$set: {
'lock.read': [],
'lock.write': ISODate("lock expiration")
}
}
)
锁定
使用其到期时间戳删除获取的读锁定。
READ
(update(
{
_id: 1234,
'lock.read': ISODate("lock expiration")
},
{
$unset: { 'lock.read.$': null }
}
)
update(
{
_id: 1234,
},
{
$pull: { 'lock.read': { $lt: ISODate("now") } }
}
)
update(
{
_id: 1234
},
{
$pull: { 'lock.read': null }
}
)
数组可能包含多个相同的时间戳,如果多个进程获得lock.read
锁定。虽然我们只需删除一个时间戳,这不会与READ
一起使用,但可以使用位置运算符$pull
。
此外,我还通过其他更新删除了所有过期的锁。我尝试过一些东西,但是无法将其减少到2次甚至1次更新。)
发布$
锁定
删除写日志。这里应该没什么好检查的。
WRITE
update(
{
_id: 1234
},
{
$set: { 'lock.write': null }
}
)
和READ
查询 WRITE
仅匹配,如果字段不包含任何内容{ $not: { $gte: ISODate("now") } }
。虽然它会匹配$gte: ISODate("now")
和不存在的字段以及一个空数组。
获取null
锁定
READ
获取update(
{
_id: 1234,
'lock.write': { $not: { $gte: ISODate("now") } }
},
{
$set: { 'lock.write': null },
$push: { 'lock.read': ISODate("lock expiration") }
}
)
锁定
WRITE
但仍然不知道"发布update(
{
_id: 1234,
'lock.write': { $not: { $gte: ISODate("now") } },
'lock.read': { $not: { $gte: ISODate("now") } }
},
{
$set: {
'lock.read': [],
'lock.write': ISODate("lock expiration")
}
}
)
锁定"查询...
我想到了某种具有超时时间戳和锁数的元组。但随后问题在于获取READ
锁定查询。
READ
锁定READ
这是有效的,因为{
_id: 1234,
lock: {
read: [
{ timeout: ISODate("2015-06-26T12:00:00Z"), process: ObjectId("...") }
],
write: null
}
}
由时间戳,机器ID,进程ID和计数器组成。这样就不可能创建多个相等的ObjectId
。长话短说:
获取ObjectIds
锁时,我们会插入一个包含超时时间戳和唯一READ
的文档。在释放它时,我们使用这种组合将其从阵列中移除。所以唯一有趣的问题是:
获取ObjectId
锁定
WRITE
发布update(
{
_id: 1234,
'lock.write': { $not: { $gte: 4 } },
'lock.read.timeout': { $not: { $gte: 4 } }
},
{
$set: {
'lock.read': [],
'lock.write': ISODate("lock expiration")
}
}
)
锁定
READ
正如您所看到的,我们现在只需要一个查询来删除我们对清理所有超时锁定的锁定。
唯一进程标识符非常重要,因为没有它,update(
{
_id: 1234,
},
{
$pull: {
'lock.read': {
$or: [
{ 'timeout': ISODate("lock expiration"), process: ObjectId("...") },
{ 'timeout': { $lt: ISODate("now") } }
]
}
}
}
)
操作可以在获取具有相同超时值的锁时删除另一个进程的锁。
下一步是删除$pull
字段,只使用能够容纳process
部分的ObjectId
。 (例如Mongodb: Perform a Date range query from the ObjectId in the mongo shell)
这是一个使用MongoDB的有效且无懈可击的实现吗?
如果"是":我可以以某种方式改善它吗? (至少"发布timeout
锁定"部分)
如果"不":它有什么问题?我错过了什么?
提前感谢您的帮助!