sql >> Base de Datos >  >> NoSQL >> Redis

Cómo implementar una transacción distribuida en Mysql, Redis y Mongo

Mysql, Redis y Mongo son tiendas muy populares y cada una tiene sus propias ventajas. En las aplicaciones prácticas, es común utilizar varias tiendas al mismo tiempo y garantizar la coherencia de los datos en varias tiendas se convierte en un requisito.

Este artículo brinda un ejemplo de implementación de una transacción distribuida en varios motores de tienda, Mysql, Redis y Mongo. Este ejemplo se basa en Distributed Transaction Framework https://github.com/dtm-labs/dtm y, con suerte, ayudará a resolver sus problemas de coherencia de datos entre microservicios.

La capacidad de combinar de manera flexible múltiples motores de almacenamiento para formar una transacción distribuida fue propuesta en primer lugar por DTM, y ningún otro marco de transacciones distribuidas ha establecido una capacidad como esta.

Escenarios de problemas

Veamos primero el escenario del problema. Supongamos que un usuario ahora participa en una promoción:tiene saldo, recarga la factura del teléfono y la promoción le regalará puntos de centro comercial. El saldo se almacena en Mysql, la factura se almacena en Redis, los puntos del centro comercial se almacenan en Mongo. Debido a que la promoción es limitada en el tiempo, existe la posibilidad de que la participación falle, por lo que se requiere soporte de reversión.

Para el escenario del problema anterior, puede usar la transacción Saga de DTM, y le explicaremos la solución en detalle a continuación.

Preparación de los datos

El primer paso es preparar los datos. Para facilitar que los usuarios comiencen rápidamente con los ejemplos, hemos preparado los datos relevantes en en.dtm.pub, que incluye Mysql, Redis y Mongo, y el nombre de usuario y la contraseña de conexión específicos se pueden encontrar en https:// github.com/dtm-labs/dtm-examples.

Si desea preparar el entorno de datos localmente usted mismo, puede usar https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml para iniciar Mysql, Redis, Mongo; y luego ejecute scripts en https://github.com/dtm-labs/dtm/tree/main/sqls para preparar los datos para este ejemplo, donde busi.* son los datos comerciales y la barrier.* es la mesa auxiliar utilizada por DTM

Escribiendo el Código Comercial

Empecemos con el código comercial del Mysql más familiar.

El siguiente código está en Golang. Puede encontrar otros lenguajes como C#, PHP, Java aquí: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
}

Este código realiza principalmente el ajuste del saldo del usuario en la base de datos. En nuestro ejemplo, esta parte del código se usa no solo para la operación de avance de Saga, sino también para la operación de compensación, en la que solo se debe transferir una cantidad negativa para la compensación.

Para Redis y Mongo, el código comercial se maneja de manera similar, simplemente aumentando o disminuyendo los saldos correspondientes.

Cómo garantizar la idempotencia

Para el patrón de transacción de Saga, cuando tengamos una falla temporal en el servicio de subtransacción, se volverá a intentar la operación fallida. Esta falla puede ocurrir antes o después de que se confirme la subtransacción, por lo que la operación de la subtransacción debe ser idempotente.

DTM proporciona tablas auxiliares y funciones auxiliares para ayudar a los usuarios a lograr la idempotencia rápidamente. Para Mysql, creará una tabla auxiliar barrier en la base de datos comercial, cuando el usuario inicie una transacción para ajustar el saldo, primero insertará Gid en la barrier mesa. Si hay una fila duplicada, la inserción fallará y luego omitirá el ajuste de saldo para garantizar el idempotente. El código que utiliza la función auxiliar es el siguiente:

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 maneja la idempotencia de manera similar a Mysql, por lo que no volveré a entrar en detalles.

Redis maneja la idempotencia de manera diferente a Mysql, principalmente debido a la diferencia en el principio de las transacciones. Las transacciones de Redis están garantizadas principalmente por la ejecución atómica de Lua. la función auxiliar de DTM ajustará el saldo a través de un script Lua. Antes de ajustar el saldo, consultará Gid en Redis. Si Gid existe, omitirá el ajuste de saldo; si no, registrará Gid y realice el ajuste de equilibrio. El código utilizado para la función auxiliar es el siguiente:

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

Cómo hacer una Compensación

Para Saga, también debemos lidiar con la operación de compensación, pero la compensación no es simplemente un ajuste inverso, y hay muchas trampas que debe tener en cuenta.

Por un lado, la compensación debe tener en cuenta la idempotencia, porque en la compensación también existe el fallo y los reintentos descritos en el apartado anterior. Por otro lado, la compensación también debe tener en cuenta la "compensación nula", ya que la operación de avance de Saga puede devolver una falla, que puede haber ocurrido antes o después del ajuste de datos. Para fallas donde se ha cometido el ajuste, necesitamos realizar el ajuste inverso; pero para fallas donde el ajuste no se ha comprometido, debemos omitir la operación inversa.

En la tabla auxiliar y las funciones auxiliares proporcionadas por DTM, por un lado, determinará si la compensación es una compensación nula basada en el Gid insertado por la operación de reenvío y, por otro lado, insertará Gid+'compensar' nuevamente. para determinar si la compensación es una operación duplicada. Si hay una operación de compensación normal, entonces ejecutará el ajuste de datos en el negocio; si hay una compensación nula o una compensación duplicada, se saltará el ajuste en el negocio.

El código Mysql es el siguiente.

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

El código para Redis es el siguiente.

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

El código del servicio de compensación es casi idéntico al código anterior de la operación a plazo, excepto que el monto se multiplica por -1. La función auxiliar de DTM gestiona automáticamente la idempotencia y la compensación nula de forma adecuada.

Otras excepciones

Al escribir operaciones a plazo y operaciones de compensación, en realidad hay otra excepción llamada "Suspensión". Una transacción global se revertirá cuando se agote el tiempo de espera o los reintentos alcancen el límite configurado. Lo normal es que la operación de reenvío se realice antes de la compensación, pero en caso de suspensión del proceso la compensación podrá realizarse antes de la operación de reenvío. Por lo tanto, la operación de reenvío también debe determinar si se ha ejecutado la compensación y, en caso de que así sea, también se debe omitir el ajuste de datos.

Para los usuarios de DTM, estas excepciones se han manejado correctamente y usted, como usuario, solo necesita seguir el MustBarrierFromGin(c).Call llamada descrita anteriormente y no necesita preocuparse por ellos en absoluto. El principio para que DTM maneje estas excepciones se describe en detalle aquí:Excepciones y barreras a subtransacciones

Iniciar una transacción distribuida

Después de escribir los servicios de subtransacción individuales, los siguientes códigos del código inician una transacción global de 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()

En esta parte del código, se crea una transacción global de Saga que consta de 3 subtransacciones.

  • Transferir 50 desde Mysql
  • Traslado en 30 a Mongo
  • Transferir en 20 a Redis

A lo largo de la transacción, si todas las subtransacciones se completan con éxito, entonces la transacción global tiene éxito; si una de las subtransacciones devuelve una falla comercial, entonces la transacción global se revierte.

Ejecutar

Si desea ejecutar un ejemplo completo de lo anterior, los pasos son los siguientes.

  1. Ejecutar DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
  1. Ejecutar un ejemplo exitoso
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
  1. Ejecutar un ejemplo fallido
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback

Puede modificar el ejemplo para simular varias fallas temporales, situaciones de compensación nula y varias otras excepciones donde los datos son consistentes cuando finaliza la transacción global completa.

Resumen

Este artículo ofrece un ejemplo de una transacción distribuida en Mysql, Redis y Mongo. Describe en detalle los problemas que deben abordarse y las soluciones.

Los principios de este artículo son adecuados para todos los motores de almacenamiento que admiten transacciones ACID y puede extenderlos rápidamente a otros motores como TiKV.

Bienvenido a visitar github.com/dtm-labs/dtm. Es un proyecto dedicado a facilitar las transacciones distribuidas en microservicios. Admite varios idiomas y varios patrones, como un mensaje de 2 fases, Saga, Tcc y Xa.