MongoDB:实现读/写锁(互斥锁)
我需要为MongoDB实现一些锁定机制,以防止数据不一致,但允许脏读。
条件:
获取WRITE
锁才有可能,如果没有READ
锁和无WRITE
锁。
只有在没有WRITE
锁的情况下才能获取READ
锁。
单个文档上可以有许多并行READ
锁。
必须有某种超时机制:如果(由于某种原因)一些进程没有释放它的锁,那么应用程序必须能够恢复。
通过简单地忽略查询中的所有锁,脏读是可能的。
( WRITE
过程的饥饿不是这个话题的一部分)
为什么READ
和WRITE
锁定/为什么不仅使用WRITE
锁定:
我们假设,我们有2个集合: contacts
和categories
。 这是一种nm关系,每个联系人都有一个类别ID数组。
READ
锁定:向联系人添加类别时,我们必须确保此类别不会被删除(这需要WRITE
锁,请参阅下文)。 由于同一文档上可能有多个READ
锁,所以多个进程可以将此单个类别添加到多个联系人。
WRITE
锁定:删除类别时,我们必须先从所有联系人中删除类别ID。 在此操作正在运行时,我们必须确保无法将此类别添加到任何联系人(此操作需要READ
锁定)。 之后,我们可以安全地删除类别文档。
这样,总是会有一致的状态。
超时:
这是最难的部分。 我已经尝试过两次实施,但总是发现一些问题,这似乎很难解决。
基本思想:每一个获得的锁都带有一个时间戳,直到这个锁有效。 如果此时间戳过去,我们可以忽略该锁定。 当一个进程完成任务时,它应该删除它的锁。
最大的挑战是拥有多个READ
锁,每个READ
锁都有其自己的超时,但多个READ
锁可以具有相同的超时值。 当释放一个READ
锁时,它只能释放它自己,所有其他的READ
锁必须被保留。
我最后的实现:
{
_id: 1234,
lock: {
read: [
ISODate("2015-06-26T12:00:00Z")
],
write: null
}
}
lock.read
可以包含元素,也可以设置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 }
}
)
(如果多个进程获得READ
锁定, lock.read
数组可能包含多个相同的时间戳,尽管我们只需要移除一个时间戳,这对于$pull
不起作用,但是使用位置操作符$
。使用额外的更新删除所有过期的锁。我尝试了一些东西,但无法将其减少到2甚至1次更新。)
释放WRITE
锁定:
删除写日志。 这里应该没什么可检查的。
update(
{
_id: 1234
},
{
$set: { 'lock.write': null }
}
)
编辑1:简化获取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
锁定查询。
编辑2:不同的数据结构更容易释放READ
锁
{
_id: 1234,
lock: {
read: [
{ timeout: ISODate("2015-06-26T12:00:00Z"), process: ObjectId("...") }
],
write: null
}
}
这是有效的,因为ObjectId
由时间戳,机器ID,进程ID和计数器组成。 这样就不可能创建多个相同的ObjectIds
。 长话短说:
当获取READ
锁时,我们插入一个由超时时间戳和唯一ObjectId
组成的文档。 释放它时,我们使用这个组合从阵列中移除它。 所以唯一有趣的查询是:
Aquire 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
,它应该能够保存timeout
部分。 (例如,Mongodb:从mongo shell中的ObjectId执行日期范围查询)
问题:
这是一个使用MongoDB的有效和无懈可击的实现吗?
如果“是”:我可以以某种方式改善它吗? (至少“Release READ
锁定”部分)
如果“否”:它有什么问题? 我错过了什么?
在此先感谢您的帮助!
链接地址: http://www.djcxy.com/p/41807.html