8.1 ID de transacción

Se designa como XID o TxID (si hay alguna diferencia, dímelo). Las marcas de tiempo se pueden usar como TxID, lo que puede resultar útil si queremos restaurar todas las acciones a algún punto en el tiempo. El problema puede surgir si la marca de tiempo no es lo suficientemente granular, entonces las transacciones pueden obtener la misma ID.

Por lo tanto, la opción más confiable es generar ID de producto UUID únicos. En Python esto es muy fácil:

>>> import uuid 
>>> str(uuid.uuid4()) 
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0' 
>>> str(uuid.uuid4()) 
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba' 

También hay una opción para codificar un conjunto de datos que definen transacciones y usar este hash como TxID.

8.2 Reintentos

Si sabemos que una determinada función o programa es idempotente, esto significa que podemos y debemos intentar repetir su llamada en caso de error. Y solo tenemos que estar preparados para el hecho de que alguna operación dará un error; dado que las aplicaciones modernas se distribuyen a través de la red y el hardware, el error no debe considerarse como una excepción, sino como la norma. El error puede ocurrir debido a un bloqueo del servidor, error de red, congestión de aplicaciones remotas. ¿Cómo debe comportarse nuestra aplicación? Así es, intenta repetir la operación.

Dado que una pieza de código puede decir más que una página completa de palabras, usemos un ejemplo para comprender cómo debería funcionar idealmente el mecanismo de reintento ingenuo. Demostraré esto usando la biblioteca Tenacity (está tan bien diseñada que incluso si no planea usarla, el ejemplo debería mostrarle cómo puede diseñar el mecanismo de recurrencia):

import logging
import random
import sys
from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential, retry_if_exception_type, before_log

logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)

@retry(
	stop=(stop_after_delay(10) | stop_after_attempt(5)),
	wait=wait_exponential(multiplier=1, min=4, max=10),
	retry=retry_if_exception_type(IOError),
	before=before_log(logger, logging.DEBUG)
)
def do_something_unreliable():
	if random.randint(0, 10) > 1:
    	raise IOError("Broken sauce, everything is hosed!!!111one")
	else:
    	return "Awesome sauce!"

print(do_something_unreliable.retry.statistics)

> Por si acaso, diré: \@retry(...) es una sintaxis especial de Python llamada "decorador". Es solo una función de reintento (...) que envuelve otra función y hace algo antes o después de que se ejecute.

Como podemos ver, los reintentos se pueden diseñar de forma creativa:

  • Puede limitar los intentos por tiempo (10 segundos) o número de intentos (5).
  • Puede ser exponencial (es decir, 2 ** algún número creciente n). o de alguna otra manera (por ejemplo, fijo) para aumentar el tiempo entre intentos separados. La variante exponencial se denomina "colapso de congestión".
  • Puede volver a intentarlo solo para ciertos tipos de errores (IOError).
  • Los reintentos pueden estar precedidos o completados por algunas entradas especiales en el registro.

Ahora que hemos completado el curso de joven luchador y conocemos los componentes básicos que necesitamos para trabajar con transacciones en el lado de la aplicación, familiaricémonos con dos métodos que nos permiten implementar transacciones en sistemas distribuidos.

8.3 Herramientas avanzadas para los amantes de las transacciones

Solo daré definiciones bastante generales, ya que este tema merece un artículo extenso por separado.

Compromiso de dos fases (2pc) . 2pc tiene dos fases: una fase de preparación y una fase de confirmación. Durante la fase de preparación, se pedirá a todos los microservicios que se preparen para algunos cambios de datos que se pueden realizar de forma atómica. Una vez que estén todos listos, la fase de confirmación realizará los cambios reales. Para coordinar el proceso, se necesita un coordinador global, que bloquea los objetos necesarios, es decir, se vuelven inaccesibles para cambios hasta que el coordinador los desbloquea. Si un microservicio en particular no está listo para los cambios (por ejemplo, no responde), el coordinador anulará la transacción y comenzará el proceso de reversión.

¿Por qué es bueno este protocolo? Proporciona atomicidad. Además, garantiza el aislamiento a la hora de escribir y leer. Esto significa que los cambios en una transacción no son visibles para los demás hasta que el coordinador confirma los cambios. Pero estas propiedades también tienen una desventaja: como este protocolo es síncrono (bloqueador), ralentiza el sistema (a pesar de que la propia llamada RPC es bastante lenta). Y de nuevo, existe el peligro de un bloqueo mutuo.

saga _ En este patrón, una transacción distribuida se ejecuta mediante transacciones locales asíncronas en todos los microservicios asociados. Los microservicios se comunican entre sí a través de un bus de eventos. Si algún microservicio no completa su transacción local, otros microservicios realizarán transacciones compensatorias para revertir los cambios.

La ventaja de Saga es que no se bloquea ningún objeto. Pero hay, por supuesto, desventajas.

Saga es difícil de depurar, especialmente cuando hay muchos microservicios involucrados. Otra desventaja del patrón Saga es que carece de aislamiento de lectura. Es decir, si las propiedades indicadas en ACID son importantes para nosotros, entonces Saga no es muy adecuado para nosotros.

¿Qué vemos en la descripción de estas dos técnicas? El hecho de que en los sistemas distribuidos, la responsabilidad de la atomicidad y el aislamiento recae en la aplicación. Lo mismo sucede cuando se utilizan bases de datos que no brindan garantías ACID. Es decir, cosas como la resolución de conflictos, las reversiones, las confirmaciones y la liberación de espacio recaen sobre los hombros del desarrollador.

8.4 ¿Cómo sé cuándo necesito las garantías de ACID?

Cuando existe una alta probabilidad de que un determinado conjunto de usuarios o procesos trabajen simultáneamente en los mismos datos .

Perdón por la banalidad, pero un ejemplo típico son las transacciones financieras.

Cuando importa el orden en que se ejecutan las transacciones.

Imagine que su empresa está a punto de cambiar del mensajero FunnyYellowChat al mensajero FunnyRedChat, porque FunnyRedChat le permite enviar gifs, pero FunnyYellowChat no. Pero no solo está cambiando el mensajero: está migrando la correspondencia de su empresa de un mensajero a otro. Haces esto porque tus programadores eran demasiado perezosos para documentar programas y procesos en algún lugar central y, en cambio, publicaron todo en diferentes canales en el mensajero. Sí, y sus vendedores publicaron los detalles de las negociaciones y acuerdos en el mismo lugar. En resumen, toda la vida de su empresa está ahí, y dado que nadie tiene tiempo de transferir todo a un servicio de documentación, y la búsqueda de mensajería instantánea funciona bien, decidió en lugar de limpiar los escombros simplemente copiar todos los mensajes a una nueva ubicación. El orden de los mensajes es importante

Por cierto, para la correspondencia en un mensajero, el orden generalmente es importante, pero cuando dos personas escriben algo en el mismo chat al mismo tiempo, en general, no es tan importante de quién mensaje aparecerá primero. Entonces, para este escenario en particular, ACID no sería necesario.

Otro posible ejemplo es la bioinformática. No entiendo esto en absoluto, pero asumo que el orden es importante al descifrar el genoma humano. Sin embargo, escuché que los bioinformáticos generalmente usan algunas de sus herramientas para todo, tal vez tengan sus propias bases de datos.

Cuando no puedes dar a un usuario o procesar datos obsoletos.

Y de nuevo - transacciones financieras. Para ser honesto, no podía pensar en ningún otro ejemplo.

Cuando las transacciones pendientes están asociadas con costos significativos. Imagine los problemas que pueden surgir cuando un médico y una enfermera actualizan el registro de un paciente y borran los cambios del otro al mismo tiempo, porque la base de datos no puede aislar las transacciones. El sistema de salud es otra área, además de las finanzas, donde las garantías de ACID tienden a ser críticas.

8.5 ¿Cuándo no necesito ACID?

Cuando los usuarios actualizan solo algunos de sus datos privados.

Por ejemplo, un usuario deja comentarios o notas adhesivas en una página web. O edita datos personales en una cuenta personal con un proveedor de cualquier servicio.

Cuando los usuarios no actualizan los datos en absoluto, sino que solo los complementan con otros nuevos (adjuntar).

Por ejemplo, una aplicación de carrera que guarda datos de tus carreras: cuánto has corrido, a qué hora, ruta, etc. Cada nueva ejecución son datos nuevos y los antiguos no se editan en absoluto. Tal vez, según los datos, obtenga análisis, y solo las bases de datos NoSQL son buenas para este escenario.

Cuando la lógica empresarial no determina la necesidad de un determinado orden en el que se realizan las transacciones.

Probablemente, para un blogger de Youtube que recolecta donaciones para la producción de nuevo material durante la próxima transmisión en vivo, no es tan importante quién, cuándo y en qué orden le tiró el dinero.

Cuando los usuarios permanecerán en la misma página web o ventana de la aplicación durante varios segundos o incluso minutos y, por lo tanto, de alguna manera verán datos obsoletos.

Teóricamente, estos son cualquier medio de noticias en línea, o el mismo Youtube. O "Habr". Cuando no le importa que las transacciones incompletas puedan almacenarse temporalmente en el sistema, puede ignorarlas sin ningún daño.

Si está agregando datos de muchas fuentes y datos que se actualizan con alta frecuencia, por ejemplo, datos sobre la ocupación de espacios de estacionamiento en una ciudad que cambia al menos cada 5 minutos, en teoría no será un gran problema. para usted si en algún momento la transacción de uno de los estacionamientos no se realiza. Aunque, por supuesto, depende de qué quieras hacer exactamente con estos datos.