sql >> Database teknologi >  >> NoSQL >> Redis

Sådan implementeres en distribueret transaktion på tværs af Mysql, Redis og Mongo

Mysql, Redis og Mongo er alle meget populære butikker, og hver har sine egne fordele. I praktiske applikationer er det almindeligt at bruge flere butikker på samme tid, og at sikre datakonsistens på tværs af flere butikker bliver et krav.

Denne artikel giver et eksempel på implementering af en distribueret transaktion på tværs af flere butiksmotorer, Mysql, Redis og Mongo. Dette eksempel er baseret på Distributed Transaction Framework https://github.com/dtm-labs/dtm og vil forhåbentlig hjælpe med at løse dine problemer med datakonsistens på tværs af mikrotjenester.

Evnen til fleksibelt at kombinere flere lagermotorer for at danne en distribueret transaktion er først foreslået af DTM, og ingen anden distribueret transaktionsramme har angivet denne mulighed.

Problemscenarier

Lad os først se på problemscenariet. Antag, at en bruger nu deltager i en kampagne:han eller hun har en saldo, genoplad telefonregningen, og kampagnen vil give indkøbscenterpoint væk. Saldoen er gemt i Mysql, regningen er gemt i Redis, indkøbscentrene er gemt i Mongo. Fordi kampagnen er begrænset i tid, er der mulighed for, at deltagelse mislykkes, så rollback-support er påkrævet.

Til ovenstående problemscenarie kan du bruge DTM's Saga-transaktion, og vi vil forklare løsningen i detaljer nedenfor.

Forberedelse af data

Det første skridt er at forberede dataene. For at gøre det nemmere for brugerne hurtigt at komme i gang med eksemplerne, har vi udarbejdet de relevante data på en.dtm.pub, som omfatter Mysql, Redis og Mongo, og det specifikke forbindelsesbrugernavn og adgangskode kan findes på https:// github.com/dtm-labs/dtm-examples.

Hvis du selv vil forberede datamiljøet lokalt, kan du bruge https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml til at starte Mysql, Redis, Mongo; og kør derefter scripts i https://github.com/dtm-labs/dtm/tree/main/sqls for at forberede dataene til dette eksempel, hvor busi.* er forretningsdata og barrier.* er den hjælpetabel, der bruges af DTM

At skrive forretningskoden

Lad os starte med forretningskoden for den mest velkendte Mysql.

Følgende kode er i Golang. Andre sprog som C#, PHP, Java kan findes her:DTM SDK'er

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
}

Denne kode udfører hovedsageligt justeringen af ​​brugerens saldo i databasen. I vores eksempel bruges denne del af koden ikke kun til Sagas forward-drift, men også til kompensationsoperationen, hvor der kun skal sendes et negativt beløb ind til kompensation.

For Redis og Mongo håndteres forretningskoden på samme måde, blot forøgelse eller formindskelse af de tilsvarende saldi.

Sådan sikrer du idempotens

For Saga-transaktionsmønsteret, når vi har en midlertidig fejl i sub-transaktionstjenesten, vil den mislykkede operation blive forsøgt igen. Denne fejl kan opstå før eller efter undertransaktionen commits, så undertransaktionen skal være idempotent.

DTM leverer hjælpetabeller og hjælpefunktioner for at hjælpe brugere med at opnå idempotens hurtigt. For Mysql vil det oprette en hjælpetabel barrier i virksomhedsdatabasen, når brugeren starter en transaktion for at justere saldoen, vil den først indsætte Gid i barrier bord. Hvis der er en dublet række, vil indsættelsen mislykkes, og derefter springe balancejusteringen over for at sikre idempotent. Koden ved hjælp af hjælpefunktionen er som følger:

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 håndterer idempotens på samme måde som Mysql, så jeg vil ikke gå i detaljer igen.

Redis håndterer idempotens anderledes end Mysql, primært på grund af forskellen i princippet om transaktioner. Redis-transaktioner sikres hovedsageligt ved atomudførelse af Lua. DTM-hjælperfunktionen justerer balancen via et Lua-script. Inden saldoen justeres, vil den forespørge Gid i Redis. Hvis Gid eksisterer, vil den springe balancejusteringen over; hvis ikke, vil den registrere Gid og udfør balancejusteringen. Koden, der bruges til hjælpefunktionen, er som følger:

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

Sådan laver du kompensation

For Saga skal vi også beskæftige os med kompensationsoperationen, men kompensationen er ikke blot en omvendt justering, og der er mange faldgruber, man skal være opmærksom på.

På den ene side skal kompensation tage hensyn til idempotens, fordi svigt og genforsøg beskrevet i forrige underafsnit også eksisterer i kompensation. På den anden side skal kompensation også tage højde for "nul kompensation", da fremdriften af ​​Saga kan returnere en fejl, som kan være sket før eller efter datajusteringen. For fejl, hvor justeringen er begået, skal vi udføre den omvendte justering; men for fejl, hvor justeringen ikke er blevet begået, er vi nødt til at springe den omvendte operation over.

I hjælpetabellen og hjælpefunktioner leveret af DTM vil den på den ene side afgøre, om kompensationen er en nulkompensation baseret på det Gid, der er indsat ved fremdriften, og på den anden side vil den indsætte Gid+'kompensation' igen at afgøre, om kompensationen er en dobbeltoperation. Hvis der er en normal kompensationsoperation, vil den udføre datajusteringen på virksomheden; hvis der er en ugyldig kompensation eller dobbelt kompensation, vil det springe justeringen over for virksomheden.

Mysql-koden er som følger.

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 til Redis er som følger.

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

Kompensationstjenestekoden er næsten identisk med den tidligere kode for fremsendelsen, bortset fra at beløbet ganges med -1. DTM-hjælperfunktionen håndterer automatisk idempotens og nulkompensation korrekt.

Andre undtagelser

Når man skriver fremdrift og kompensationsoperationer, er der faktisk en anden undtagelse kaldet "Suspension". En global transaktion vil rulle tilbage, når den har timeout, eller genforsøg har nået den konfigurerede grænse. Normaltilfældet er, at fremdriften udføres før kompensationen, men i tilfælde af processuspension kan kompensationen udføres før fremdriften. Så den fremadrettede operation skal også afgøre, om kompensationen er blevet udført, og i tilfælde af, at den har det, skal datajusteringen også springes over.

For DTM-brugere er disse undtagelser blevet håndteret yndefuldt og korrekt, og du som bruger behøver kun at følge MustBarrierFromGin(c).Call opkald beskrevet ovenfor og behøver slet ikke at bekymre sig om dem. Princippet for DTM-håndtering af disse undtagelser er beskrevet i detaljer her:Undtagelser og undertransaktionsbarrierer

Start af en distribueret transaktion

Efter at have skrevet de enkelte deltransaktionstjenester, initierer følgende 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 denne del af koden oprettes en Saga global transaktion, som består af 3 deltransaktioner.

  • Overfør 50 fra Mysql
  • Overfør 30 til Mongo
  • Overfør 20 til Redis

Hvis alle undertransaktioner gennemføres med succes under hele transaktionen, lykkes den globale transaktion; hvis en af ​​undertransaktionerne returnerer en forretningsfejl, så ruller den globale transaktion tilbage.

Kør

Hvis du vil køre et komplet eksempel på ovenstående, er trinene som følger.

  1. Kør DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
  1. Kør et vellykket eksempel
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
  1. Kør et mislykket eksempel
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback

Du kan ændre eksemplet for at simulere forskellige midlertidige fejl, nulkompensationssituationer og forskellige andre undtagelser, hvor dataene er konsistente, når hele den globale transaktion er afsluttet.

Resume

Denne artikel giver et eksempel på en distribueret transaktion på tværs af Mysql, Redis og Mongo. Den beskriver i detaljer de problemer, der skal håndteres, og løsningerne.

Principperne i denne artikel er velegnede til alle lagringsmotorer, der understøtter ACID-transaktioner, og du kan hurtigt udvide det til andre motorer såsom TiKV.

Velkommen til at besøge github.com/dtm-labs/dtm. Det er et dedikeret projekt, der skal gøre distribuerede transaktioner i mikrotjenester nemmere. Det understøtter flere sprog og flere mønstre som en 2-faset besked, Saga, Tcc og Xa.


  1. Kom godt i gang med CouchDB

  2. gradle bygge lokale værker. I docker-container gør det ikke. HVORFOR?

  3. Mongodb aggregeringspipeline, hvordan man begrænser et gruppe-push

  4. Hvordan udfører man mongo-kommandoer gennem shell-scripts?