Redis
 sql >> Base de Dados >  >> NoSQL >> Redis

Como implementar uma transação distribuída no Mysql, Redis e Mongo


Mysql, Redis e Mongo são lojas muito populares e cada uma tem suas próprias vantagens. Em aplicações práticas, é comum usar várias lojas ao mesmo tempo e garantir a consistência dos dados em várias lojas se torna um requisito.

Este artigo fornece um exemplo de implementação de uma transação distribuída em vários mecanismos de loja, Mysql, Redis e Mongo. Este exemplo é baseado no Distributed Transaction Framework https://github.com/dtm-labs/dtm e esperamos ajudar a resolver seus problemas de consistência de dados em microsserviços.

A capacidade de combinar de forma flexível vários mecanismos de armazenamento para formar uma transação distribuída é proposta primeiramente pelo DTM, e nenhuma outra estrutura de transação distribuída declarou essa capacidade.

Cenários de problemas


Vejamos primeiro o cenário do problema. Suponha que um usuário agora participe de uma promoção:ele tem saldo, recarrega a conta telefônica e a promoção vai sortear pontos no shopping. O saldo é armazenado no Mysql, a conta é armazenada no Redis, os pontos do shopping são armazenados no Mongo. Como a promoção é limitada no tempo, existe a possibilidade de a participação falhar, portanto, é necessário suporte de reversão.

Para o cenário de problema acima, você pode usar a transação Saga do DTM e explicaremos a solução em detalhes abaixo.

Preparando os dados


O primeiro passo é preparar os dados. Para tornar mais fácil para os usuários começarem rapidamente com os exemplos, preparamos os dados relevantes em en.dtm.pub, que inclui Mysql, Redis e Mongo, e o nome de usuário e senha de conexão específicos podem ser encontrados em https:// github.com/dtm-labs/dtm-examples.

Se você deseja preparar o ambiente de dados localmente, pode usar https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml para iniciar o Mysql, Redis, Mongo; e, em seguida, execute scripts em https://github.com/dtm-labs/dtm/tree/main/sqls para preparar os dados para este exemplo, onde busi.* são os dados de negócios e barrier.* é a tabela auxiliar usada pelo DTM

Escrevendo o código comercial


Vamos começar com o código de negócios para o MySQL mais familiar.

O código a seguir está em Golang. Outras linguagens como C#, PHP, Java podem ser encontradas aqui: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
}

Este código realiza principalmente o ajuste do saldo do usuário no banco de dados. Em nosso exemplo, esta parte do código é usada não apenas para a operação de encaminhamento do Saga, mas também para a operação de compensação, onde apenas um valor negativo precisa ser repassado para compensação.

Para Redis e Mongo, o código comercial é tratado de forma semelhante, apenas incrementando ou decrementando os saldos correspondentes.

Como garantir a idempotência


Para o padrão de transação Saga, quando houver uma falha temporária no serviço de subtransação, a operação com falha será repetida. Essa falha pode ocorrer antes ou depois da confirmação da subtransação, portanto, a operação da subtransação precisa ser idempotente.

O DTM fornece tabelas auxiliares e funções auxiliares para ajudar os usuários a obter idempotência rapidamente. Para o Mysql, ele criará uma tabela auxiliar barrier no banco de dados de negócios, quando o usuário iniciar uma transação para ajustar o saldo, ele primeiro inserirá Gid na barrier tabela. Se houver uma linha duplicada, a inserção falhará e pule o ajuste de saldo para garantir o idempotente. O código usando a função auxiliar é o seguinte:

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)
    })
}))

O Mongo lida com a idempotência de maneira semelhante ao Mysql, então não entrarei em detalhes novamente.

O Redis trata a idempotência de forma diferente do Mysql, principalmente por causa da diferença no princípio das transações. As transações do Redis são garantidas principalmente pela execução atômica de Lua. a função auxiliar do DTM ajustará o equilíbrio por meio de um script Lua. Antes de ajustar o saldo, ele consultará Gid em Redis. Se Gid existir, pulará o ajuste de saldo; caso contrário, ele gravará Gid e faça o ajuste de equilíbrio. O código usado para a função auxiliar é o seguinte:

app.POST(BusiAPI+"/SagaRedisTransOut", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), -reqFrom(c).Amount, 7*86400)
}))

Como fazer a compensação


Para Saga, também precisamos lidar com a operação de compensação, mas a compensação não é simplesmente um ajuste reverso, e existem muitas armadilhas que devemos estar atentos.

Por um lado, a compensação precisa levar em conta a idempotência, pois a falha e as tentativas descritas na subseção anterior também existem na compensação. Por outro lado, a compensação também precisa levar em consideração a “compensação nula”, pois a operação avante do Saga pode retornar uma falha, que pode ter ocorrido antes ou depois do ajuste dos dados. Para falhas onde o ajuste foi cometido precisamos realizar o ajuste reverso; mas para falhas em que o ajuste não foi confirmado, precisamos pular a operação inversa.

Na tabela auxiliar e nas funções auxiliares fornecidas pelo DTM, por um lado, determinará se a compensação é uma compensação nula com base no Gid inserido pela operação de encaminhamento e, por outro lado, inserirá Gid+'compensar' novamente para determinar se a compensação é uma operação duplicada. Se houver uma operação de compensação normal, então ele executará o ajuste de dados no negócio; se houver compensação nula ou compensação duplicada, pulará o ajuste no negócio.

O código Mysql é o seguinte.

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, "")
    })
}))

O código para Redis é o seguinte.

app.POST(BusiAPI+"/SagaRedisTransOutCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), reqFrom(c).Amount, 7*86400)
}))

O código do serviço de compensação é quase idêntico ao código anterior da operação a termo, exceto que o valor é multiplicado por -1. A função auxiliar DTM trata automaticamente a idempotência e a compensação nula corretamente.

Outras exceções


Ao escrever operações a termo e operações de compensação, existe na verdade outra exceção chamada "Suspensão". Uma transação global será revertida quando atingir o tempo limite ou as novas tentativas atingirem o limite configurado. O caso normal é que a operação forward seja realizada antes da compensação, mas em caso de suspensão do processo a compensação pode ser realizada antes da operação forward. Portanto, a operação de encaminhamento também precisa determinar se a compensação foi executada e, caso tenha, o ajuste de dados também precisa ser ignorado.

Para usuários do DTM, essas exceções foram tratadas de maneira adequada e você, como usuário, precisa apenas seguir o MustBarrierFromGin(c).Call chamada descrita acima e não precisa se preocupar com eles. O princípio para o tratamento dessas exceções pelo DTM é descrito em detalhes aqui:Exceções e barreiras de subtransação

Iniciando uma Transação Distribuída


Depois de escrever os serviços de subtransação individuais, os seguintes códigos do código iniciam uma transação global 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()

Nesta parte do código, é criada uma transação global Saga que consiste em 3 subtransações.
  • Transferir 50 do Mysql
  • Transferência em 30 para Mongo
  • Transfira em 20 para o Redis

Ao longo da transação, se todas as subtransações forem concluídas com êxito, a transação global será bem-sucedida; se uma das subtransações retornar uma falha nos negócios, a transação global será revertida.

Executar


Se você deseja executar um exemplo completo do acima, as etapas são as seguintes.
  1. Executar DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
  1. Execute um exemplo bem-sucedido
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
  1. Executar um exemplo com falha
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback

Você pode modificar o exemplo para simular várias falhas temporárias, situações de compensação nula e várias outras exceções em que os dados são consistentes quando toda a transação global é concluída.

Resumo


Este artigo fornece um exemplo de uma transação distribuída entre Mysql, Redis e Mongo. Ele descreve em detalhes os problemas que precisam ser tratados e as soluções.

Os princípios deste artigo são adequados para todos os mecanismos de armazenamento que suportam transações ACID e você pode estendê-lo rapidamente para outros mecanismos, como o TiKV.

Bem-vindo a visitar github.com/dtm-labs/dtm. É um projeto dedicado a facilitar as transações distribuídas em microsserviços. Ele suporta vários idiomas e vários padrões, como uma mensagem de 2 fases, Saga, Tcc e Xa.