…используя копии транзакционных документов и клерка

TL;DR

HTTP RESTful API — это неправильный уровень абстракции для доставки сложных транзакций.
Предлагаемое решение:

  • Клиентское приложение помещает все данные в документ и сохраняет их локально
  • Клиентское приложение постоянно синхронизирует его с удаленной репликой.
  • Клиентское приложение прослушивает изменения в этом документе и позволяет пользовательскому интерфейсу отражать эти изменения.
  • Клерк, работающий над серверной частью, обновляет документ и выполняет переходы между состояниями этого документа.
  • Клиентское приложение также обновляет документ и выполняет переходы между состояниями.

Введение

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

Теперь, когда у вас есть новая блестящая архитектура микросервисов, и вы можете развертывать новые функции и исправления несколько раз в день, как вы доставляете сложные транзакции своим клиентам? Как вы доставляете платежи, бронирование поездок или покупку всей корзины с хорошим пользовательским интерфейсом?

HTTP увел нас далеко вперед, но это, вероятно, не лучший транспорт для доставки транзакций, особенно когда они выполняются через ненадежные мобильные сети. На клиентское приложение должна возлагаться большая часть логики обработки ошибок: как приложение реагирует на тайм-ауты запросов? Или проблемы со шлюзом? Может ли он предположить, что транзакция не удалась, не опасаясь дублирования? Может ли транзакция пережить сбои клиента? Может ли клиентская часть приложения решить все эти существующие пограничные случаи, не делая его слишком сложным и подверженным ошибкам?

В этой статье предлагается оригинальный архитектурный стиль, который будет располагаться перед вашим стеком микросервисов, к которому вы сможете подключиться к любой существующей серверной части сервиса. Я покажу вам реализацию этого архитектурного шаблона: приложение для проверки концепции и набор клиентских и серверных библиотек с открытым исходным кодом, созданных поверх PouchDB и Node.js.

Удаленные CRUD API — это неправильный уровень абстракции

HTTP существует уже давно, и мы воспринимаем его как должное. Мы используем его для доставки гипертекста, изображений, звука, видео. Мы используем его для передачи данных формы от клиента к серверу. Мы также используем его в качестве транспортного механизма для вызовов удаленных служб, инициированных веб-приложениями, собственными мобильными приложениями или даже простым интерфейсом командной строки. Мы начали с использования сложных конструкций, таких как XML-сообщения внутри SOAP-конвертов и доставки с использованием HTTP, но мы приобрели некоторый смысл по пути, и теперь мы полностью охватываем HTTP и живем и дышим URL-путем, методами HTTP и кодами состояния ответа HTTP. Структура URL-адреса представляет ресурсы, лежащие в основе службы, а методы HTTP указывают на действие: запрос GET не должен изменять состояние сервера, запрос POST используется для создания ресурса, статус ответа 403 означает, что вам не разрешено для доступа к заданному ресурсу и т. д. Совокупность знаний, связанных с HTTP и ожидаемым поведением, проникла во многие и постоянно растущее число клиентских библиотек, сервисных оболочек и приложений.

Теперь мы полагаемся на API-интерфейсы, передаваемые по протоколу HTTP, для выполнения почти всего цифрового, что требует удаленной связи: от простых вещей, таких как чтение сообщения в блоге до отправки сообщения, до более сложных операций, таких как оплата с помощью онлайн-банкинга или бронирование рейса вашей любимой авиакомпании. перевозчик. Но действительно ли HTTP подходит для этих более сложных транзакций? Будучи протоколом «запрос-ответ», HTTP накладывает серьезные ограничения на то, что можно решить на уровне протокола.
Возьмем следующий пример: Джон бронирует трехэтапную поездку на поезде, которая объединяет двух поставщиков поездов с помощью веб-интерфейса. Как только он вводит все необходимые личные и платежные данные и нажимает кнопку «Подтвердить и оплатить», что происходит за кулисами?

Для начала веб-приложение, которое использует Джон, выполняет POST-запрос к URL-адресу http://api.awesometrainbookingplatformservice.com/trip, при этом тело запроса содержит все данные, необходимые для бронирования поездки: все идентификаторы. для ног, его личные данные и необходимые платежные данные. В конечном итоге запрос достигнет HTTP-сервера, где он начнет обрабатываться.

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

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

Запрос возвращает ошибку 5xx

В данном случае произошла ошибка на стороне сервера, из-за которой многое могло пойти не так:

  • Балансировщик нагрузки мог потерять связь с сервером приложений.
  • Процесс сервера приложений мог умереть
  • Может быть ошибка на сервере приложений
  • Непредвиденное поведение одной из внутренних служб

Каково текущее состояние сделки? Было ли оно завершено, было ли оно прервано, находится ли оно в пути? Может в ответе есть какая-то полезная информация о текущем состоянии транзакции, а может и нет...

Время запроса истекает

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

Ошибка сети

Здесь у нас может быть два подсценария: первый — когда клиент так и не смог подключиться к серверу, и в этом случае можно безопасно повторить попытку.

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

Сеанс браузера исчезает

Когда Джон нажимает кнопку «Подтвердить и оплатить», он должен быть очень осторожен в отношении того, что он делает с браузером. При переключении вкладок он может непреднамеренно закрыть вкладку с выполняемой транзакцией или активировать другое веб-приложение, что приведет к сбою всего браузера.

Что происходит сейчас? Теперь Джон не знает результата транзакции, и он смотрит на свой почтовый ящик, задаваясь вопросом…

Общий результат

Я думаю, что большинство из нас проходили через это: нажимали кнопку «Подтвердить и оплатить» и надеялись, что все пройдет хорошо. Транзакции в большинстве случаев сложны по своей сути, но если мы не переместим эту сложность из клиентского кода, мы вызовем недоверие, страх и разочарование в использовании той части приложения, которая генерирует ценность.

Существующие обходные пути

Многие клиентские приложения знают об этих проблемах и используют некоторые технические решения/обходные пути для решения этой проблемы. Основная проблема здесь заключается в том, что эта проблема не является одной единственной проблемой: учитывая низкий уровень абстракции (предоставляемый CRUDy RESTful HTTP API) для клиента, эта проблема представляет собой набор проблем, и для каждой из них мы можем попытаться придумать с раствором.

В случае ошибки повторить попытку или нет? Какой должна быть реакция клиента, когда он получает ошибку? Должен ли он попытаться опубликовать транзакцию еще раз? Если транзакция действительно прошла, есть риск ее дублирования.

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

Длительные транзакции

В некоторых типах транзакций время, необходимое для завершения, слишком велико, чтобы HTTP-запрос оставался зависшим, что приводит к тайм-ауту.

Если это так, способ справиться с длительными транзакциями заключается в том, что первоначальный запрос только начинает транзакцию. Затем клиент будет уведомлен, как только транзакция достигнет конечного состояния. Это может включать несколько методов:

  • Опять же, создание идентификатора транзакции на стороне клиента, который можно использовать для опроса (это также позволяет избежать дублирования, если пользователю необходимо повторно отправить первый запрос);
  • Либо опрос сервера о состоянии транзакции, либо
  • получить уведомление от сервера (используя длинный опрос XHR, отправленные сервером события или веб-сокеты) о том, что транзакция изменила состояние.

Сессия клиента исчезает

Если вкладка браузера закрывается, процесс браузера умирает или даже если батарея устройства садится или процессор взрывается, как клиент может восстановиться?

Если транзакции связаны с учетной записью пользователя, потеря сеанса во время транзакции (например, из-за закрытия вкладки браузера или выключения компьютера) является проблемой, которую можно решить с помощью:

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

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

Есть ли способ лучше?

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

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

Привет офлайн-первый

Сначала автономное приложение — это приложение, созданное без предположения, что устройство, на котором оно запущено, подключено к Интернету.

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

Этот режим работы очень реалистичен: мобильные сети ненадежны, прерывисты и часто недоступны. Большинство интернет-пользователей используют мобильные сети, и эта доля будет продолжать расти.

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

Не все операции?

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

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

Использование репликации

Автономное решение для транзакций требует двух важных частей: одного хранилища данных на стороне клиента и соответствующего хранилища на стороне сервера. Они оба синхронизируются с помощью протокола синхронизации: изменение в клиенте асинхронно распространяется на сервер, и верно обратное: изменение данных клиента, выполненное на сервере, реплицируется в клиенте.

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

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

PouchDB — близкий родственник CouchDB, но он написан на JavaScript и может работать в клиентском браузере JS или на сервере с использованием Node.js. PouchDB совместим по протоколу с CouchDB, что означает, что вы можете реплицировать базу данных CouchDB в PouchDB и наоборот.

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

А конфликты?

И CouchDB, и PouchDB допускают одностороннюю репликацию, но в этой статье мы собираемся использовать двустороннюю репликацию: любое изменение клиентской или серверной базы данных будет распространяться на другую. Это означает, что обе базы данных ведут себя как основные базы данных, а это означает, что могут возникать конфликты. Как с ними обращаться?

CouchDB и PouchDB могут хранить все изменения для данного документа в древовидной структуре. Каждая версия документа может иметь одного или нескольких потомков. Когда в процессе репликации возникает конфликт, одна из версий выбирается как победившая, но база данных хранит всю информацию о конфликте. Любая из клиентских или серверных баз данных может затем получить неразрешенные конфликты и решить их, используя любую стратегию слияния, которую они сочтут подходящей.

Как насчет аутентификации?

Способ, которым мы собираемся использовать Couch или PouchDB, заключается в том, чтобы иметь одну базу данных для каждого клиента (на языке CouchDB одна «база данных» соответствует тому, что «таблица» является реляционной базой данных). Мы должны убедиться, что к каждой клиентской базе данных может получить доступ только клиент-владелец и работающий с ней клерк.

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

  • Если вы используете CouchDB в качестве сервера, вы можете использовать системных пользователей CouchDB. Здесь один клиент — это один пользователь системы CouchDB, получивший разрешение на запись только в эту базу данных.
  • Используйте специальный механизм аутентификации (например, при открытии базы данных через смарт-прокси)

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

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

Постоянный документ транзакции

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

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

Затем клиент сохраняет этот документ транзакции в локальной базе данных.

Конечный автомат

Этот документ имеет одно особое свойство: текущее состояние.

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

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

Упрощенный конечный автомат для службы такси

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

Переходы состояний

Каждое состояние имеет набор возможных следующих состояний. Для упрощения предположим, что в любом заданном состоянии либо клиент, либо сервер могут выполнить изменение состояния, но не оба одновременно.

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

Пользовательский интерфейс клиента

Поскольку документ реплицируется в обоих направлениях, любое изменение состояния отправляется в обратном направлении. Если, например, сервер изменит статус документа запроса такси с «запрошено» на «назначен водитель», это изменение будет передано клиенту. Затем клиент должен прослушивать изменения документа (PouchDB позволяет вам сделать это очень легко) и отражать их в пользовательском интерфейсе.

Если вы используете такие технологии, как Angular, React или Ember, пользовательский интерфейс может реагировать на изменения документа и легко отражать его состояние. Например, если документ находится в состоянии «водитель в пути», вы можете указать предполагаемое время прибытия водителя. Или, когда состояние документа «в пути», пользовательский интерфейс может отображать карту текущего местоположения и т. д.

Статус синхронизации клиента

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

Клерк, работающий от вашего имени

Важной частью этой системы является клерк. Клерк — это сущность, которая находится на стороне сервера. Он реагирует на изменения документа (это легко сделать путем прослушивания потока изменений), делает то, что ему нужно (например, вызывает серверные службы), а затем изменяет состояние документа транзакции. Любое изменение документа транзакции на стороне сервера сохраняется и, следовательно, реплицируется на клиенте.

В этом есть несколько преимуществ:

Задержка

Что касается задержки в сети, клерк находится в лучшем положении для взаимодействия с внутренними системами.

Меньшая сложность клиента

Все восстановление после ошибок и странных пограничных случаев теперь будет выполняться клерком в контролируемой среде, а не клиентом.

Кроме того, это гораздо более простая модель программирования для фронтенд-разработчика.

Меньше ответственности на клиенте

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

Безопасность

В типичном общедоступном сервисе клиенту не разрешается напрямую изменять центральные записи, но большинство API позволяют клиентам делать именно это: напрямую изменять записи, к которым у них есть доступ (минус некоторые проверки, которые разработчики не забывают проводить).

Рассматривая документ транзакции как заявление о намерениях клиента, клерк обязан внести много информированных и контролируемых изменений в любую центральную запись.

Преимущества для клиента

  • Сетевая отказоустойчивость: при использовании этой схемы клиент может изящно восстанавливаться после сетевых сбоев без какой-либо дополнительной логики.
  • Отзывчивость. Кроме того, поскольку все изменения, происходящие в результате взаимодействия с пользователем, являются локальными, это может улучшить восприятие отклика приложения.
  • Удобство и простота использования: пользователи могут запускать и выполнять транзакции независимо от того, доступен ли серверный сервис или нет.
  • Улучшение качества: можно утверждать, что с более простой моделью программирования будет легче отправлять меньше ошибок.
  • Повышение удовлетворенности: поскольку доступность транзакций и общий опыт улучшаются, удовлетворенность клиентов обслуживанием также должна повышаться.

Преимущества для программиста

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

Даже этой последней проблемы можно избежать, используя современную веб-инфраструктуру, такую ​​​​как React, и плагин, который позаботится об этом — это подробно объясняется ниже.

Среда программирования

Теперь есть два места, куда должна перейти бизнес-логика: клерк и клиентское приложение.

Клерк

Внутри клерка есть два места, куда должна идти бизнес-логика: обработка переходов состояний и выполнение асинхронных обновлений.

Обработка переходов состояний

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

// handle arrived destination module.exports = handleArrivedDestination; function handleArrivedDestination(transaction, next) { backendServices.arrivedDestination((err, transaction) => { if (err) next(err); else { transaction.somefield = result.someotherfield; next(null, 'finished'); // transition to state 'finished' } }); }

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

// example of an async updater exports.start = function(doc) { this._listener = backendMessageQueue.listen(doc._id, onMessage); function onMessage(message) { // get the latest doc version and update it doc.get((err, transaction) => { transaction.somefield = message.someotherfield; doc.put(transaction); } } }; exports.stop = function() { backendMessageQueue.stopListening(this._listener); };

Клиентское приложение

Это, как и было обещано, самое простое. Предполагая, что вы используете библиотеку, чтобы а) изменения в документе транзакции, происходящие в локальной клиентской базе данных, синхронизировались с представлением в памяти этого документа и б) изменения, происходящие в представлении в памяти этого документа, записывались в локальную базу данных, все клиент должен сделать, чтобы пользовательский интерфейс отражал изменения представления памяти этого документа. Эту последнюю часть можно легко выполнить с помощью современного веб-фреймворка, такого как Angular или React.

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

Некоторые библиотеки с открытым исходным кодом

Помимо PouchDB, React и Redux, вот несколько библиотек с открытым исходным кодом, которые я использовал для создания приложения для проверки концепции.

pouch-redux-middleware

Двунаправленная репликация между состоянием Redux и локальной базой данных PouchDB.

мешочек-веб-сокет-синхронизация

Синхронизирует локальную базу данных с удаленной через соединение через веб-сокет, обеспечивая настраиваемые механизмы аутентификации.

курьер

Серверный клерк Node.js, где вы можете программировать

  • Реакции на любой переход по документу сделки;
  • Асинхронные обновления данного документа транзакции, зависящие от внешних событий.

Демо

Я создал очень грубое и неотшлифованное приложение для проверки концепции. Вот краткая демонстрация этого:

Вот некоторые проблемы, которые этот тип архитектуры не решает:

Клерк: действие idem-потенция

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

По сути, это та же проблема, с которой клиент сталкивался раньше (когда использовал API вместо реплицированного документа транзакции), но с гораздо меньшей вероятностью (клиенты и клиентские сети обычно имеют более высокую задержку и гораздо более высокую вероятность отказ). На самом деле это не проблема, чтобы быть на этом уровне, но мы можем увидеть два общих решения для этого:

а) Распределенная транзакция

Реализация протокола, такого как двухфазная фиксация или общая блокировка, откат транзакции в случае сбоя одной из сторон. Для этого все участники одной транзакции должны реализовать такой протокол.

б) Идемпотентные операции

Поскольку трудно иметь распределенные транзакции, все внутренние операции должны быть идемпотентными: их можно безопасно повторить в случае сбоя. Есть несколько способов сделать это, все зависит от сервиса. Например, если вы собираетесь выполнить денежную транзакцию, поддержите уникальный клиентский ключ в серверной части. Этот клиентский ключ должен быть создан в документе транзакции клиентом или клерком. Если предпринимается попытка дублирования транзакции с тем же идентификатором клиента, это должно быть обнаружено серверной частью. Затем клерк может корректно восстановиться, объявить транзакцию выполненной и перейти к следующему состоянию.

Клерк: планирование и параллелизм

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

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

Все это имеет свои плюсы и минусы, но опять же, это не та проблема, которую нужно решать на этом уровне.

Безопасность: клиент может изменить документ по своему желанию

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

Если информация была сгенерирована серверной частью и должна оставаться секретом для клиента, продавец может:

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

Оффлайн-первый лагерь

Я пойду в первый Offline-First Camp, где буду говорить и обсуждать эту и другие связанные темы. Если вам интересны офлайн-технологии, возвращайтесь в наш блог для дальнейших обновлений!

HTTP RESTful API — это неправильный уровень абстракции для доставки сложных транзакций.
Предлагаемое решение:

  • Клиентское приложение помещает все данные в документ и сохраняет их локально
  • Клиентское приложение постоянно синхронизирует его с удаленной репликой.
  • Клиентское приложение прослушивает изменения в этом документе и позволяет пользовательскому интерфейсу отражать эти изменения.
  • Клерк, работающий над серверной частью, обновляет документ и выполняет переходы между состояниями этого документа.
  • Клиентское приложение также обновляет документ и выполняет переходы между состояниями.

Написано Педро Тейшейрой — опубликовано для YLD.

Вам также может понравиться: