Как реализовать распределенную транзакцию в Mysql, Redis и Mongo

Mysql, Redis и Mongo — очень популярные хранилища, и каждое из них имеет свои преимущества. В практических приложениях обычно используется несколько хранилищ одновременно, и обеспечение согласованности данных в нескольких хранилищах становится обязательным требованием.

В этой статье приводится пример реализации распределенной транзакции в нескольких хранилищах — Mysql, Redis и Mongo. Этот пример основан на Distributed Transaction Framework https://github.com/dtm-labs/dtm и, надеюсь, поможет решить ваши проблемы с согласованностью данных в микросервисах.

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

Проблемные сценарии

Давайте сначала рассмотрим сценарий проблемы. Предположим, что пользователь участвует в акции: у него есть баланс, он пополняет счет телефона, а в рамках акции раздаются баллы mall. Баланс хранится в Mysql, счет — в Redis, баллы — в Mongo. Поскольку акция ограничена по времени, есть вероятность, что участие в ней может не состояться, поэтому необходима поддержка отката.

Для решения вышеописанной проблемы можно использовать транзакцию Saga от DTM, и мы подробно объясним решение ниже.

Подготовка данных

Первым шагом является подготовка данных. Чтобы пользователям было проще быстро начать работу с примерами, мы подготовили соответствующие данные на en.dtm.pub, которые включают Mysql, Redis и Mongo, а конкретное имя пользователя и пароль для подключения можно найти на сайте https://github.com/dtm-labs/dtm-examples.

Если вы хотите подготовить среду данных локально самостоятельно, вы можете использовать https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml для запуска Mysql, Redis, Mongo; а затем выполнить скрипты в https://github.com/dtm-labs/dtm/tree/main/sqls для подготовки данных для этого примера, где busi.* — это бизнес-данные, а barrier.* — вспомогательная таблица, используемая DTM.

Написание бизнес-кода

Давайте начнем с бизнес-кода для наиболее знакомого Mysql.

Следующий код написан на языке Golang. Другие языки, такие как C#, PHP, Java, можно найти здесь: DTM SDKs

func SagaAdjustBalance(db dtmcli.DB, uid int, amount int) error {
    _, err := dtmimp.DBExec(db, "update dtm_busi.user_account set balance = balance + ? where user_id = ?" , amount, uid)
    return err
}
Вход в полноэкранный режим Выйти из полноэкранного режима

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

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

Как обеспечить идемпотентность

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

DTM предоставляет вспомогательные таблицы и вспомогательные функции, чтобы помочь пользователям быстро достичь идемпотентности. Для Mysql, он создаст вспомогательную таблицу barrier в базе данных бизнеса, когда пользователь начнет транзакцию для корректировки баланса, он сначала вставит Gid в таблицу barrier. Если существует дублирующая строка, то вставка будет неудачной, а затем пропустит корректировку баланса, чтобы обеспечить идемпотентность. Код, использующий вспомогательную функцию, выглядит следующим образом:

app.POST(BusiAPI+"/SagaBTransIn", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
        return SagaAdjustBalance(tx, TransInUID, reqFrom(c).Amount, reqFrom(c).TransInResult)
    })
}))
Вход в полноэкранный режим Выход из полноэкранного режима

Mongo обрабатывает idempotency аналогично Mysql, поэтому я не буду вдаваться в подробности.

Redis обрабатывает idempotency иначе, чем Mysql, в основном из-за разницы в принципе транзакций. Транзакции Redis в основном обеспечиваются атомарным выполнением Lua. Вспомогательная функция DTM будет корректировать баланс с помощью сценария Lua. Перед корректировкой баланса она запросит Gid в Redis. Если Gid существует, она пропустит корректировку баланса; если нет, она запишет Gid и выполнит корректировку баланса. Код, используемый для вспомогательной функции, выглядит следующим образом:

app.POST(BusiAPI+"/SagaRedisTransOut", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), -reqFrom(c).Amount, 7*86400)
}))
Вход в полноэкранный режим Выход из полноэкранного режима

Как выполнить компенсацию

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

С одной стороны, компенсация должна учитывать идемпотентность, поскольку отказ и повторные попытки, описанные в предыдущем подразделе, также существуют в компенсации. С другой стороны, компенсация также должна учитывать «нулевую компенсацию», поскольку прямая операция Saga может вернуть сбой, который мог произойти до или после корректировки данных. Для сбоев, где корректировка была зафиксирована, необходимо выполнить обратную корректировку; но для сбоев, где корректировка не была зафиксирована, необходимо пропустить обратную операцию.

В вспомогательной таблице и вспомогательных функциях, предоставляемых DTM, с одной стороны, он определит, является ли компенсация нулевой компенсацией, основываясь на Gid, вставленном при прямой операции, а с другой стороны, он снова вставит Gid+’compensate’, чтобы определить, является ли компенсация дублирующей операцией. Если есть нормальная операция компенсации, то будет выполнена корректировка данных по бизнесу; если есть нулевая компенсация или дублирующая компенсация, то корректировка данных по бизнесу будет пропущена.

Код Mysql выглядит следующим образом.

app.POST(BusiAPI+"/SagaBTransInCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
        return SagaAdjustBalance(tx, TransInUID, -reqFrom(c).Amount, "")
    })
}))
Вход в полноэкранный режим Выход из полноэкранного режима

Код для Redis выглядит следующим образом.

app.POST(BusiAPI+"/SagaRedisTransOutCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), reqFrom(c).Amount, 7*86400)
}))
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Другие исключения

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

Для пользователей DTM эти исключения были обработаны изящно и правильно, и вам, как пользователю, нужно только следовать описанному выше вызову MustBarrierFromGin(c).Call и не заботиться о них вообще. Принцип обработки таких исключений в DTM подробно описан здесь: Исключения и барьеры субтранзакций

Инициирование распределенной транзакции

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

saga := dtmcli.NewSaga(dtmutil.DefaultHTTPServer, dtmcli.MustGenGid(dtmutil.DefaultHTTPServer)).
  Add(busi.Busi+"/SagaBTransOut", busi.Busi+"/SagaBTransOutCom", &busi.TransReq{Amount: 50}).
  Add(busi.Busi+"/SagaMongoTransIn", busi.Busi+"/SagaMongoTransInCom", &busi.TransReq{Amount: 30}).
  Add(busi.Busi+"/SagaRedisTransIn", busi.Busi+"/SagaRedisTransOutIn", &busi.TransReq{Amount: 20})
err := saga.Submit()
Вход в полноэкранный режим Выход из полноэкранного режима

В этой части кода создается глобальная транзакция Saga, состоящая из 3 подтранзакций.

  • Передача 50 из Mysql
  • Передать 30 в Mongo
  • Передача 20 в Redis

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

Запустить

Если вы хотите запустить полный пример вышеописанного, шаги следующие.

  1. Запустить DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
Войдите в полноэкранный режим Выйдите из полноэкранного режима
  1. Запустить успешный пример
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Запуск неудачного примера
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Резюме

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

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

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

Оцените статью
Procodings.ru
Добавить комментарий