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

Ben*_*n M 5 multithreading mutex locking mongodb mongodb-query

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

条件:

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

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

  • READ单个文档上可以有许多并行锁.

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

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

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

为什么READWRITE锁/为什么不只使用WRITE锁:

让我们假设,我们有2个集合:contactscategories.这是一种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
  }
}
Run Code Online (Sandbox Code Playgroud)

要么lock.read可以包含元素,要么 lock.write可以设置.必须永远不可能同时设置!

查询:

对此的查询是可以的,有些可能更容易一些(尤其是"释放读锁").但向你展示它们的主要原因是我仍然不确定我是否没有错过任何东西.

前言:

  • ISODate("now")是当前时间.它用于忽略所有过期的锁.它还用于删除所有过期的读锁.
  • ISODate("lock expiration")用于指示此锁定何时到期并可以忽略/删除.(例如now + 5 seconds)
    1. 获取新锁时使用此选项.
    2. 它也在释放读锁时使用.

获取READ锁定:

如果没有有效的写锁定,则插入读锁定.

update(
  {
    _id: 1234,
    $or: [
      { 'lock.write': null },
      { 'lock.write': { $lt: ISODate("now") } }
    ]
  },
  {
    $set: { 'lock.write': null },
    $push: { 'lock.read': ISODate("lock expiration") }
  }
)
Run Code Online (Sandbox Code Playgroud)

获取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")
    }
  }
)
Run Code Online (Sandbox Code Playgroud)

释放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 }
  }
)
Run Code Online (Sandbox Code Playgroud)

(lock.read如果多个进程获得了READ锁定,那么数组可能包含多个相同的时间戳.虽然我们只需要删除一个时间戳,但这不起作用$pull,但是使用位置运算符$.我还删除了所有过期的锁更新.我尝试了一些东西,但无法将其减少到2甚至1次更新.)

释放WRITE锁定:

删除写日志.这里应该没什么好检查的.

update(
  {
    _id: 1234
  },
  {
    $set: { 'lock.write': null }
  }
)
Run Code Online (Sandbox Code Playgroud)

编辑1:简化获取READWRITE查询

{ $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") }
  }
)
Run Code Online (Sandbox Code Playgroud)

获取WRITE锁定:

update(
  {
    _id: 1234,
    'lock.write': { $not: { $gte: ISODate("now") } },
    'lock.read': { $not: { $gte: ISODate("now") } }
  },
  {
    $set: {
      'lock.read': [],
      'lock.write': ISODate("lock expiration")
    }
  }
)
Run Code Online (Sandbox Code Playgroud)

但仍然不知道"释放READ锁定"查询...

我想到了某种具有超时时间戳和锁数的元组.但随后问题来自Acquire READ锁定查询.


编辑2:不同的数据结构,更容易释放READ锁定

{
  _id: 1234,
  lock: {
    read: [
      { timeout: ISODate("2015-06-26T12:00:00Z"), process: ObjectId("...") }
    ],
    write: null
  }
}
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为它ObjectId包含时间戳,机器ID,进程ID和计数器.这样就不可能创建多个相等的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")
    }
  }
)
Run Code Online (Sandbox Code Playgroud)

释放READ锁定:

update(
  {
    _id: 1234,
  },
  {
    $pull: {
      'lock.read': {
        $or: [
          { 'timeout': ISODate("lock expiration"), process: ObjectId("...") },
          { 'timeout': { $lt: ISODate("now") } }
        ]
      }
    }
  }
)
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我们现在只需要一个查询来删除我们对清除所有超时锁定的锁定.

唯一的进程标识符非常重要,因为没有它,$pull如果获取具有相同超时值的锁,则操作可以删除另一个进程的锁.

下一步将是,摆脱该process领域,只使用一个ObjectId应该能够保持该timeout部分.(例如Mongodb:从mongo shell中的ObjectId执行日期范围查询)


问题:

  • 这是使用MongoDB的有效且无懈可击的实现吗?

  • 如果"是":我可以以某种方式改善它吗?(至少"释放READ锁定"部分)

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

在此先感谢您的帮助!