Esta es una propiedad del aislamiento de transacciones. Se ha escrito mucho al respecto y recomendaría encarecidamente la descripción general en Diseño de uso intensivo de datos Aplicaciones . Descubrí que es la descripción más útil para mejorar mi comprensión personal.
El nivel predeterminado de postgres es LEER COMPROMETIDO lo que permite que cada una de estas transacciones simultáneas vea un estado similar (fondos disponibles) aunque deberían ser dependientes.
Una forma de abordar esto sería marcar cada una de estas transacciones como coherencia "SERIALIZABLE".
Esto debería hacer cumplir la corrección de su solicitud a un costo de disponibilidad, es decir, en este caso, la segunda transacción no podrá modificar los registros y sería rechazada, lo que requeriría un nuevo intento. Para un POC o una aplicación de poco tráfico, este suele ser un primer paso perfectamente aceptable, ya que puede garantizar la corrección en este momento.
También en el libro al que se hace referencia anteriormente, creo que había un ejemplo de cómo los cajeros automáticos manejan la disponibilidad. ¡Permiten esta condición de carrera y el usuario sobregira si no puede conectarse al banco centralizado, pero limita el retiro máximo para minimizar el radio de explosión!
Otra forma arquitectónica de abordar esto es desconectar las transacciones y hacerlas asincrónicas, de modo que cada transacción invocada por el usuario se publique en una cola, y luego, al tener un único consumidor de la cola, naturalmente evita cualquier condición de carrera. La compensación aquí es similar, hay un rendimiento fijo disponible de un solo trabajador, pero ayuda a abordar el problema de corrección por ahora:P
El bloqueo entre máquinas (como el uso de redis en postgres/grpc) se denomina bloqueo distribuido y se ha escrito mucho al respecto https://martin.kleppmann.com/2016/02/08/how-to-distributed-locking.html