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 неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант