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, ondebusi.*
são os dados de negócios ebarrier.*
é 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.
- Executar DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
- Execute um exemplo bem-sucedido
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
- 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.