paint-brush
Resuelva problemas de concurrencia de bases de datos con TypeOrmpor@gcode
19,196 lecturas
19,196 lecturas

Resuelva problemas de concurrencia de bases de datos con TypeOrm

por 4m2021/01/24
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Resuelva los problemas de simultaneidad con la consulta typeorm para corregir la condición de carrera y el error de interbloqueo.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Resuelva problemas de concurrencia de bases de datos con TypeOrm
null HackerNoon profile picture

Utilizo un patrón en mis servicios donde tengo métodos que siempre devuelven una Entidad, pero depende de ese método recuperarla de la base de datos o crearla de nuevo. Llamo a estos métodos

 getOrCreate*
métodos.

Un ejemplo. Los usuarios pueden agregar un

 Tag
a un
 Post
. Cuando un usuario ingresa una etiqueta "JavaScript", tengo un método en el
 TagService
llamó
 getOrCreateTag(name: string)
. En ese método, pongo el nombre en minúsculas y trato de obtener etiquetas de la base de datos con ese nombre homogeneizado. En caso de que lo encuentre, lo recupera; de lo contrario, creará una nueva entidad de etiqueta y la recuperará.

Esto puede conducir a un problema. Cuando se producen dos solicitudes al backend exactamente al mismo tiempo, con exactamente la misma cadena, se insertan dos etiquetas con el mismo nombre. (¿Crees que es poco probable? ¡Lee la nota debajo del artículo!)

Lo que sucede en cámara lenta:

  1. Solicitud A: pregunta si existe esa etiqueta
  2. no lo encuentra
  3. Decide crearlo
  4. Mientras tanto, la solicitud B pregunta si existe esa etiqueta.
  5. no lo encuentra
  6. Decide crearlo
  7. La solicitud A lo crea
  8. La solicitud B lo crea

Puede evitarlo fácilmente mediante una restricción de clave única. Pero solo cambias el tema. Porque entonces el paso 8 simplemente fallará con una excepción que dejará la aplicación fallando.

Una forma de solucionarlo, la describiré aquí implementada con TypeOrm.

bloqueo pesimista

Siempre se crea un bloqueo de la base de datos durante la escritura en la base de datos. Por ejemplo, los pasos 7 y 8 crean un bloqueo de escritura de muy corta duración. Sin embargo, el problema fue que en los pasos 2 y 5, respectivamente, ambas solicitudes no pudieron encontrarlo y ya decidieron que lo escribirían en la base de datos.

Un bloqueo de lectura pesimista es algo que se crea manualmente . En TypeOrm necesitas la conexión a la base de datos para eso.

Repositorio:

 async getOrCreateTag(name: string): Promise <Tag> { return this .manager.transaction( (entityManager: EntityManager): Promise <Tag> => { return entityManager .createQueryBuilder(Tag, "tag" ) .setLock( "pessimistic_read" ) .where({ name}) .getOne() .then( async (result) => { let tag = result; if ( undefined === tag) { tag = new Tag(); tag.name = name; return await entityManager.save(tag); } return tag; }); } ); }

Entonces, lo que hace es establecer una transacción (requerida para bloqueos) e intenta obtener la etiqueta. Si no es posible (la etiqueta no está definida), creará una.

Hasta aquí todo bien. Si se encontraba con el problema de dos solicitudes que intentaban crear la misma entidad simultáneamente, ahora está mejor. Pero, cuando tenga solicitudes simultáneas, ahora se encontrará con el problema de los interbloqueos .

"Se encontró un punto muerto al intentar obtener el bloqueo; intente reiniciar la transacción".

interbloqueos

Lo que sucede ahora en cámara lenta:

  1. Solicitud A: crea un candado
  2. Solicitud A: pregunta si existe esa etiqueta
  3. no lo encuentra
  4. Decide crearlo
  5. Mientras tanto, la solicitud B intenta crear un bloqueo
  6. encontrará el candado de la solicitud A
  7. fallará con el mensaje de arriba
  8. La solicitud A lo crea

El mensaje significa que debe reiniciar la transacción. Al igual que los bloqueos son manuales, la captura de errores y el reinicio es manual.

El servicio

 async getOrCreateTag(name: string): Promise <Tag> { const maxTries = 10 ; let currentTry = 0 , tryTransaction = true ; let tag = null ; while (tryTransaction) { currentTry++; try { tag = await this .tagRepository.getOrCreateTag( name, this .createTagService ); } catch (e) {} if ( null !== tag) { tryTransaction = false ; // proceed, because everything is fine } if (currentTry >= maxTries) { throw new Error ( "Deadlock on getOrCreateTag found." ); } } return tag; }

Esa es mi solución. Si tienes uno mejor, ¡avísame!

Aunque es muy poco probable que dos usuarios tengan la misma etiqueta ingresando al mismo tiempo, todavía puede ser. Imagine que un usuario crea una etiqueta, pero usted activa dos solicitudes de backend en el frontend cuando lo hace. Uno para almacenar la etiqueta, otro para enviar una notificación automática a las personas que se han suscrito a esa etiqueta.

También hay otras formas con bloqueos optimistas, pero me gusta la idea de dejar que el servicio simplemente "espere" una fracción de segundo. Tengo una aplicación monolítica, así que funciona bien para mí.

Dime qué piensas de esto.