TransWikia.com

Атомарность нескольких операций

Stack Overflow на русском Asked on November 10, 2021

Есть функционал который создает некоторый объект Some в несколько этапов.
По завершению каждого этапа я пишу в базу данные. Всего этапов 5. Я могу получить ошибку на любом из шагов, при этом предыдущие шаги выполнятся успешно.
Но эти сохраненные данные уже будут не актуальны, и по сути являются информационным мусором. Для корректности работы хорошо было бы откатывать промежуточные сохраненные данные.
Как с точки зрения архитектуры реализовать атомарность операций?

One Answer

Принципиально подхода два

  1. транзакции
  2. согласованность в конечном счёте (eventual consistency)

Транзакции

Это самый простой вариант и если нет противопоказаний (см. ниже), то использовать проще всего.

Достаточно сделать так, чтобы функция (назовем ее doSomething), которая реализует функционал, выполнялась в транзакции БД.

scala не знаю, поэтому примеры на java чтобы продемострировать:

@Transactional
void doSomething() {
   doStep1();
   doStep2();
   doStep3();
   doStep4();
   doStep5();
}

Если, например, после шага 3 произойдет ошибка, то транзакция просто откатится, и все записи в БД, которые делали шаги 1-3 тоже откатятся. Все будет выглядеть так, как будто ничего и не начиналось.

Если же все шаги завершаться успешно, то после 5-го шага, все изменения сделанные шагами в БД будет зафиксированы (произойдет commit).

Т.е. либо все что делали шаги сохранится в БД, либо ничего - то что нам нужно.

Важный момент, что решения с try-catch не годятся. Так как catch не всегда выполняется, например, если сервер на котором эта функция выполнялась вообще рухнул, т.е. сгорел, пропало питание и т.д., по-этому расчитывать на очистку и приведения состояния в порядок, если промежуточные шаги комитят в БД, в catch нельзя.

С транзакциями в БД даже в этом случае все будет работать правильно. БД откатит транзакцию и все.

К сожалению, это не всегда годится. Например:

  1. у вас больше одной БД
  2. хранилище, которое вы используете, не поддерживает транзакции
  3. используются внешние сервисы (например внешний сервис покупки билетов). Например, на шаге 3 нужно сделать вызов внешнего сервиса через интернет. Это потенциально долгая операция, сервис может быть недоступен, а долго держать транзакцию открытой - плохо по разным причинам.
  4. производительности БД не хватает, т.е. она является узким горлышком (за транзакционность нужно платить производительностью).

В такой ситуации используется eventual consistency.

Eventual consistency

В этом подходе каждый шаг выполняется отдельно и сохраняет свои результаты. Если где-то произошла ошибка, то в зависимости от ошибки, процесс либо повторяется, либо делается откат. Но все это вручную.

Рассмотрим пример:

void buyOnline(Cart cart, User user) {
   // эта операция помечает товары как зарезервированные
   reserveItems(cart);
   // эта операция грубо говоря блокирует необходимую сумму в банке
   PaymentTransaction payment = blockAmount(cart.getTotalPrice(), user.getPaymentInfo());
   if (payment.isSuccess()) {
      // эта операция помечает товары как купленные и создает задачу отделу доставки
      createShipment(cart.getItems(), user.getAddress());
      // эта операция собственно подтверждает банку, что заблокированные средства нужно списать у плательщика
      finalizePayment(payment);
   } else {
      // эта операция отменяет резервацию
      cancelReservationForItems(cart);
   }
}

Плюс к этой процедуре нужны еще несколько, которые и занимаются откатами и исправлением проблем, я их упомяну по ходу.

Рассмотрим теперь, как будут различные сценарии ошибок обрабатываться.

Если, например, удачно зарезервировали, но потом сервер вообще накрылся, то эту ситуацию будет обрабатывать фоновый процесс, который регулярно просматривает зарезервированные товары и отменяет их резервации, если она была сделана достаточно давно (т.е. мы закладываем, что от резервации до момента createShipment может пройти скажем максимум 1 час).

Если удалось заблокировать деньги, и потом все упало, то тут можно процесс createShipment + finalizePayment перезапустить вручную, например это может сделать служба поддержки специально кнопкой после звонка пользователя. Если же пользователь не позвонит вообще, то сам банк через какое-то время откатит блокировку средств, если мы не сделали finalizePayment. А с нашей стороны тоже нужна задача, которая периодически будет искать старые заказы в статусе payed и удалять их, ну или можно их автоматически перезапускать - это уже часть бизнес-логики (тут бизнес-логика начинает понимать, что некоторые операции могут не пройти и это бизнес-решение, как их обрабатывать).

Если же у нас все прошло, но мы не смогли сделать finalizePayment, то эту операцию повторит автоматическая задача. Понимаю, что возникает вопрос, а что если нам так и не удастся отослать подтверждение после многих попыток? Товар то мы уже возможно отправили. В этом случае придется вручную разбираться с системой платежей. Главное это сохранить у себя подтверждение от системы что оплата прошла (это обычно какой-то идентификатор транзакции или может быть информация о платеже подписанная цифровой подписью системы платежей). Это подтверждение является доказательством и в случае таких уж глобальных проблем с недоступностью, операция finalizePayment делается, грубо говоря, по телефону через поддержку платежной системы.

Бывают случаи когда невозможно принципиально из-за ограниченого API сделать так, чтобы две операции (оплата + отгрузка) гарантированно завершились даже с повторными попытками. Или действие невозможно отменить, например, нельзя отозвать email, в котором мы написали, что товар куплен. Или, например, может случится, что мы отгрузим товар, но оплату все же сделать не можем.

В таких случаях мы делаем компенсационные действия. Для отгруженного товара без оплаты - пишем пользователю чтобы заплатил или вернул товар (или подаем на него в суд, ведь у него же нет никакого подтверждение об оплате). Для email посылаем новый email, где пишем, извините, но случилась проблема и товар не будет отгружен.

Answered by Roman Konoval on November 10, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP