MongoDB:实现读/写锁(互斥锁)

时间:2015-06-26 04:10:09

标签: multithreading mongodb locking mutex mongodb-query

我需要使用MongoDB实现一些锁定机制,以防止数据不一致,但允许脏读。

条件:

  • 只有在WRITE锁定没有READ锁定的情况下才能获得WRITE锁定。

  • 只有在没有READ锁定的情况下才能获得WRITE锁定。

  • 单个文档上可能存在多个并行READ锁。

  • 必须有某种超时机制:如果(无论出于何种原因)某个进程未释放其锁定,则应用程序必须能够恢复。

只需忽略查询中的所有锁即可进行脏读。

WRITE进程的饥饿不是本主题的一部分)

为什么READWRITE锁定/为什么不仅仅使用WRITE锁:

我们假设有2个集合:contactscategories。它是一种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")
    1. 获取新锁时使用。
    2. 释放读锁时也会使用它。

获取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

编辑1:简化获取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锁定查询。

编辑2:不同的数据结构,以便更容易发布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锁定"部分)

  • 如果"不":它有什么问题?我错过了什么?

提前感谢您的帮助!

0 个答案:

没有答案