Implementa Transacciones ACID en PostgreSQL
Como usar transacciones de PostgreSQL para asegurar Atomicidad, Consistencia, Aislamiento y Durabilidad en operaciones de base de datos de multiples pasos
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Implementa Transacciones ACID en PostgreSQL
Las propiedades ACID — Atomicidad, Consistencia, Aislamiento, Durabilidad — son la fundacion de operaciones de base de datos confiables. PostgreSQL proporciona cumplimiento ACID completo con multiples niveles de aislamiento, savepoints para transacciones anidadas y manejo robusto de errores que asegura integridad de datos incluso en escenarios de fallo.
Cuando Usar Esto
- Multiples operaciones relacionadas deben tener exito o fallar juntas
- El acceso concurrente a los mismos registros requiere comportamiento predecible
- Operaciones financieras, de inventario o reservas no deben dejar datos en estado intermedio
Requisitos Previos
- PostgreSQL 14+ ejecutandose localmente o en un servicio administrado
- Comprension basica de SQL y conexiones a base de datos
Solucion
1. Transaccion Basica con Commit y Rollback
-- Transferir fondos entre cuentas
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- Verificar que ambas actualizaciones tuvieron exito
IF NOT FOUND THEN
ROLLBACK;
RAISE EXCEPTION 'Fondos insuficientes o cuenta no encontrada';
END IF;
COMMIT;
// db/transfer.ts
import { Pool } from 'pg';
async function transferFunds(pool: Pool, fromId: number, toId: number, amount: number) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const debitResult = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1 RETURNING balance',
[amount, fromId]
);
if (debitResult.rowCount === 0) {
throw new Error('Fondos insuficientes');
}
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
return { success: true, newBalance: debitResult.rows[0].balance };
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
2. Niveles de Aislamiento
-- READ COMMITTED (default): previene dirty reads
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1;
-- Otra transaccion hace commit de un cambio aqui
SELECT balance FROM accounts WHERE id = 1; -- ve el cambio commiteado
COMMIT;
-- REPEATABLE READ: previene non-repeatable reads
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
-- Otra transaccion hace commit de un cambio aqui
SELECT balance FROM accounts WHERE id = 1; -- sigue viendo el valor original
COMMIT;
-- SERIALIZABLE: previene phantom reads, aislamiento mas fuerte
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT COUNT(*) FROM orders WHERE status = 'pending';
-- Otra transaccion inserta una orden pendiente
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- mismo count que antes
COMMIT;
3. Savepoints para Operaciones Anidadas
BEGIN;
INSERT INTO orders (customer_id, total) VALUES (1, 250.00) RETURNING id;
-- order_id = 100
SAVEPOINT before_items;
INSERT INTO order_items (order_id, product_id, quantity) VALUES (100, 5, 2);
INSERT INTO order_items (order_id, product_id, quantity) VALUES (100, 8, 1);
-- Rollback parcial si el check de inventario falla
SAVEPOINT before_inventory;
UPDATE inventory SET stock = stock - 2 WHERE product_id = 5;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 8;
-- Si algun stock fue negativo
ROLLBACK TO SAVEPOINT before_inventory;
-- Los items permanecen, pero la actualizacion de inventario se deshace
COMMIT;
4. Advisory Locks para Coordinacion a Nivel de Aplicacion
// db/distributed-lock.ts
async function withAdvisoryLock(pool: Pool, lockId: number, task: () => Promise<void>) {
const client = await pool.connect();
try {
// Obtener advisory lock exclusivo
await client.query('SELECT pg_advisory_lock($1)', [lockId]);
await task();
} finally {
await client.query('SELECT pg_advisory_unlock($1)', [lockId]);
client.release();
}
}
// Uso: prevenir procesamiento duplicado de ordenes
await withAdvisoryLock(pool, orderId, async () => {
await processOrder(orderId);
});
Como Funciona
- Atomicidad asegura que todas las operaciones completen o ninguna lo haga via
COMMIT/ROLLBACK - Consistencia enforcea constraints (foreign keys, check constraints) dentro de transacciones
- Aislamiento previene que transacciones concurrentes interfieran via MVCC y locks
- Durabilidad garantiza que datos commiteados sobreviven crashes a traves de WAL (Write-Ahead Logging)
Consideraciones de Produccion
- Usa READ COMMITTED para la mayoria de aplicaciones; actualiza a SERIALIZABLE solo cuando sea necesario
- Manten transacciones cortas para minimizar contencion de locks
- Usa advisory locks cuando necesites serializacion a nivel de aplicacion entre servicios
- Habilita pg_stat_statements para identificar transacciones de larga duracion
Errores Comunes
- Mantener transacciones abiertas mientras se llaman APIs externas
- No manejar fallos de serializacion en modo SERIALIZABLE
- Olvidar liberar conexiones al pool despues de ROLLBACK
FAQ
P: Deberia usar SERIALIZABLE para todas las transacciones? R: No. SERIALIZABLE tiene mayor overhead y requiere reintentos. READ COMMITTED es suficiente para la mayoria de casos de uso.
P: Que pasa si la conexion cae durante una transaccion? R: PostgreSQL automaticamente hace rollback de cualquier trabajo no commiteado cuando la conexion termina.
P: Como debuggeo contencion de locks?
R: Consulta pg_locks y pg_stat_activity para ver transacciones esperando y sus bloqueadores.
Recursos Relacionados
Optimize Slow Database Queries
How to identify, analyze, and fix slow SQL queries using EXPLAIN, query refactoring, and database-specific optimization techniques.
GuideDatabase Design Guide
A practical guide to designing relational databases with normalization, indexing, and relationship modeling.