sql >> Databasteknik >  >> NoSQL >> Redis

Hur man implementerar en distribuerad transaktion över Mysql, Redis och Mongo

Mysql, Redis och Mongo är alla mycket populära butiker, och alla har sina egna fördelar. I praktiska tillämpningar är det vanligt att använda flera butiker samtidigt och att säkerställa datakonsistens över flera butiker blir ett krav.

Den här artikeln ger ett exempel på implementering av en distribuerad transaktion över flera butiksmotorer, Mysql, Redis och Mongo. Det här exemplet är baserat på Distributed Transaction Framework https://github.com/dtm-labs/dtm och kommer förhoppningsvis att hjälpa till att lösa dina problem med datakonsistens över mikrotjänster.

Möjligheten att flexibelt kombinera flera lagringsmotorer för att bilda en distribuerad transaktion föreslås först av DTM, och ingen annan distribuerad transaktionsram har angett möjligheten som denna.

Problemscenarier

Låt oss först titta på problemscenariot. Anta att en användare nu deltar i en kampanj:han eller hon har ett saldo, ladda om telefonräkningen och kampanjen ger bort köpcentrumpoäng. Saldot lagras i Mysql, räkningen lagras i Redis, köpcentret lagras i Mongo. Eftersom kampanjen är begränsad i tid finns det en möjlighet att deltagandet kan misslyckas, så återställningsstöd krävs.

För ovanstående problemscenario kan du använda DTM:s Saga-transaktion, och vi kommer att förklara lösningen i detalj nedan.

Förbereda data

Det första steget är att förbereda data. För att göra det enklare för användare att snabbt komma igång med exemplen har vi förberett relevant data på en.dtm.pub, som inkluderar Mysql, Redis och Mongo, och det specifika användarnamnet och lösenordet för anslutningen finns på https:// github.com/dtm-labs/dtm-examples.

Om du själv vill förbereda datamiljön lokalt kan du använda https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml för att starta Mysql, Redis, Mongo; och kör sedan skript i https://github.com/dtm-labs/dtm/tree/main/sqls för att förbereda data för detta exempel, där busi.* är affärsdata och barrier.* är hjälptabellen som används av DTM

Skriva affärskoden

Låt oss börja med affärskoden för den mest välbekanta Mysql.

Följande kod är i Golang. Andra språk som C#, PHP, Java kan hittas här:DTM SDK

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
}

Denna kod utför huvudsakligen justeringen av användarens saldo i databasen. I vårt exempel används denna del av koden inte bara för Sagas forward-verksamhet, utan även för kompensationsoperationen, där endast ett negativt belopp behöver skickas in för kompensation.

För Redis och Mongo hanteras affärskoden på samma sätt, bara ökning eller minskning av motsvarande saldon.

Hur man säkerställer idempotens

För Saga-transaktionsmönstret, när vi har ett tillfälligt fel i deltransaktionstjänsten, kommer den misslyckade operationen att försökas igen. Detta misslyckande kan inträffa före eller efter att deltransaktionen genomförs, så deltransaktionen måste vara idempotent.

DTM tillhandahåller hjälptabeller och hjälpfunktioner för att hjälpa användare att snabbt uppnå idempotens. För Mysql kommer det att skapa en hjälptabell barrier i affärsdatabasen, när användaren startar en transaktion för att justera saldot, kommer den först att infoga Gid i barrier tabell. Om det finns en dubblettrad kommer insättningen att misslyckas och sedan hoppa över balansjusteringen för att säkerställa idempotent. Koden som använder hjälpfunktionen är som följer:

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 hanterar idempotens på ett liknande sätt som Mysql, så jag kommer inte att gå in på detaljer igen.

Redis hanterar idempotens annorlunda än Mysql, främst på grund av skillnaden i principen för transaktioner. Redis-transaktioner säkerställs huvudsakligen genom atomär avrättning av Lua. DTM-hjälparfunktionen kommer att justera balansen via ett Lua-skript. Innan balansen justeras kommer den att fråga Gid i Redis. Om Gid finns, kommer den att hoppa över balansjusteringen; Om inte kommer den att spela in Gid och utför balansjusteringen. Koden som används för hjälpfunktionen är följande:

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

Hur man gör kompensation

För Saga behöver vi också ta itu med kompensationsoperationen, men kompensationen är inte bara en omvänd justering, och det finns många fallgropar som bör vara medvetna om.

Å ena sidan måste ersättningen ta hänsyn till idempotens, eftersom det misslyckande och omförsök som beskrivs i föregående underavsnitt också finns i ersättning. Å andra sidan behöver kompensation också ta hänsyn till "nollkompensation", eftersom framåtdriften av Saga kan returnera ett fel, som kan ha inträffat före eller efter datajusteringen. För fel där justeringen har begåtts måste vi utföra den omvända justeringen; men för fel där justeringen inte har utförts måste vi hoppa över den omvända operationen.

I hjälptabellen och hjälpfunktionerna som tillhandahålls av DTM kommer den å ena sidan att avgöra om kompensationen är en nollkompensation baserat på den Gid som infogats av framåtoperationen, och å andra sidan kommer den att infoga Gid+'kompensera' igen för att avgöra om ersättningen är en dubblettoperation. Om det finns en normal kompensationsoperation kommer den att utföra datajusteringen på verksamheten; om det finns en noll kompensation eller dubblerad kompensation, kommer det att hoppa över justeringen på företaget.

Mysql-koden är som följer.

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

Koden för Redis är följande.

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

Kompensationstjänstens kod är nästan identisk med den föregående koden för vidarebefordran, förutom att beloppet multipliceras med -1. DTM-hjälpfunktionen hanterar automatiskt idempotens och nollkompensation korrekt.

Andra undantag

När man skriver forward operationer och kompensationsoperationer finns det faktiskt ett annat undantag som heter "Avstängning". En global transaktion kommer att rullas tillbaka när den har timeout eller om försök har nått den konfigurerade gränsen. Normalfallet är att framåtoperationen utförs före kompensationen, men vid processavbrott kan kompensationen utföras före forwardoperationen. Så vidarebefordran måste också avgöra om kompensationen har utförts, och i det fall den har gjort det, måste datajusteringen också hoppas över.

För DTM-användare har dessa undantag hanterats graciöst och korrekt och du som användare behöver bara följa MustBarrierFromGin(c).Call samtal som beskrivs ovan och behöver inte bry sig om dem alls. Principen för DTM-hantering av dessa undantag beskrivs i detalj här:Undantag och undertransaktionshinder

Initiera en distribuerad transaktion

Efter att ha skrivit de enskilda deltransaktionstjänsterna initierar följande koder i koden en Saga global transaktion.

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

I denna del av koden skapas en Saga global transaktion som består av 3 deltransaktioner.

  • Överför 50 från Mysql
  • Överför in 30 till Mongo
  • Överför in 20 till Redis

Under hela transaktionen, om alla deltransaktioner slutförs framgångsrikt, så lyckas den globala transaktionen; om en av deltransaktionerna returnerar ett affärsmisslyckande, rullar den globala transaktionen tillbaka.

Kör

Om du vill köra ett komplett exempel på ovanstående är stegen som följer.

  1. Kör DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
  1. Kör ett framgångsrikt exempel
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
  1. Kör ett misslyckat exempel
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback

Du kan modifiera exemplet för att simulera olika tillfälliga fel, situationer med nollkompensation och olika andra undantag där data är konsekventa när hela den globala transaktionen är klar.

Sammanfattning

Den här artikeln ger ett exempel på en distribuerad transaktion över Mysql, Redis och Mongo. Den beskriver i detalj de problem som måste hanteras och lösningarna.

Principerna i den här artikeln är lämpliga för alla lagringsmotorer som stöder ACID-transaktioner, och du kan snabbt utöka den för andra motorer som TiKV.

Välkommen att besöka github.com/dtm-labs/dtm. Det är ett dedikerat projekt för att göra distribuerade transaktioner i mikrotjänster enklare. Den stöder flera språk och flera mönster som ett 2-fasmeddelande, Saga, Tcc och Xa.


  1. Webbskrapning och genomsökning med Scrapy och MongoDB

  2. ställ in expire-nyckeln vid en viss tidpunkt när du använder Spring caching med Redis

  3. MongoDB - kopiera samling i java utan looping alla objekt

  4. Använda Hive för att interagera med HBase, del 1