Ir al contenido
angel.library

Transacciones, aislamiento y concurrencia en PostgreSQL

Una transacción agrupa operaciones como una sola unidad lógica. Debe completar todas o revertir todas, pero eso no significa que cualquier secuencia sea segura ante concurrencia.

ProblemaHerramienta inicial
varios cambios forman una unidadtransacción
dos escritores compiten por una filaactualización atómica o FOR UPDATE
edición humana prolongadaversión optimista, no lock abierto
conflicto raro entre rangos o filasSERIALIZABLE con reintento
efecto externo después del committransactional outbox
repetir una peticiónclave de idempotencia
BEGIN;
SELECT stock
FROM products
WHERE id = $1
FOR UPDATE;
UPDATE products
SET stock = stock - $2
WHERE id = $1 AND stock >= $2;
INSERT INTO order_items (order_id, product_id, quantity)
VALUES ($3, $1, $2);
COMMIT;

FOR UPDATE bloquea la fila seleccionada para escrituras incompatibles hasta terminar la transacción. La condición stock >= $2 sigue siendo una defensa importante: una actualización atómica suele ser más segura que leer, calcular en JavaScript y escribir después.

ACID resume atomicidad, consistencia, aislamiento y durabilidad. PostgreSQL usa MVCC (Multi-Version Concurrency Control o control de concurrencia multiversión): cada transacción observa una versión coherente de las filas, lo que reduce bloqueos entre lecturas y escrituras.

UPDATE y DELETE no eliminan inmediatamente todas las versiones antiguas. VACUUM permite reutilizar espacio y evita problemas de identificadores de transacción. Por eso una transacción que permanece abierta durante mucho tiempo también puede dificultar mantenimiento.

NivelIdea práctica
READ COMMITTEDCada sentencia ve datos confirmados al comenzar; es el valor predeterminado
REPEATABLE READLa transacción conserva una instantánea estable
SERIALIZABLEEl resultado equivale a una ejecución serial, pero puede exigir reintentos

PostgreSQL trata READ UNCOMMITTED como READ COMMITTED. REPEATABLE READ evita cambios de la instantánea durante la transacción, pero no sustituye todas las reglas de negocio. SERIALIZABLE detecta dependencias peligrosas y puede abortar una transacción para preservar el resultado serial.

En SERIALIZABLE, un fallo por serialización es un resultado esperado de contención, no necesariamente un error del servidor. La aplicación debe reintentar la transacción completa con límite y espera breve.

  • actualización perdida: dos procesos leen el mismo valor y uno sobrescribe al otro;
  • lectura no repetible: la misma fila cambia entre dos lecturas;
  • phantom: una condición devuelve un conjunto diferente por inserciones concurrentes;
  • write skew: dos transacciones modifican filas distintas basándose en una regla conjunta y ambas la rompen.

Una restricción o actualización atómica suele ser más robusta que “consultar y luego confiar”. Para reglas entre varias filas, estudia el nivel de aislamiento, locks o un modelo que permita expresarlas como constraint.

FormaIntención aproximada
FOR UPDATEbloquear filas que se modificarán
FOR NO KEY UPDATEmodificar sin cambiar claves referenciables
FOR SHAREpermitir lecturas compartidas y bloquear cambios incompatibles
FOR KEY SHAREproteger claves ante eliminación o cambio

NOWAIT falla inmediatamente si la fila está bloqueada. SKIP LOCKED salta filas ocupadas y es útil para colas de trabajo, pero produce una vista deliberadamente inconsistente y no corresponde a cualquier listado.

SELECT id
FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;

Después, el worker marca esos jobs dentro de la misma transacción para que otro worker no los tome.

Un savepoint permite revertir una parte sin descartar toda la transacción:

BEGIN;
INSERT INTO imports (source) VALUES ('catalog.csv');
SAVEPOINT before_optional_metadata;
-- Si esta parte falla:
ROLLBACK TO SAVEPOINT before_optional_metadata;
COMMIT;

No convierte una transacción enorme en una buena idea. Sirve para unidades parciales controladas, especialmente cuando una biblioteca implementa transacciones anidadas mediante savepoints.

No hagas llamadas HTTP, envíes correos ni esperes interacción humana dentro de una transacción. Los locks retenidos aumentan contención y pueden formar deadlocks —esperas circulares—. Para efectos externos utiliza un patrón como transactional outbox: confirma el cambio y un evento pendiente en la misma transacción; un worker procesa el evento después.

PostgreSQL detecta deadlocks y cancela una de las transacciones. Reduce el riesgo adquiriendo recursos en un orden constante, actualizando solo lo necesario y evitando transacciones largas. Registra el contexto sin exponer datos sensibles y reintenta únicamente si la operación completa es segura.

Una transferencia exige debitar, acreditar y registrar el movimiento en una misma transacción. Además necesita:

  • restricciones para impedir saldos inválidos;
  • un orden constante al bloquear cuentas, reduciendo deadlocks;
  • una clave de idempotencia para no repetir la operación;
  • registro auditable y reintentos controlados.
BEGIN;
UPDATE orders SET status = 'paid' WHERE id = $1;
INSERT INTO outbox_events (event_id, kind, payload)
VALUES ($2, 'order.paid', $3::jsonb);
COMMIT;

Un worker publica eventos pendientes y marca su entrega. Como el consumidor también puede recibir duplicados, debe usar event_id para ser idempotente. La outbox evita el fallo “base confirmada, mensaje no enviado”, pero exige monitoreo, reintentos y limpieza.

  • la transacción representa una unidad de negocio;
  • todas las sentencias usan la misma conexión;
  • no hay red ni interacción externa dentro del bloque;
  • locks adquiridos en orden consistente;
  • timeout y rollback en cualquier error;
  • reintento completo y acotado para errores transitorios reconocidos;
  • idempotencia para solicitudes y eventos repetibles.