Mysql, Redis et Mongo sont tous des magasins très populaires, et chacun a ses propres avantages. Dans les applications pratiques, il est courant d'utiliser plusieurs magasins en même temps et assurer la cohérence des données entre plusieurs magasins devient une exigence.
Cet article donne un exemple de mise en œuvre d'une transaction distribuée sur plusieurs moteurs de magasin, Mysql, Redis et Mongo. Cet exemple est basé sur le Distributed Transaction Framework https://github.com/dtm-labs/dtm et nous espérons qu'il vous aidera à résoudre vos problèmes de cohérence des données entre les microservices.
La possibilité de combiner de manière flexible plusieurs moteurs de stockage pour former une transaction distribuée est d'abord proposée par DTM, et aucun autre cadre de transaction distribuée n'a déclaré cette capacité.
Scénarios de problèmes
Examinons d'abord le scénario du problème. Supposons qu'un utilisateur participe maintenant à une promotion :il ou elle a un solde, recharge la facture de téléphone et la promotion offrira des points de centre commercial. Le solde est stocké dans Mysql, la facture est stockée dans Redis, les points du centre commercial sont stockés dans Mongo. Étant donné que la promotion est limitée dans le temps, il est possible que la participation échoue, une prise en charge de la restauration est donc nécessaire.
Pour le scénario de problème ci-dessus, vous pouvez utiliser la transaction Saga de DTM, et nous expliquerons la solution en détail ci-dessous.
Préparation des données
La première étape consiste à préparer les données. Pour permettre aux utilisateurs de démarrer rapidement avec les exemples, nous avons préparé les données pertinentes sur en.dtm.pub, qui comprend Mysql, Redis et Mongo, et le nom d'utilisateur et le mot de passe de connexion spécifiques peuvent être trouvés sur https:// github.com/dtm-labs/dtm-examples.
Si vous souhaitez préparer vous-même l'environnement de données localement, vous pouvez utiliser https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml pour démarrer Mysql, Redis, Mongo ; puis exécutez des scripts dans https://github.com/dtm-labs/dtm/tree/main/sqls pour préparer les données de cet exemple, où
busi.*
est les données d'entreprise etbarrier.*
est la table auxiliaire utilisée par DTM
Rédiger le code de l'entreprise
Commençons par le code métier pour le Mysql le plus familier.
Le code suivant est en Golang. D'autres langages tels que C#, PHP, Java peuvent être trouvés ici :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
}
Ce code effectue principalement l'ajustement du solde de l'utilisateur dans la base de données. Dans notre exemple, cette partie du code est utilisée non seulement pour l'opération de transfert de Saga, mais également pour l'opération de compensation, où seul un montant négatif doit être transmis pour compensation.
Pour Redis et Mongo, le code d'entreprise est géré de la même manière, en incrémentant ou en décrémentant simplement les soldes correspondants.
Comment garantir l'idempotence
Pour le modèle de transaction Saga, en cas d'échec temporaire du service de sous-transaction, l'opération ayant échoué sera retentée. Cet échec peut se produire avant ou après la validation de la sous-transaction, l'opération de sous-transaction doit donc être idempotente.
DTM fournit des tables d'assistance et des fonctions d'assistance pour aider les utilisateurs à atteindre rapidement l'idempotence. Pour Mysql, il va créer une table auxiliaire barrier
dans la base de données métier, lorsque l'utilisateur lance une transaction pour ajuster le solde, il insère d'abord Gid
dans la barrier
table. S'il y a une ligne en double, l'insertion échouera, puis ignorera l'ajustement de la balance pour garantir l'idempotent. Le code utilisant la fonction d'assistance est le suivant :
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 gère l'idempotence de la même manière que Mysql, donc je n'entrerai plus dans les détails.
Redis gère l'idempotence différemment de Mysql, principalement en raison de la différence de principe des transactions. Les transactions Redis sont principalement assurées par l'exécution atomique de Lua. la fonction d'assistance DTM ajustera l'équilibre via un script Lua. Avant d'ajuster le solde, il interrogera Gid
dans Redis. Si Gid
existe, il ignorera l'ajustement du solde ; sinon, il enregistrera Gid
et effectuer le réglage de la balance. Le code utilisé pour la fonction d'assistance est le suivant :
app.POST(BusiAPI+"/SagaRedisTransOut", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), -reqFrom(c).Amount, 7*86400)
}))
Comment effectuer une indemnisation ?
Pour Saga, nous devons également nous occuper de l'opération de compensation, mais la compensation n'est pas simplement un ajustement inverse, et il existe de nombreux pièges dont il faut être conscient.
D'une part, la compensation doit tenir compte de l'idempotence, car l'échec et les tentatives décrites dans la sous-section précédente existent également dans la compensation. D'autre part, la compensation doit également prendre en compte la "compensation nulle", car l'opération en avant de Saga peut renvoyer un échec, qui peut s'être produit avant ou après l'ajustement des données. Pour les défaillances où l'ajustement a été validé, nous devons effectuer l'ajustement inverse ; mais pour les échecs où l'ajustement n'a pas été validé, nous devons ignorer l'opération inverse.
Dans la table d'assistance et les fonctions d'assistance fournies par DTM, d'une part, il déterminera si la compensation est une compensation nulle basée sur le Gid inséré par l'opération vers l'avant, et d'autre part, il insérera à nouveau Gid+'compensate' pour déterminer si la compensation est une opération en double. S'il y a une opération de compensation normale, il exécutera l'ajustement des données sur l'entreprise ; s'il y a une compensation nulle ou une compensation en double, il ignorera l'ajustement sur l'entreprise.
Le code Mysql est le suivant.
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, "")
})
}))
Le code pour Redis est le suivant.
app.POST(BusiAPI+"/SagaRedisTransOutCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), reqFrom(c).Amount, 7*86400)
}))
Le code de service de compensation est presque identique au code précédent de l'opération à terme, sauf que le montant est multiplié par -1. La fonction d'assistance DTM gère automatiquement l'idempotence et la compensation nulle correctement.
Autres exceptions
Lors de l'écriture d'opérations à terme et d'opérations de compensation, il existe en fait une autre exception appelée "Suspension". Une transaction globale sera annulée lorsqu'elle aura expiré ou que les tentatives auront atteint la limite configurée. Le cas normal est que l'opération vers l'avant est effectuée avant la compensation, mais en cas de suspension du processus, la compensation peut être effectuée avant l'opération vers l'avant. Ainsi, l'opération en avant doit également déterminer si la compensation a été exécutée, et dans le cas où c'est le cas, l'ajustement des données doit également être ignoré.
Pour les utilisateurs DTM, ces exceptions ont été gérées avec élégance et correctement et vous, en tant qu'utilisateur, n'avez qu'à suivre le MustBarrierFromGin(c).Call
appel décrit ci-dessus et n'ont pas besoin de s'en soucier du tout. Le principe de gestion de ces exceptions par DTM est décrit en détail ici :Exceptions et barrières de sous-transaction
Initier une transaction distribuée
Après avoir écrit les services de sous-transaction individuels, les codes suivants du code lancent une transaction 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()
Dans cette partie du code, une transaction globale Saga est créée, composée de 3 sous-transactions.
- Transférez 50 depuis Mysql
- Transfert en 30 à Mongo
- Transfert en 20 vers Redis
Tout au long de la transaction, si toutes les sous-transactions se terminent avec succès, la transaction globale réussit ; si l'une des sous-transactions renvoie un échec commercial, la transaction globale est annulée.
Exécuter
Si vous souhaitez exécuter un exemple complet de ce qui précède, les étapes sont les suivantes.
- Exécuter DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
- Exécuter un exemple réussi
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
- Exécuter un exemple ayant échoué
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback
Vous pouvez modifier l'exemple pour simuler diverses défaillances temporaires, des situations de compensation nulle et diverses autres exceptions où les données sont cohérentes lorsque l'ensemble de la transaction globale est terminée.
Résumé
Cet article donne un exemple de transaction distribuée sur Mysql, Redis et Mongo. Il décrit en détail les problèmes à résoudre et les solutions.
Les principes de cet article conviennent à tous les moteurs de stockage prenant en charge les transactions ACID, et vous pouvez rapidement les étendre à d'autres moteurs tels que TiKV.
Bienvenue sur github.com/dtm-labs/dtm. Il s'agit d'un projet dédié pour faciliter les transactions distribuées dans les microservices. Il prend en charge plusieurs langues et plusieurs modèles comme un message en 2 phases, Saga, Tcc et Xa.