Существует два вида блокировок: пессимистическая (та, которую вы пытаетесь избежать) и оптимистическая блокировка.
При оптимистической блокировке вы не удерживаете какую-либо блокировку, но пытаетесь сохранить документ; если документ уже был изменен в то же время (то есть он был изменен с тех пор, как мы его загрузили), тогда вы повторите попытку всего процесса (загрузка + изменение + сохранение).
Один из способов сделать это - иметь столбец version
, который увеличивается каждый раз, когда вы изменяете сущность. Когда вы пытаетесь сохранить, вы ожидаете, что сущность с version = version + 1
не существует. Если он уже существует, это означает, что произошло параллельное обновление, и вы повторите попытку (загрузка + изменение + сохранение).
В псевдокоде алгоритм выглядит так:
function updateEntity(ID, load, mutate, create)
do
{
entity, version = load(ID) or create entity
entity = mutate entity
updateRow(that matches the ID and version) and increment version
}
while (row has not changed and was not inserted)
Я дам вам также пример кода на PHP (надеюсь, это легко понять) для MongoDB:
class OptimisticMongoDocumentUpdater
{
public function addOrUpdate(Collection $collection, $id, callable $hidrator, callable $factory = null, callable $updater, callable $serializer)
{
/**
* We try to add/update the entity in a concurrent safe manner
* using optimistic locking: we always try to update the existing version;
* if another concurrent write has finished before us in the mean time
* then retry the *whole* updating process
*/
do {
$document = $collection->findOne([
'_id' => new ObjectID($id),
]);
if ($document) {
$version = $document['version'];
$entity = \call_user_func($hidrator, $document);
} else {
if (!$factory) {
return;//do not create if factory does not exist
}
$entity = $factory();
$version = 0;
}
$entity = $updater($entity);
$serialized = $serializer($entity);
unset($serialized['version']);
try {
$result = $collection->updateOne(
[
'_id' => new ObjectID($id),
'version' => $version,
],
[
'$set' => $serialized,
'$inc' => ['version' => 1],
],
[
'upsert' => true,
]
);
} catch (\MongoDB\Driver\Exception\WriteException $writeException) {
$result = $writeException->getWriteResult();
}
} while (0 == $result->getMatchedCount() && 0 == $result->getUpsertedCount());//no side effect? then concurrent update -> retry
}
}