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

Come implementare una transazione distribuita su Mysql, Redis e Mongo

Mysql, Redis e Mongo sono tutti negozi molto popolari e ognuno ha i suoi vantaggi. Nelle applicazioni pratiche, è comune utilizzare più negozi contemporaneamente e garantire la coerenza dei dati tra più negozi diventa un requisito.

Questo articolo fornisce un esempio di implementazione di una transazione distribuita su più motori di negozio, Mysql, Redis e Mongo. Questo esempio si basa sul Distributed Transaction Framework https://github.com/dtm-labs/dtm e si spera possa aiutare a risolvere i tuoi problemi di coerenza dei dati tra i microservizi.

La capacità di combinare in modo flessibile più motori di archiviazione per formare una transazione distribuita è inizialmente proposta da DTM e nessun altro framework di transazione distribuita ha affermato tale capacità.

Scenari problematici

Diamo prima un'occhiata allo scenario del problema. Supponiamo che un utente partecipi ora a una promozione:lui o lei ha un saldo, ricaricherà la bolletta del telefono e la promozione regalerà punti centro commerciale. Il saldo è archiviato in Mysql, il conto è archiviato in Redis, i punti vendita sono archiviati in Mongo. Poiché la promozione è limitata nel tempo, esiste la possibilità che la partecipazione non vada a buon fine, pertanto è necessario il supporto per il rollback.

Per lo scenario del problema di cui sopra, puoi utilizzare la transazione Saga di DTM e spiegheremo la soluzione in dettaglio di seguito.

Preparazione dei dati

Il primo passo è preparare i dati. Per consentire agli utenti di iniziare rapidamente con gli esempi, abbiamo preparato i dati pertinenti su en.dtm.pub, che include Mysql, Redis e Mongo, e il nome utente e la password di connessione specifici sono disponibili su https:// github.com/dtm-labs/dtm-examples.

Se desideri preparare l'ambiente dati in locale, puoi utilizzare https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml per avviare Mysql, Redis, Mongo; e quindi eseguire gli script in https://github.com/dtm-labs/dtm/tree/main/sqls per preparare i dati per questo esempio, dove busi.* sono i dati aziendali e la barrier.* è la tabella ausiliaria utilizzata da DTM

Scrivere il codice aziendale

Iniziamo con il codice aziendale per il MySQL più familiare.

Il codice seguente è in Golang. Altri linguaggi come C#, PHP, Java possono essere trovati qui: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
}

Questo codice esegue principalmente la regolazione del saldo dell'utente nel database. Nel nostro esempio, questa parte del codice viene utilizzata non solo per l'operazione a termine di Saga, ma anche per l'operazione di compensazione, in cui è necessario trasferire solo un importo negativo per la compensazione.

Per Redis e Mongo, il codice aziendale viene gestito in modo simile, semplicemente incrementando o decrementando i saldi corrispondenti.

Come garantire l'idempotenza

Per il modello di transazione Saga, quando si verifica un errore temporaneo nel servizio di transazione secondaria, l'operazione non riuscita verrà ripetuta. Questo errore può verificarsi prima o dopo il commit della transazione secondaria, quindi l'operazione della transazione secondaria deve essere idempotente.

DTM fornisce tabelle e funzioni di supporto per aiutare gli utenti a raggiungere rapidamente l'idempotenza. Per Mysql, creerà una tabella ausiliaria barrier nel database aziendale, quando l'utente avvia una transazione per aggiustare il saldo, inserirà prima Gid nella barrier tavolo. Se è presente una riga duplicata, l'inserimento avrà esito negativo e quindi salterà la regolazione del saldo per garantire l'idempotenza. Il codice che utilizza la funzione helper è il seguente:

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 gestisce l'idempotenza in modo simile a Mysql, quindi non entrerò più nei dettagli.

Redis gestisce l'idempotenza in modo diverso rispetto a Mysql, principalmente a causa della differenza nel principio delle transazioni. Le transazioni Redis sono principalmente assicurate dall'esecuzione atomica di Lua. la funzione DTM helper regolerà il bilanciamento tramite uno script Lua. Prima di regolare il saldo, interrogherà Gid a Redis. Se Gid esiste, salterà la regolazione del saldo; in caso contrario, registrerà Gid ed eseguire la regolazione del bilanciamento. Il codice utilizzato per la funzione di supporto è il seguente:

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

Come fare la compensazione

Per Saga, dobbiamo anche occuparci dell'operazione di compensazione, ma la compensazione non è semplicemente un aggiustamento inverso e ci sono molte insidie ​​di cui dovresti essere consapevole.

Da un lato, la compensazione deve tenere conto dell'idempotenza, perché il fallimento e i tentativi descritti nella precedente sottosezione esistono anche nella compensazione. D'altra parte, la compensazione deve anche tenere in considerazione la "compensazione nulla", poiché l'operazione a termine di Saga potrebbe restituire un errore, che potrebbe essersi verificato prima o dopo l'adeguamento dei dati. Per i guasti in cui è stato commesso l'adeguamento, è necessario eseguire l'adeguamento inverso; ma per i guasti in cui l'adeguamento non è stato commesso dobbiamo saltare l'operazione inversa.

Nella tabella helper e nelle funzioni helper fornite da DTM, da un lato, determinerà se la compensazione è una compensazione nulla in base al Gid inserito dall'operazione forward, e dall'altro, inserirà nuovamente Gid+'compensate' per determinare se la compensazione è un'operazione duplicata. Se c'è una normale operazione di compensazione, eseguirà l'adeguamento dei dati sull'azienda; se c'è un compenso nullo o un compenso duplicato, salterà l'adeguamento sull'attività.

Il codice MySQL è il seguente.

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

Il codice per Redis è il seguente.

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

Il codice del servizio di compensazione è quasi identico al codice precedente dell'operazione a termine, tranne per il fatto che l'importo viene moltiplicato per -1. La funzione di supporto DTM gestisce automaticamente l'idempotenza e la compensazione nulla in modo corretto.

Altre eccezioni

Quando si scrivono operazioni a termine e operazioni di compensazione, esiste in realtà un'altra eccezione chiamata "Sospensione". Una transazione globale verrà ripristinata quando è scaduto il timeout o quando i tentativi hanno raggiunto il limite configurato. Il caso normale è che l'operazione a termine sia eseguita prima della compensazione, ma in caso di sospensione del processo la compensazione può essere eseguita prima dell'operazione a termine. Quindi l'operazione in avanti deve anche determinare se la compensazione è stata eseguita e, nel caso in cui lo sia, anche la rettifica dei dati deve essere saltata.

Per gli utenti DTM, queste eccezioni sono state gestite correttamente e correttamente e tu, come utente, devi solo seguire il MustBarrierFromGin(c).Call chiamata sopra descritta e non c'è bisogno di preoccuparsene affatto. Il principio per la gestione di queste eccezioni da parte di DTM è descritto in dettaglio qui:Eccezioni e barriere alle sub-transazioni

Avvio di una transazione distribuita

Dopo aver scritto i singoli servizi di sub-transazione, i seguenti codici del codice avviano una transazione globale 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()

In questa parte del codice viene creata una transazione globale Saga composta da 3 sottotransazioni.

  • Trasferisci 50 da Mysql
  • Trasferimento in 30 a Mongo
  • Trasferisci in 20 a Redis

Durante la transazione, se tutte le sottotransazioni vengono completate correttamente, la transazione globale ha esito positivo; se una delle sottotransazioni restituisce un errore aziendale, la transazione globale viene annullata.

Corri

Se vuoi eseguire un esempio completo di quanto sopra, i passaggi sono i seguenti.

  1. Esegui DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
  1. Esegui un esempio di successo
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
  1. Esegui un esempio non riuscito
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback

Puoi modificare l'esempio per simulare vari guasti temporanei, situazioni di compensazione nulla e varie altre eccezioni in cui i dati sono coerenti al termine dell'intera transazione globale.

Riepilogo

Questo articolo fornisce un esempio di transazione distribuita su Mysql, Redis e Mongo. Descrive in dettaglio i problemi che devono essere affrontati e le soluzioni.

I principi in questo articolo sono adatti a tutti i motori di archiviazione che supportano le transazioni ACID e puoi estenderli rapidamente ad altri motori come TiKV.

Benvenuti a visitare github.com/dtm-labs/dtm. È un progetto dedicato per rendere più semplici le transazioni distribuite nei microservizi. Supporta più lingue e più modelli come un messaggio a 2 fasi, Saga, Tcc e Xa.