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

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

条件:

  • 获取WRITE锁才有可能,如果没有READWRITE锁。

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

  • 单个文档上可以有许多并行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
      }
    }
    

    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:简化获取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") }
      }
    )
    

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

    上一篇: MongoDB: Implement a read / write lock (mutex)

    下一篇: Reentrant lock and deadlock with Java