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:
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.
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".
Lo que sucede ahora en cámara lenta:
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.