Познакомьтесь с двухэтапным сообщением

В этой статье предлагается альтернативный шаблон для папки Исходящие: двухэтапное сообщение. Он основан не на очереди сообщений, а на github.com/dtm-labs/dtm, высокодоступной среде распределенных транзакций.

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

Основная проблема в том, что перевод должен обновлять две системы одновременно — приращение баланса А и декремент баланса Б. Это называется известной «двойной записью». Сбой процесса между двумя обновлениями оставляет всю систему в несогласованном состоянии.

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

2-этапное сообщение

Во-первых, давайте взглянем на то, как выполнить описанную выше задачу переноса с помощью нового шаблона. Следующие коды находятся на Go, другие языки, такие как C#, PHP, можно найти здесь: dtm SDK

msg := dtmcli.NewMsg(DtmServer, gid).
	Add(busi.Busi+"/TransIn", &TransReq{Amount: 30})
err := msg.DoAndSubmitDB(busi.Busi+"/QueryPrepared", db, func(tx *sql.Tx) error {
	return AdjustBalance(tx, busi.TransOutUID, -req.Amount)
})

В приведенных выше кодах:

  • Сначала создайте глобальную транзакцию DTM msg, передав адрес сервера dtm и глобальный идентификатор транзакции.
  • Добавьте к msg филиалу бизнеса, который является переводной операцией TransIn, вместе с данными, которые необходимо передать в этот сервис, сумму 30$
  • Затем вызовите DoAndSubmitDB msg. Эта функция обеспечит атомарное выполнение как бизнеса, так и отправки msg, либо обоих успешно, либо обоих не удалось. У этой функции есть три параметра:
  1. URL-адрес возврата будет объяснен позже.
  2. DB, это объект базы данных для бизнеса
  3. Бизнес-функция здесь, в нашем примере, заключается в дебетовании 30 $ для баланса А.

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

app.GET(BusiAPI+"/QueryPrepared", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
		return MustBarrierFromGin(c).QueryPrepared(db)
	}))

После написания этих двух фрагментов кода создается двухфазное сообщение, которое намного проще в использовании, чем Исходящие.

Запустить его

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

Запустить DTM

git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go

Пример запуска

git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_msg_doAndCommit

Успешный процесс

Как DoAndSubmitDB обеспечивает атомарность успешного выполнения бизнеса и отправки сообщений? См. следующую временную диаграмму.

Как правило, 5 шагов на временной диаграмме завершаются нормально, и глобальная транзакция завершается. Здесь необходимо кое-что объяснить: обязательство msg выполняется в два этапа: сначала Подготовка, затем Отправка.

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

Сбой после фиксации

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

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

Давайте посмотрим на временную диаграмму в этом случае.

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

Эта служба обратной связи входит в таблицу сообщений и запрашивает, была ли зафиксирована локальная транзакция для бизнеса.

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

Сбой перед обязательством

Давайте посмотрим на временную диаграмму отката локальной транзакции.

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

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

2-этапное сообщение VS OutBox

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

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

По сравнению с «Исходящими» двухфазное сообщение имеет следующие преимущества.

  • Нет необходимости изучать или поддерживать какие-либо очереди сообщений
  • Нет задач опроса для обработки
  • Нет необходимости потреблять сообщения

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

  • Открытые интерфейсы двухфазных сообщений полностью независимы от очереди и связаны только с фактическими деловыми и служебными вызовами, что делает их более удобными для разработчиков.
  • 2-фазные сообщения не должны учитывать укладку сообщений и другие сбои, потому что 2-фазные сообщения зависят только от dtm. Разработчики могут думать о dtm как о любой другой обычной службе без сохранения состояния в системе, полагаясь только на хранилище, стоящее за ней, Mysql/Redis.
  • Очередь сообщений является асинхронной, а двухфазные сообщения поддерживают как асинхронные, так и синхронные. Поведение по умолчанию — асинхронное, и вы можете дождаться синхронного завершения нижестоящей службы, просто установив msg.WaitResult=true.
  • 2-фазные сообщения также поддерживают указание нескольких нисходящих сервисов одновременно.

Применение двухфазного сообщения

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

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

Пример использования Redis, механизма хранения Mongo в сочетании с двухфазными сообщениями можно найти в dtm-examples.

Принцип проверки

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

Чтобы выполнить проверку, мы сначала создаем отдельную таблицу в экземпляре бизнес-базы данных, где хранится gid (глобальный идентификатор транзакции). Gid записывается в эту таблицу при обработке бизнес-транзакции.

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

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

  1. Транзакция все еще продолжается.
  2. Транзакция отменена.
  3. Транзакция не началась.

Я искал много информации о возврате RocketMQ, но не нашел безошибочного решения. Большинство предложений заключается в том, что если gid не найден, то ничего не делать и ждать следующей проверки в течение следующих 10 секунд. Если откат длился 2 минуты или дольше и по-прежнему не удается найти gid, то локальная транзакция считается откатанной.

Проблемы возникают в следующих случаях.

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

Эти проблемы полностью решаются двухфазным решением dtm для передачи сообщений. Это работает следующим образом.

  1. Когда локальная транзакция обрабатывается, gid вставляется в таблицу dtm_barrier.barrier с причиной вставки COMMITTED. Таблица dtm_barrier.barrier имеет уникальный индекс в gid.
  2. При обратной проверке двухфазное сообщение не запрашивает напрямую, существует ли gid, а вместо этого вставляет игнорирование строки с тем же gid вместе с причиной ROLLBACKED. В это время, если в таблице уже есть запись с gid, то новая операция вставки будет проигнорирована, иначе строка будет вставлена.
  3. Запросите записи в таблице с помощью gid, если причина записи COMMITTED, то локальная транзакция была зафиксирована; если причина записи ROLLBACKED, то локальная транзакция откатилась или будет откатана.

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

Обычные сообщения

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

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

msg := dtmcli.NewMsg(DtmServer, gid).
	Add(busi.Busi+"/AuthBook", &Req{UID: 1, BookID: 5}).
	Add(busi.Busi+"/AuthBook", &Req{UID: 1, BookID: 6})
err := msg.Submit()

Этот подход также обеспечивает асинхронный интерфейс, не полагаясь на очередь сообщений.

Краткое содержание

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

Добро пожаловать на сайт github.com/dtm-labs/dtm. Это специальный проект, призванный упростить распределенные транзакции в микросервисах. Он поддерживает несколько языков и несколько шаблонов, таких как двухфазное сообщение, Saga, Tcc и Xa.