⚡CQRS и check-then-actВ CQRS (Command and Query Responsibility Segregation) мы разделяем запросы на чтение (queries) и запросы на модификацию (commands). Зачастую это ведет к тому, что у нас есть две базы данных: куда пишем, и откуда читаем. Данные из write-базы асинхронно реплицируются на read-базу
Далее представим, что мы хотим сделать какую-то выборку, проверить ее на соответствие условиям (check), и далее сделать модифицирующий запрос (then act)
Для примера возьмем модель данных, которая уже была в постах выше - с ticket и assignee
assignee(id);
ticket(id, assignee_id);
Хотим назначить тикет на оператора, если на него сейчас назначено меньше 4х тикетов:
1. read-db: считаем кол-во тикетов
2. write-db: если count < 4, то назначаем новый тикет по assignee_id
Далее представим что приходят два конкурентных запроса
[rq1] read-db: считаем кол-во тикетов, count = 3
[rq2] read-db: считаем кол-во тикетов, count = 3
[rq1] write-db: назначаем новый тикет
[rq2] write-db: назначаем новый тикет
... Данные асинхронно реплицируются, count = 4 ...
... Данные асинхронно реплицируются, count = 5 ...
Итого получаем 5 назначенных тикетов, и мы нарушили инвариант
---
Решить эту проблему можно с помощью оптимистических блокировок. К сущности assignee добавляется поле update_id:
assignee(id, update_id);
ticket(id, assignee_id);
В таком случае назначение будет происходить не по assignee_id, а по паре (assignee_id, update_id)
[rq1] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq2] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq1] write-db: назначаем новый тикет по update_id = 0, получаем update_id = 1
[rq2] write-db: назначаем новый тикет по update_id = 0, ошибка, 0 != 1
... Данные асинхронно реплицируются, count = 4, update_id = 1 ...
Таким образом, когда [rq2] попытается назначить тикет, он увидит в write-db неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант