"Si un trabajador quiere hacer bien su trabajo, primero debe afilar sus herramientas." - Confucio, "Las Analectas de Confucio. Lu Linggong"
Página delantera > Programación > Cómo lidiar con las condiciones de carrera usando Java y PostgreSQL

Cómo lidiar con las condiciones de carrera usando Java y PostgreSQL

Publicado el 2024-08-01
Navegar:271

How to deal with race conditions using Java and PostgreSQL

Uso del bloqueo para controlar la simultaneidad de la base de datos

Imagina que estás trabajando en un sistema de comercio electrónico y miles de personas intentan comprar el último producto restante al mismo tiempo. Sin embargo, muchos de ellos podrían pasar por caja y finalizar el pedido. Cuando revisas tu stock, tienes un producto con cantidad negativa. ¿Cómo fue posible y cómo se puede solucionar?

¡Codifiquemos! Lo primero que se te ocurrirá es comprobar el stock antes de realizar el pago. Quizás algo como esto:

public void validateAndDecreaseSolution(long productId, int quantity {
    Optional stockByProductId = 
 stockRepository.findStockByProductId(productId);

    int stock = stockByProductId.orElseThrow().getStock();
    int possibleStock = stock - quantity;

    if (stock 



Puedes utilizar esta validación, pero cuando hablamos de cientos, miles, millones o incluso docenas de solicitudes por segundo, esta validación no será suficiente. Cuando 10 solicitudes lleguen a este fragmento de código exactamente al mismo tiempo y la base de datos devuelva el mismo valor para stockByProductId, su código se romperá. Necesita una forma de bloquear otras solicitudes mientras realizamos esta verificación.

Primera solución - PARA ACTUALIZAR

Agregue una declaración de bloqueo en su SELECT. En este ejemplo, hice esto usando FOR UPDATE con Spring Data. Como dice la documentación de PostgreSQL

FOR UPDATE hace que las filas recuperadas por la instrucción SELECT se bloqueen como si fueran a actualizarse. Esto evita que otras transacciones los modifiquen o eliminen hasta que finalice la transacción actual.

@Query(value = "SELECT * FROM stocks s WHERE s.product_id = ?1 FOR UPDATE", nativeQuery = true)
Optional findStockByProductIdWithLock(Long productId);
public void validateAndDecreaseSolution1(long productId, int quantity) {
    Optional stockByProductId = stockRepository.findStockByProductIdWithLock(productId);

    // ... validate

    stockRepository.decreaseStock(productId, quantity);
}

Todas las solicitudes a la tabla de existencias utilizando el ID del producto esperarán hasta que finalice la transacción real. El objetivo aquí es garantizar que obtenga el último valor actualizado de la acción.

Segunda solución: pg_advisory_xact_lock

Esta solución es similar a la anterior, pero puedes seleccionar cuál es la clave de bloqueo. Bloquearemos toda la transacción hasta que finalicemos todo el procesamiento de validación y disminución de stock.

public void acquireLockAndDecreaseSolution2(long productId, int quantity) {
    Query nativeQuery = entityManager.createNativeQuery("select pg_advisory_xact_lock(:lockId)");
    nativeQuery.setParameter("lockId", productId);
    nativeQuery.getSingleResult();

    Optional stockByProductId = stockRepository.findStockByProductId(productId);

    // check stock and throws exception if it is necessary

    stockRepository.decreaseStock(productId, quantity);
}

La siguiente solicitud solo interactuará con un producto con el mismo ID después de que finalice esta transacción.

Tercera solución: cláusula WHERE

En este caso, no bloquearemos nuestra fila o transacción. Permitamos que esta transacción continúe hasta la declaración de actualización. Observe la última condición: stock > 0. Esto no permitirá que nuestro stock sea menor que cero. Entonces si dos personas intentan comprar al mismo tiempo, una de ellas recibirá un error porque nuestra base de datos no permitirá stock

@Transactional
@Modifying
@Query(nativeQuery = true, value = "UPDATE stocks SET stock = stock - :quantity WHERE product_id = :productId AND stock > 0")
int decreaseStockWhereQuantityGreaterThanZero(@Param("productId") Long productId, @Param("quantity") int quantity);

Conclusión

La primera y la segunda solución utilizan el bloqueo pesimista como estrategia. El tercero es el bloqueo optimista. La estrategia de bloqueo pesimista se utiliza cuando desea un acceso restrictivo a un recurso mientras realiza cualquier tarea que involucre este recurso. El recurso de destino quedará bloqueado para cualquier otro acceso hasta que finalice el proceso. ¡Cuidado con los puntos muertos!

Con el bloqueo optimista, puede realizar varias consultas sobre el mismo recurso sin ningún bloqueo. Se utiliza cuando no es probable que ocurran conflictos. Por lo general, tendrá una versión relacionada con su fila y, cuando actualice esta fila, la base de datos comparará la versión de su fila con la versión de la fila en la base de datos. Si ambos son iguales, el cambio será exitoso. Si no, tienes que volver a intentarlo. Como puede ver, no uso ninguna fila de versión en este artículo, pero mi tercera solución no bloquea ninguna solicitud y controla la simultaneidad usando la condición stock > 0.

Si quieres ver el código completo, puedes consultar mi GitHub.

Existen muchas otras estrategias para implementar el bloqueo pesimista y optimista; puede buscar más sobre PARA ACTUALIZAR CON SALTAR BLOQUEADO, por ejemplo.

Declaración de liberación Este artículo se reproduce en: https://dev.to/ramoncunha/how-to-deal-with-race-conditions-using-java-and-postgresql-4jk6?1 Si hay alguna infracción, comuníquese con Study_golang@163 .com para eliminarlo
Último tutorial Más>

Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.

Copyright© 2022 湘ICP备2022001581号-3