Introducción
Dos puestos venden plazas para un taller de artesanía. Una persona espera una plaza confirmada, no un contador que simplemente parezca no negativo. Primero harás una reserva normal de grupo y después reproducirás dos confirmaciones para una sola plaza en un taller de práctica desechable. Por último, protegerás esa plaza y permitirás que otro taller siga atendiendo a sus participantes.
Necesitas conocimientos básicos para moverte por la terminal y de SELECT, WHERE, INSERT y UPDATE en PostgreSQL. Aquí se explican las transacciones, la identidad de conexión, los bloqueos de fila, RETURNING y la recuperación. PostgreSQL y los datos sintéticos están preparados. Workshop Desk es un intérprete del sistema; Desk A y Desk B son conexiones nativas psql independientes a la misma base de datos en una sola máquina. Mantén abiertas las tres pestañas. El texto y las etiquetas del entorno están en inglés. La duración estimada por el autor incluye lectura y errores habituales; no es un estudio de tiempos de principiantes.
Añade únicamente las reservas de prueba indicadas y actualiza places_left de sus talleres. La práctica de concurrencia permite explícitamente borrar tus propias reservas de prueba. Conserva las reservas originales, los ID de taller, los títulos, los aforos, las notas, las definiciones de tablas y las herramientas preparadas. No cambies la configuración del servidor ni crees tablas. Este ejercicio usa el aislamiento READ COMMITTED de PostgreSQL en una base de datos; los pagos, los reintentos y los interbloqueos entre varios talleres quedan fuera de su alcance.
Hacer una reserva normal de grupo
En este paso, relacionarás una confirmación real de grupo con el número de plazas que consume.
Empieza en Workshop Desk, en el intérprete del sistema. ./ ejecuta un programa de la carpeta actual. desk status lee las tablas reales; no las repara. Su primer resultado enumera workshop_id|title|capacity|places_left|booked_seats. El siguiente enumera request_code|workshop_id|seats|desk. Las barras verticales separan los campos.
./desk status
El taller 101 tiene un aforo de 5, una plaza reservada original y 4 plazas libres. El taller 102 tiene una plaza y ninguna reserva. Conserva original-family en el taller 101 exactamente como está.
Cambia a Desk A. Su indicador Desk A workshops=> significa que estás dentro del cliente psql de PostgreSQL, conectado a la base de datos workshops. Introduce aquí SQL, no comandos del intérprete del sistema. Un punto y coma termina una instrucción SQL. Una conexión es una conversación con el servidor de base de datos; pg_backend_pid() identifica su proceso servidor. Un valor diferente en Desk B identificará una conexión independiente.
SELECT pg_backend_pid(), current_user;
El usuario es workshop_write; el ID de proceso varía. BEGIN inicia una transacción: un grupo de cambios que se hacen visibles juntos en COMMIT. Hasta entonces, otra conexión no puede ver tu reserva nueva. ROLLBACK descarta los cambios sin confirmar. En este primer ejemplo, ningún otro puesto vende plazas del taller 101.
RETURNING muestra las filas que UPDATE ha cambiado realmente. Resta 2 plazas e inserta la reserva correspondiente de dos plazas en la misma transacción. El texto usa comillas simples; el código de solicitud identifica esta reserva de forma única.
BEGIN;
UPDATE workshops
SET places_left = places_left - 2
WHERE workshop_id = 101
RETURNING workshop_id, places_left;
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('normal-group', 101, 2, 'A');
La actualización devuelve 101 | 2. El indicador contiene *, que señala una transacción abierta. Cambia a Workshop Desk e inspecciona el registro confirmado:
./desk status
Todavía muestra 4 plazas y solo la reserva original del taller 101. Vuelve a Desk A y termina:
COMMIT;
Cambia a Workshop Desk:
./desk status
Ahora 101 tiene 2 plazas libres y 3 reservadas: la original más tu grupo de dos. Su reserva real es normal-group|101|2|A. Una transacción correcta conserva capacity = places_left + booked_seats. La primera comprobación lee estos datos de negocio; no deduce cómo los escribiste.
Si escribes mal el SQL dentro de una transacción, el indicador puede cambiar a !. La transacción está entonces abortada: el SQL posterior fallará hasta ejecutar ROLLBACK;. Revierte en ese mismo puesto, inspecciona ./desk status en Workshop Desk y repite solo el trabajo que no se haya confirmado. Nunca repitas a ciegas una solicitud ya confirmada. Un conflicto de código de solicitud es un error, no una segunda confirmación.
Reproducir una decisión de disponibilidad desactualizada
En este paso, producirás deliberadamente un registro de práctica no válido, observarás por qué un contador no negativo es insuficiente y limitarás el daño al taller 102.
Cambia a Desk A. Este experimento escribe deliberadamente un número desactualizado en lugar de calcular una asignación condicional de plazas. No lo uses como método final de reserva. El nivel de aislamiento READ COMMITTED da a cada instrucción una vista de los datos confirmados al comenzar esa instrucción; un SELECT anterior no reserva su resultado.
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT workshop_id, places_left FROM workshops WHERE workshop_id = 102;
Cambia a Desk B. Confirma que su ID de proceso difiere del de Desk A, inicia su propia transacción y lee la misma plaza:
SELECT pg_backend_pid(), current_user;
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT workshop_id, places_left FROM workshops WHERE workshop_id = 102;
Ambos SELECT devuelven una plaza. Ninguno la ha reservado. Cambia a Desk A y guarda una decisión basada en ese número anterior:
UPDATE workshops SET places_left = 0 WHERE workshop_id = 102;
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('unsafe-A', 102, 1, 'A');
Deja abierta la transacción de A. Cambia a Desk B e introduce solo este UPDATE:
UPDATE workshops SET places_left = 0 WHERE workshop_id = 102;
El indicador todavía no vuelve. Es una espera de bloqueo prevista, no una conexión rota. A ha cambiado esta fila y mantiene su bloqueo de fila hasta que termine la transacción. Cambia a Workshop Desk:
./desk waits
El proceso servidor real de Desk B tiene wait_event_type Lock y blocked_by contiene el ID de proceso de A. Los ID varían. El observador solo lee la actividad de PostgreSQL; ninguna animación ni temporizador proporciona este estado.
Cambia a Desk A:
COMMIT;
Cambia a Desk B. Su UPDATE termina ahora. Todavía puede poner a cero el saldo que ya era cero porque su condición WHERE solo identifica el taller. Añade su confirmación y confirma la transacción:
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('unsafe-B', 102, 1, 'B');
COMMIT;
Cambia a Workshop Desk:
./desk status
El taller 102 tiene ahora un aforo de 1, 0 plazas libres y 2 plazas reservadas. Existen tanto unsafe-A como unsafe-B. El saldo cumplía la comprobación no negativa de la base de datos, pero una persona no tiene plaza. La espera por sí sola no corrigió la decisión de negocio desactualizada. El siguiente paso elimina únicamente estas pruebas explícitamente desechables y reemplaza la decisión. La comprobación del paso acepta este estado de práctica o el estado final correctamente reparado; no usa el historial de terminal como prueba de tu investigación.
Obtener la plaza antes de confirmarla
En este paso, exigirás una asignación condicional real de la plaza antes de escribir una confirmación y recuperarás una solicitud en espera cancelada.
Cambia a Desk A. DELETE elimina únicamente las filas seleccionadas por su condición WHERE. IN enumera los dos códigos de solicitud desechables; no borres la reserva original ni la normal. Restaura solo el taller de práctica 102, en la misma transacción:
BEGIN;
DELETE FROM bookings WHERE request_code IN ('unsafe-A', 'unsafe-B');
UPDATE workshops SET places_left = 1 WHERE workshop_id = 102;
COMMIT;
Primero practica cómo cancelar una espera de bloqueo en el taller independiente 104. A descartará su cambio con ROLLBACK, por lo que esta prueba no crea ninguna reserva:
BEGIN;
UPDATE workshops SET places_left = 0 WHERE workshop_id = 104;
Cambia a Desk B:
BEGIN;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 104 AND places_left >= 1
RETURNING workshop_id;
Mientras B espera, pulsa Ctrl+C en Desk B. PostgreSQL informa de canceling statement due to user request, con SQLSTATE 57014. SQLSTATE es la categoría de error de cinco caracteres de PostgreSQL. Una instrucción cancelada deja esta transacción explícita abortada. Demuestra la diferencia frente a un resultado vacío normal:
SELECT 1;
Este es un error previsto: current transaction is aborted, SQLSTATE 25P02. Recupérate en Desk B:
ROLLBACK;
Cambia a Desk A y descarta su cambio de práctica:
ROLLBACK;
Ahora protege la última plaza real del taller 102. La condición WHERE comprueba que haya plazas suficientes como parte del propio UPDATE. Después de esperar a una actualización competidora, READ COMMITTED vuelve a comprobar esta condición sobre la fila actualizada. RETURNING te indica si esta transacción obtuvo realmente la plaza.
En Desk A:
BEGIN ISOLATION LEVEL READ COMMITTED;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 102 AND places_left >= 1
RETURNING workshop_id, places_left;
A recibe 102 | 0: ha obtenido la plaza. Solo porque se devolvió una fila, crea su confirmación correspondiente:
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('safe-last-A', 102, 1, 'A');
Déjalo sin confirmar. Cambia a Desk B:
BEGIN ISOLATION LEVEL READ COMMITTED;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 102 AND places_left >= 1
RETURNING workshop_id, places_left;
B espera. Cambia a Workshop Desk y consulta la espera real:
./desk waits
Un bloqueo de fila no debe detener talleres ajenos. Todavía en Workshop Desk, abre una tercera conexión breve. ./db --desk A usa el mismo acceso preparado a la base de datos; esta invocación temporal desde el intérprete es independiente de la pestaña Desk A ya abierta. El here-document entre comillas envía todas las líneas hasta el SQL de cierre a este cliente y después lo cierra; mantén el marcador de cierre solo en su línea. El C almacenado en la reserva registra esta tercera solicitud.
./db --desk A <<'SQL'
BEGIN;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 103 AND places_left >= 1
RETURNING workshop_id, places_left;
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('other-workshop', 103, 1, 'C');
COMMIT;
SQL
Devuelve 103 | 2 y confirma mientras el taller 102 sigue bloqueado. Cambia a Desk A:
COMMIT;
Cambia a Desk B. Su UPDATE devuelve ahora cero filas y UPDATE 0: el nuevo saldo cero no cumple places_left >= 1. Es una instrucción SQL correcta sin asignación de plaza, no un error SQL. No insertes una confirmación. Termina la transacción de B:
ROLLBACK;
Cambia a Workshop Desk:
./desk status
El taller 102 tiene una reserva, safe-last-A|102|1|A, y cero plazas. El taller 103 tiene other-workshop|103|1|C y dos plazas. El taller 104 sigue teniendo su plaza original y ninguna reserva; 101 conserva su grupo normal y su familia original. Todos los talleres satisfacen ahora la ecuación de saldo.
Ante una confirmación errónea habitual de prueba nueva, primero ejecuta ROLLBACK en cualquier transacción abierta de ese puesto. En este entorno de práctica puedes eliminar únicamente tu propio código erróneo y recalcular solo el saldo de su taller en una transacción. Por ejemplo, si confirmaste por accidente typo-trial en el taller 103, usa lo siguiente en Desk A e inspecciona después el estado antes de repetir la solicitud prevista. La subconsulta suma las plazas realmente reservadas; COALESCE(..., 0) usa cero cuando no quedan reservas. Esta es una recuperación explícita de práctica, no parte del algoritmo de reserva:
BEGIN;
DELETE FROM bookings WHERE request_code = 'typo-trial' AND workshop_id = 103;
UPDATE workshops
SET places_left = capacity -
(SELECT COALESCE(SUM(seats), 0) FROM bookings WHERE workshop_id = 103)
WHERE workshop_id = 103;
COMMIT;
El ejemplo no cambia nada si no has cometido ese error. Nunca lo uses para borrar una reserva original o solicitada legítimamente. El siguiente laboratorio vincula la asignación y la confirmación en una instrucción reutilizable, para que quien la llama no pueda insertar por accidente después de una asignación vacía.
Resumen
Creaste una reserva normal real, reprodujiste una sobreventa por lectura desactualizada y protegiste la última plaza con un UPDATE condicional y una confirmación en la misma transacción. Las observaciones de bloqueos nativos mostraron un puesto competidor esperando mientras otro taller continuaba. También cancelaste una instrucción en espera y recuperaste su transacción abortada con ROLLBACK. Tu registro final conserva las reservas originales y relaciona cada plaza confirmada con el aforo restante.



