Re[32]: Помогите правильно спроектировать микросервисное при
От: Sinclair Россия https://github.com/evilguest/
Дата: 16.02.26 16:51
Оценка: +1 :)
Здравствуйте, gandjustas, Вы писали:

G>У нас мобильное приложение или браузер, обращается через REST API к серверу, а сервер в свою очередь обращается к БД. Типичная трехзвенная архитектура.


G>Все проще, пользователь просто закрыл страницу когда не дождался ответа или интернет ему выключили. В любом случае сервер увидел потерю соединения и запустил отмену через CancellationToken, который из контроллера прокидывается во всю БЛ.

Нет конечно, сервер ничего не "увидел", и никакую "отмену" он не запустил.
Сервер может заметить потерю клиента только в тот момент, когда он начинает отправлять клиенту респонс — после того, как все транзакции уже уехали в durable log.
Наоборот делать ни в коем случае нельзя — иначе клиент получит ответ, который может исчезнуть после перезагрузки сервиса.
Более того — сервер может так и не увидеть отвала клиента даже после отправки ответа.
Например, между клиентом и сервером стоит прокси, который в любом случае вычитает ответ до конца и вежливо дождётся закрытия соединения со стороны сервера.

G>И это событие по счастливой случайности происходит ровно в момент между тем как А ответил подтверждением, а на сервер Б еще не отправлена команда на изменение статуса заказа.

Вы по-прежнему пишете какие-то непонятные мне вещи. Никакой "команды на изменение статуса заказа" на сервер Б не отправляется.
В этой архитектуре
  1. Пользователь на странице заказа нажимает кнопку "зарезервировать"
  2. Клиент отправляет в "сервис Б" команду
    PATCH /orders/42 
    
    {"status": "reserved"}

  3. Сервис Б в локальной базе делает
    update orders set status = "reserving" where id = 42

    и немедленно возвращает клиенту 202 Accepted
  4. Клиент, в зависимости от развитости чувства прекрасного у тех лида, либо делает регулярный polling этого ордера через GET /orders/42, либо идёт читать GET /orders/42/events.
  5. Тем временем город засыпает просыпается workflow-процесс, который делает select * from orders where status = "reserving" (с поправкой на engine-specific приседания для того, чтобы сделать аккуратное разгребание этой очереди без лишних блокировок и рейсов)
  6. Обнаружив в базе наш заказ с номером 42, он вычитывает его позиции, и делает PUT на адрес сервиса А:
    PUT /reservations/42/
    
    {
      "items": [ 
       {"product":17, "quantity":10},
       {"product":5, "quantity":1}
      ]
    }

    обратите внимание, что этот процесс вообще ничего не знает о клиенте и пользователе. Его работа — двигать workflow по предписанной траектории, даже если эта траектория пересекает произвольное количество вынужденных и добровольных рестартов любого из участников.
  7. Сервис А в ответ на этот запрос лезет в свою базу и ищет там reservation where id = 42.
    • Если находит, то проверяет, совпадают ли указанные в запросе продукты и количества с reservationItems where orderId = 42. Если совпали — то возвращает 200 Ok с ETag, вычисленным как хеш от всех продуктов и количеств. Если не совпали — возвращает 409 Conflict. (Тут на самом деле чуть сложнее, но в данном конкретном сценарии эту сложность можно пока проигнорировать)
    • Если не находит, то делает
      begin transaction;
      update stock 
        set reserved = reserved + 10,
            available = available - 10 
        where productId = 17;
      update stock 
        set reserved = reserved + 1,
            available = available - 1 
        where productId = 5;
       
      insert into reservations(orderId) values(42);
      insert into reservationItems(orderId, productId, quantity) values(42, 17, 10);
      insert into reservationItems(orderId, productId, quantity) values(42, 5, 1);
      commit transaction

      Эта радость может быть прервана constraint availability check(available >= 0) или ещё чем-нибудь — тогда сервис отдаёт обратно 4хх c понятным клиенту объяснением.
      Конкретный SQL тут не так важен — важна его атомарность. Если мы всё же довели транзакцию до конца и получили в сервисе А положительный ответ от СУБД, то мы в ответ на это отдаём сервису Б
      201 Object Created
      ETag: 0x213123123412312
      
      {
        "items": [ 
         {"product":17, "quantity":10},
         {"product":5, "quantity":1}
        ]
      }
  8. Сервис Б, дождавшись этого ответа, делает в своей базе
    update orders set status = "reserved", reservationETag = 0x213123123412312 where id = 42

  9. Если вдруг так получилось, что сервис Б так и не смог сделать эту запись (питание моргнуло до коммита в базу; сеть моргнула до того, как респонс доехал до сервиса Б), то тот же самый процесс перейдёт к обработке следующего заказа в этом статусе; а потом очередь снова дойдёт до заказа 42 и сервис Б пойдёт по кругу, пока не получит от сервиса А внятного ответа и не сможет запомнить этот ответ.
    Клиент в этом всём никакого участия не принимает: надёжность его связи с сервером на порядок хуже надёжности сети между сервисами А и Б.
  10. В зависимости от вкусов разработчика, упомянутых выше, та же транзакция, которая пишет в orders, может заодно писать и в orderEvents. Тогда клиент, если он ещё не ушёл, узнает об этом по появлению новых данных в GET /orders/42/events.
  11. Иначе, на очередном витке GET /orders/42, он увидит "status":"reserved" и покажет пользователю окошко для реквизитов оплаты.
G>Даже если мы эту отправку на серверы А и Б делаем на сервере, а не клиенте, то проблема никуда не девается. Процесс на сервере тоже также может упасть между.
Нет никаких "между".

G>В описанном выше сценарии пользователь перезагрузивший страницу\приложение увидит что заказ все еще в статусе "резервируется".

Можно и так написать, но зачем делать плохое решение, когда есть хорошее?

G>Что именно будет сохранено в WAL зависит в итоге от реализации хранилища, к которому мы будем этот самый WAL применять.

Я не знаю других вариантов устройства WAL, кроме как "данные до + данные после", т.к. опирается он на идемпотентность записи. Если у нас есть "хранилише", в которое можно делать идемпотентную запись И оно разрешает произвольное количество перезаписей туда/обратно, то можно сделать поверх него WAL. Иначе — нет.

G>От кого он этот ответ должен получить? Какой процесс должен сработать?

От сервиса А, к которому он обращается. Выше — пошаговый алгоритм.

G>Мне кажется дальнейшее пока рано обсуждать, потому что мы пока не достигли единого понимания сценария фейла для "транзакции" из двух таблиц. Когда поймем эти сценарии и поймем как их можно компенсировать за счет кода можно обсуждать все остальное.

Ок, давайте достигнем. Есть какие-то моменты, непонятные в схеме выше?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.