¿Cómo garantizar la idempotencia de la interfaz en condiciones de alta concurrencia?

Directorio de artículos

Prefacio:

1. Seleccionar antes de insertar

2. Agregar bloqueo pesimista

 3. Agregue bloqueo optimista

 4. Agregar índice único

Cinco, construye una mesa anti-pesada.

6. Según la máquina de estados.

 7. Agregar bloqueo distribuido

8. Obtener ficha


Prefacio:

        La cuestión de la idempotencia de la interfaz es una cuestión pública para los desarrolladores que no tiene nada que ver con el lenguaje. Este artículo comparte algunas formas muy prácticas de resolver este tipo de problemas. La mayoría de ellas las he implementado en el proyecto y pueden usarse como referencia para amigos necesitados.

No sé si te has encontrado con estos escenarios:

1. A veces, cuando completamos algunos formularios , accidentalmente hacemos clic en el botón Guardar dos veces rápidamente y se generan dos datos duplicados en la tabla, pero las ID son diferentes.

2. Para resolver el problema del tiempo de espera de la interfaz en nuestros proyectos , generalmente introducimos un mecanismo de reintento . Se agotó el tiempo de espera de la primera solicitud a la interfaz y el solicitante no pudo obtener el resultado devuelto a tiempo (es posible que haya tenido éxito en este momento) Para evitar devolver un resultado incorrecto (esta situación no puede devolver directamente un error, ¿verdad?) , la solicitud se reintentará varias veces, lo que también producirá datos duplicados.

3. Cuando los consumidores de mq leen mensajes, a veces leen mensajes duplicados . Si no se procesan bien, también se generarán datos duplicados. Sí, estos son problemas de idempotencia.

La idempotencia de la interfaz significa que los resultados de una solicitud o múltiples solicitudes iniciadas por el usuario para la misma operación son consistentes y no habrá efectos secundarios causados ​​por múltiples clics.

Este tipo de problema ocurre principalmente en la interfaz:

Operación de inserción ; en este caso, varias solicitudes pueden producir datos duplicados.

Si la operación de actualización simplemente actualiza datos, como por ejemplo: actualizar el estado del conjunto de usuarios = 1 donde id = 1 , no hay problema. Si hay cálculos, como: actualizar el estado del conjunto de usuarios = estado +1 donde id = 1 , en este caso, múltiples solicitudes pueden causar errores de datos.

Entonces, ¿cómo garantizamos la idempotencia de la interfaz ?

1. Seleccionar antes de insertar

Normalmente, en la interfaz para guardar datos, para evitar datos duplicados, generalmente seleccionamos los datos según el campo de nombre o código antes de insertarlos . Si los datos ya existen se realiza la operación de actualización, si no existen   se realiza la operación de inserción .

Esta solución puede ser la que más solemos utilizar para evitar datos duplicados. Sin embargo, esta solución no es adecuada para escenarios concurrentes . En escenarios concurrentes, debe usarse junto con otras soluciones, de lo contrario también se generarán datos duplicados . Lo menciono aquí para evitar que nadie se meta en problemas.

2. Agregar bloqueo pesimista

En el escenario de pago, el saldo de la cuenta del usuario A es de 150 yuanes y quiere transferir 100 yuanes. En circunstancias normales, el saldo del usuario A es de sólo 50 yuanes. En general, SQL es así:

update user amount = amount-100 where id=123;

Si la misma solicitud ocurre varias veces, puede hacer que el saldo del usuario A se vuelva negativo. En este caso, el usuario A puede llorar. Al mismo tiempo, los desarrolladores de sistemas también pueden llorar porque se trata de un error de sistema muy grave.

Para resolver este problema, puede agregar un bloqueo pesimista para bloquear la fila de datos del usuario A. Solo se permite una solicitud para obtener el bloqueo y actualizar los datos al mismo tiempo, mientras que otras solicitudes esperan.

Por lo general, una sola fila de datos se bloquea mediante el siguiente SQL:

select * from user id=123 for update;

El proceso específico es el siguiente:

Pasos específicos:

  1. Varias solicitudes consultan información del usuario según la identificación al mismo tiempo.
  2. Determine si el saldo es inferior a 100. Si el saldo es insuficiente, devolverá directamente un saldo insuficiente.
  3. Si el saldo es suficiente, consulte nuevamente la información del usuario para actualizarla e intente adquirir el bloqueo.
  4. Solo la primera solicitud puede obtener el bloqueo de fila, y las solicitudes restantes que no han obtenido el bloqueo esperarán la próxima oportunidad para obtener el bloqueo.
  5. Después de que la primera solicitud obtiene el bloqueo, determina si el saldo es inferior a 100. Si el saldo es suficiente, se realiza la operación de actualización.
  6. Si el saldo es insuficiente, significa que la solicitud se repite y se devolverá el éxito directamente.

Se debe prestar especial atención a: si está utilizando una base de datos mysql, el motor de almacenamiento debe usar innodb , porque solo admite transacciones . Además, el campo de identificación aquí debe ser la clave principal o el índice único; de lo contrario, se bloqueará toda la tabla.

El bloqueo pesimista necesita bloquear una fila de datos durante la misma operación de transacción . Si la transacción lleva mucho tiempo, provocará una gran cantidad de solicitudes en espera y afectará el rendimiento de la interfaz . Además, es difícil garantizar el mismo valor de retorno para cada interfaz de solicitud, por lo que no es adecuado para escenarios de diseño idempotentes, pero puede usarse en escenarios anti-pesados. Por cierto, en realidad existe una diferencia entre el diseño antiduplicación  y  el diseño idempotente . El diseño anti-duplicación tiene como objetivo principal evitar datos duplicados y no tiene muchos requisitos para el retorno de la interfaz. Además de evitar datos duplicados, el diseño idempotente también requiere que cada solicitud devuelva el mismo resultado.

 3. Agregue bloqueo optimista

Dado que el bloqueo pesimista tiene problemas de rendimiento , para mejorar el rendimiento de la interfaz, podemos utilizar el bloqueo optimista. Debe agregar una marca de tiempo o un campo de versión a la tabla . Aquí tomamos el campo de versión como ejemplo.

Consulta los datos antes de actualizarlos:

select id,amount,version from user id=123;

Si los datos existen, suponiendo que la versión encontrada es igual a 1 , utilice los campos id y versión como condiciones de consulta para actualizar los datos:

update user set amount=amount+100,version=version+1where id=123 and version=1;

Mientras se actualizan los datos, se agrega la versión +1 y luego se determina el número de filas afectadas por esta operación de actualización . Si es mayor que 0, significa que la actualización fue exitosa. Si es igual a 0, significa que la actualización no cambió los datos.

Dado que la primera solicitud de versión igual a 1 puede tener éxito, la versión pasa a ser 2 después de que la operación sea exitosa . En este momento, si llegan solicitudes simultáneas, ejecute el mismo sql nuevamente:

 update user set amount=amount+100,version=version+1where id=123 and version=1;

Esta operación de actualización en realidad no actualizará los datos. Al final, el número de filas afectadas por el resultado de la ejecución de SQL es 0 , porque la versión se ha convertido en 2 , y la versión=1 en donde definitivamente no cumplirá las condiciones. Sin embargo, para garantizar la idempotencia de la interfaz, la interfaz puede devolver directamente el éxito. Debido a que se ha modificado el valor de la versión , la solicitud anterior debe haber tenido éxito una vez y las solicitudes posteriores se repetirán.

El proceso específico es el siguiente:

Pasos específicos:

  1. Primero consulte la información del usuario según el ID, incluido el campo de versión.
  2. De acuerdo con los valores de los campos ID y versión como parámetros de la condición Where, la información del usuario se actualiza y la versión +1
  3. Determine el número de filas afectadas por la operación, si afecta a 1 fila significa que es una solicitud y se pueden realizar otras operaciones de datos.
  4. Si se ven afectadas 0 filas, significa que la solicitud se repite y se devolverá el éxito directamente.

 4. Agregar índice único

En la mayoría de los casos, para evitar la generación de datos duplicados, agregaremos un índice único a la tabla, lo que es una solución muy simple y efectiva.

alter table `order` add UNIQUE KEY `un_code` (`code`);

Después de agregar un índice único, la primera solicitud de datos se puede insertar con éxito. Sin embargo, para solicitudes idénticas posteriores, se informará una entrada duplicada '002' para la excepción clave 'order.un_code al insertar datos, lo que indica que el índice único entra en conflicto.

Aunque lanzar una excepción no tiene ningún impacto en los datos, no provocará datos erróneos. Pero para garantizar la idempotencia de la interfaz, debemos detectar la excepción y luego devolver el éxito.

Si es un programa Java , debe detectar: ​​excepción DuplicateKeyException . Si se utiliza Spring Framework , también debe detectar: ​​excepción MySQLIntegrityConstraintViolationException .

El diagrama de flujo específico es el siguiente:

Pasos específicos:

  1. El usuario inicia una solicitud a través del navegador y el servidor recopila datos.
  2. inserte esos datos en mysql
  3. Determine si la ejecución es exitosa y, si es exitosa, opere otros datos (y posiblemente otra lógica comercial). Si la ejecución falla, detecte la excepción de conflicto de índice única y devuelva el éxito directamente.

Cinco, construye una mesa anti-pesada.

A veces, no todos los escenarios de la tabla no permiten datos duplicados, solo ciertos escenarios lo permiten. En este momento, obviamente no es apropiado agregar directamente un índice único a la tabla.

Como respuesta a esta situación, podemos solucionar el problema construyendo una mesa de defensa .

La tabla solo puede contener dos campos: id  e  índice único . El índice único puede ser un identificador único combinado con múltiples campos como nombre, código, etc., por ejemplo: susan_0001.

El diagrama de flujo específico es el siguiente:

Pasos específicos:

  1. El usuario inicia una solicitud a través del navegador y el servidor recopila datos.
  2. Inserte los datos en la tabla anti-pesada de mysql.
  3. Determine si la ejecución es exitosa y, si tiene éxito, realice otras operaciones de datos MySQL (y posiblemente otra lógica comercial).
  4. Si la ejecución falla, detecte la excepción de conflicto de índice única y devuelva el éxito directamente.

Se debe prestar especial atención a lo siguiente: la tabla antiduplicación y la tabla de negocios deben estar en la misma base de datos y las operaciones deben estar en la misma transacción.

6. Según la máquina de estados.

Muchas veces la tabla de negocios tiene estados, por ejemplo la tabla de pedidos tiene: 1-pedido, 2-pagado, 3-completado, 4-cancelado y otros estados. Si los valores de estos estados son regulares y los nodos comerciales son de pequeños a grandes, podemos usarlo para garantizar la idempotencia de la interfaz.

Si el estado del pedido de id=123 es pagado , ahora cambiará al estado completado .

update `order` set status=3 where id=123 and status=2;

Cuando se realiza la primera solicitud, el estado del pedido es pagado y el valor es 2 , por lo que la declaración de actualización puede actualizar los datos normalmente. El número de filas afectadas por el resultado de la ejecución de SQL es 1 y el estado del pedido se convierte en 3 .

La misma solicitud llega más tarde, y cuando se ejecuta nuevamente el mismo SQL, debido a que el estado del pedido se convierte en 3 y se usa status=2 como condición, los datos que deben actualizarse no se pueden consultar, por lo que el número de filas afectadas por el resultado final de la ejecución de SQL es 0. Es decir, los datos en realidad no se actualizarán. Sin embargo, para garantizar la idempotencia de la interfaz, cuando el número de filas afectadas es 0 , la interfaz también puede devolver el éxito directamente.

El diagrama de flujo específico es el siguiente:

Pasos específicos:

  1. El usuario inicia una solicitud a través del navegador y el servidor recopila datos.
  2. Actualizar al siguiente estado según la identificación y el estado actual como condiciones
  3. Determine el número de filas afectadas por la operación. Si 1 fila se ve afectada, la operación actual es exitosa y se pueden realizar otras operaciones de datos.
  4. Si se ven afectadas 0 filas, significa que la solicitud se repite y se devolverá el éxito directamente.

Lo principal a tener en cuenta es que esta solución se limita al caso especial en el que la tabla a actualizar tiene un campo de estado y el campo de estado solo necesita actualizarse. No es aplicable a todos los escenarios.

 7. Agregar bloqueo distribuido

De hecho, agregar un índice único o agregar tablas anti-duplicadas introducido anteriormente es esencialmente usar el bloqueo distribuido de la base de datos , que también es un tipo de bloqueo distribuido. Pero como el rendimiento de los bloqueos distribuidos de la base de datos no es muy bueno, podemos usar redis o zookeeper en su lugar .

Dado que los centros de configuración distribuida de muchas empresas ahora usan apollo o nacos en lugar de zookeeper , usamos redis como ejemplo para introducir bloqueos distribuidos.

Actualmente existen tres formas principales de implementar bloqueos distribuidos de Redis:

  1. comando setNx
  2. establecer comando
  3. Marco de redision

Cada opción tiene sus pros y sus contras y hay demasiados artículos relacionados, por lo que no entraré en detalles aquí.

El diagrama de flujo específico es el siguiente:

Pasos específicos:

  1. El usuario inicia una solicitud a través del navegador, el servidor recopila datos y genera el código del número de pedido como único campo comercial.
  2. Utilice el comando redis set para configurar el código de pedido en redis y configurar el tiempo de espera al mismo tiempo.
  3. Determine si la configuración es exitosa. Si la configuración es exitosa, significa que es la primera solicitud y se realiza la operación de datos.
  4. Si la configuración falla, significa que la solicitud se repite y se devolverá el éxito directamente.

Se debe prestar especial atención a lo siguiente: los bloqueos distribuidos deben configurarse con un tiempo de vencimiento razonable. Si se establece demasiado corto, no se podrán prevenir eficazmente las solicitudes repetidas. Si la configuración es demasiado larga, es posible que se desperdicie espacio de almacenamiento de Redis , que debe determinarse de acuerdo con la situación comercial real.

8. Obtener ficha

Además de las soluciones anteriores, existe una solución final que utiliza tokens . Esta solución es un poco diferente de todas las soluciones anteriores: requiere dos solicitudes para completar una operación comercial.

  1. La primera solicitud para obtener el token.
  2. La segunda solicitud lleva este token para completar la operación comercial.

El diagrama de flujo específico es el siguiente:

El primer paso es conseguir el token primero.

El segundo paso es realizar operaciones comerciales específicas.

 

Pasos específicos:

  1. Cuando un usuario accede a una página, el navegador inicia automáticamente una solicitud de token.
  2. El servidor genera el token, lo guarda en redis y luego lo devuelve al navegador.
  3. Cuando el usuario inicia una solicitud a través del navegador, se transporta el token.
  4. Consulta si el token existe en redis, si no existe significa que es la primera solicitud y se realizarán operaciones de datos posteriores.
  5. Si existe, significa que es una solicitud repetida y se devolverá el éxito directamente.
  6. En redis, el token se eliminará automáticamente después del tiempo de vencimiento.

La solución anterior está diseñada para idempotencia.

Si se trata de un diseño anti-pesado, es necesario cambiar el diagrama de flujo:

Supongo que te gusta

Origin blog.csdn.net/weixin_71921932/article/details/131123737
Recomendado
Clasificación