Вопрос:
Мне нужно реализовать некоторый механизм блокировки с MongoDB, чтобы предотвратить несогласованные данные, но разрешить грязные чтения.
Содержание
- Условия:
- Почему READ и WRITE блокирует/почему не только использует блокировку WRITE:
- Таймаут:
- Моя последняя реализация:
- Запросы:
- РЕДАКТИРОВАТЬ 1: Упрощенный запрос READ и WRITE
- EDIT 2: Разная структура данных для облегчения освобождения READ lock
- Вопросы:
Условия:
-
Приобретение блокировки WRITE возможно только при отсутствии блокировки READ lock и no WRITE.
-
Приобретение блокировки READ возможно только при отсутствии блокировки WRITE.
-
В одном документе может быть много параллельных блокировок READ.
-
Должен существовать какой-то механизм тайм-аута: если (по какой-то причине) какой-то процесс не освобождает свою блокировку, приложение должно восстановить.
Грязные чтения возможны, просто игнорируя все блокировки в запросе.
(Голодание процессов WRITE не входит в эту тему)
Почему READ и WRITE блокирует/почему не только использует блокировку WRITE:
Предположим, что у нас есть 2 коллекции: contacts и categories. Это отношение n-m, где каждый контакт имеет массив идентификаторов категорий.
READ lock: При добавлении категории к контакту мы должны убедиться, что эта категория не удаляется в данный момент (для чего требуется блокировка WRITE, см. ниже), И поскольку в одном документе может быть много блокировок READ, для нескольких процессов можно добавить эту отдельную категорию к нескольким контактам.
WRITE lock: При удалении категории мы должны сначала удалить идентификатор категории из всех контактов. Пока эта операция выполняется, мы должны удостовериться, что нельзя добавлять эту категорию к любому контакту (для этой операции требуется блокировка 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 lock:
Если нет допустимой блокировки записи, вставьте блокировку чтения.
update( { _id: 1234, $or: [ { ‘lock.write’: null }, { ‘lock.write’: { $lt: ISODate(«now») } } ] }, { $set: { ‘lock.write’: null }, $push: { ‘lock.read’: ISODate(«lock expiration») } } )
Приобретать WRITE lock:
Если нет допустимой блокировки чтения и, нет допустимой блокировки записи, установите блокировку записи.
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 } } ) РЕДАКТИРОВАТЬ 1: Упрощенный запрос READ и WRITE
{ $not: { $gte: ISODate(«now») } } будет соответствовать только, если поле не содержит что-либо $gte: ISODate(«now»). Хотя он будет соответствовать null и несуществующим полям, а также пустой массив.
Приобретать READ lock:
update( { _id: 1234, ‘lock.write’: { $not: { $gte: ISODate(«now») } } }, { $set: { ‘lock.write’: null }, $push: { ‘lock.read’: ISODate(«lock expiration») } } )
Приобретать WRITE lock:
update( { _id: 1234, ‘lock.write’: { $not: { $gte: ISODate(«now») } }, ‘lock.read’: { $not: { $gte: ISODate(«now») } } }, { $set: { ‘lock.read’: [], ‘lock.write’: ISODate(«lock expiration») } } )
Но до сих пор нет идеи относительно запроса Release READ lock…
Я думал о каком-то кортеже, имеющем временную метку тайм-аута и счет блокировок. Но тогда проблема связана с запросом блокировки Acquire READ.
EDIT 2: Разная структура данных для облегчения освобождения READ lock{ _id: 1234, lock: { read: [ { timeout: ISODate(«2015-06-26T12:00:00Z»), process: ObjectId(«…») } ], write: null } }
Это работает, потому что ObjectId состоит из метки времени, идентификатора машины, идентификатора процесса и счетчика. Таким образом, невозможно создать несколько равных 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: выполнить запрос диапазона дат из ObjectId в оболочке mongo)
Вопросы:
-
Является ли это допустимой и пуленепробиваемой версией с использованием MongoDB?
-
Если “да”: могу ли я как-то улучшить его? (по крайней мере, часть “Release READ lock” )
-
Если “нет”: что с этим не так? Что я пропустил?
Заранее благодарим за помощь!