Introdução
Dois postos vendem vagas em uma oficina de artesanato. Um visitante espera uma vaga confirmada, não um contador que apenas pareça não negativo. Você primeiro fará uma reserva normal de grupo, depois reproduzirá duas confirmações para uma vaga em uma oficina descartável de prática. Por fim, protegerá essa vaga e permitirá que outra oficina continue atendendo visitantes.
Você precisa saber navegar pelo terminal e conhecer SELECT, WHERE, INSERT e UPDATE do PostgreSQL. Transações, identidade da conexão, bloqueios de linha, RETURNING e recuperação são explicados aqui. O PostgreSQL e os dados sintéticos estão preparados. Workshop Desk é um shell; Desk A e Desk B são conexões nativas psql separadas ao mesmo banco de dados em uma única máquina. Mantenha as três abas abertas. O texto e os rótulos do ambiente de execução estão em inglês. A leitura e os erros comuns estão incluídos na duração estimada pelo autor; isto não é um estudo de tempo com iniciantes.
Adicione apenas as reservas de teste nomeadas e atualize o places_left de suas oficinas. A prática de concorrência permite explicitamente excluir suas próprias reservas de teste. Preserve as reservas originais, os IDs, títulos, capacidades e notas das oficinas, as definições das tabelas e as ferramentas preparadas. Não altere configurações do servidor nem crie tabelas. Este exercício usa o isolamento READ COMMITTED do PostgreSQL em um banco de dados; pagamentos, novas tentativas e deadlocks envolvendo várias oficinas estão fora do escopo.
Fazer uma reserva normal de grupo
Nesta etapa, você relacionará uma confirmação real de grupo à quantidade de vagas que ela consome.
Comece em Workshop Desk, no shell. ./ executa um programa da pasta atual. desk status consulta as tabelas reais; não as corrige. Seu primeiro resultado lista workshop_id|title|capacity|places_left|booked_seats. O seguinte lista request_code|workshop_id|seats|desk. As barras verticais separam os campos.
./desk status
A oficina 101 tem capacidade 5, uma vaga original reservada e 4 vagas restantes. A oficina 102 tem uma vaga e nenhuma reserva. Mantenha original-family na oficina 101 exatamente como está.
Mude para Desk A. Seu prompt Desk A workshops=> significa que você está no cliente psql do PostgreSQL, conectado ao banco de dados workshops. Insira SQL aqui, não comandos do shell. Um ponto e vírgula encerra uma instrução SQL. Uma conexão é uma conversa com o servidor do banco de dados; pg_backend_pid() identifica seu processo no servidor. Um valor diferente em Desk B identificará uma conexão independente.
SELECT pg_backend_pid(), current_user;
O usuário é workshop_write; o ID do processo varia. BEGIN inicia uma transação: um grupo de alterações que se tornam visíveis juntas em COMMIT. Até então, outra conexão não consegue ver sua nova reserva. ROLLBACK descarta alterações não confirmadas. Neste primeiro exemplo, nenhum outro posto está vendendo a oficina 101.
RETURNING exibe as linhas realmente alteradas por UPDATE. Subtraia 2 vagas e insira a reserva correspondente de duas vagas na mesma transação. Texto usa aspas simples; o código de solicitação 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');
A atualização retorna 101 | 2. O prompt contém *, indicando uma transação aberta. Mude para Workshop Desk e inspecione os registros confirmados:
./desk status
Eles ainda mostram 4 vagas e apenas a reserva original da oficina 101. Volte para Desk A e conclua:
COMMIT;
Mude para Workshop Desk:
./desk status
Agora 101 tem 2 vagas restantes e 3 vagas reservadas: a original mais seu grupo de duas pessoas. Sua reserva real é normal-group|101|2|A. Uma transação bem-sucedida preserva capacity = places_left + booked_seats. A primeira verificação consulta esses fatos de negócio; ela não deduz como você os digitou.
Se digitar SQL incorretamente dentro de uma transação, o prompt pode mudar para !. A transação está então abortada: SQL adicional falhará até ROLLBACK;. Desfaça a transação nesse mesmo posto, inspecione ./desk status em Workshop Desk e repita apenas o trabalho que não foi confirmado. Nunca repita cegamente uma solicitação já confirmada. Um conflito de código de solicitação é um erro, não uma segunda confirmação.
Reproduzir uma decisão baseada em disponibilidade desatualizada
Nesta etapa, você produzirá deliberadamente registros inválidos de prática, observará por que um contador não negativo é insuficiente e manterá o dano restrito à oficina 102.
Mude para Desk A. Este experimento grava deliberadamente um número desatualizado em vez de calcular uma obtenção condicional de vaga. Não o use como método final de reserva. O nível de isolamento READ COMMITTED dá a cada instrução uma visão dos dados confirmados quando essa instrução começa; um SELECT anterior não reserva seu resultado.
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT workshop_id, places_left FROM workshops WHERE workshop_id = 102;
Mude para Desk B. Confirme que seu ID de processo difere do de Desk A e inicie sua própria transação e consulte a mesma vaga:
SELECT pg_backend_pid(), current_user;
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT workshop_id, places_left FROM workshops WHERE workshop_id = 102;
Os dois SELECTs retornam uma vaga. Nenhum SELECT a reservou. Mude para Desk A e salve uma decisão baseada naquele 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');
Deixe a transação de A aberta. Mude para Desk B e insira apenas este UPDATE:
UPDATE workshops SET places_left = 0 WHERE workshop_id = 102;
O prompt ainda não retorna. Essa é uma espera esperada por bloqueio, não uma conexão interrompida. A alterou esta linha e mantém seu bloqueio de linha até a transação terminar. Mude para Workshop Desk:
./desk waits
O backend real de Desk B tem wait_event_type Lock, e blocked_by contém o ID do processo de A. Os IDs variam. O observador apenas consulta a atividade do PostgreSQL; nenhuma animação nem temporizador fornece esse estado.
Mude para Desk A:
COMMIT;
Mude para Desk B. Seu UPDATE agora termina. Ele ainda pode definir o saldo já zerado como zero porque sua condição WHERE apenas identifica a oficina. Adicione sua confirmação e confirme a transação:
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('unsafe-B', 102, 1, 'B');
COMMIT;
Mude para Workshop Desk:
./desk status
A oficina 102 agora tem capacidade 1, vagas restantes 0 e 2 vagas reservadas. Tanto unsafe-A quanto unsafe-B existem. O saldo atendeu à verificação de valor não negativo do banco de dados, mas um visitante ficou sem vaga. Esperar, por si só, não corrigiu a decisão de negócio desatualizada. A próxima etapa remove apenas esses testes explicitamente descartáveis e substitui a decisão. A verificação da etapa aceita esse estado de prática ou o estado final corretamente reparado; não usa o histórico do terminal como prova de sua investigação.
Obter a vaga antes de confirmá-la
Nesta etapa, você exigirá uma obtenção condicional real antes de gravar uma confirmação e recuperará uma solicitação em espera cancelada.
Mude para Desk A. DELETE remove apenas as linhas selecionadas por sua condição WHERE. IN identifica os dois códigos de solicitação descartáveis; não exclua a reserva original nem a normal. Restaure apenas a oficina de prática 102, na mesma transação:
BEGIN;
DELETE FROM bookings WHERE request_code IN ('unsafe-A', 'unsafe-B');
UPDATE workshops SET places_left = 1 WHERE workshop_id = 102;
COMMIT;
Primeiro, pratique o cancelamento de uma espera por bloqueio na oficina separada 104. A desfará sua alteração com ROLLBACK, portanto este teste não cria reserva:
BEGIN;
UPDATE workshops SET places_left = 0 WHERE workshop_id = 104;
Mude para Desk B:
BEGIN;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 104 AND places_left >= 1
RETURNING workshop_id;
Enquanto B espera, pressione Ctrl+C em Desk B. O PostgreSQL informa canceling statement due to user request, com SQLSTATE 57014. SQLSTATE é a categoria de erro de cinco caracteres do PostgreSQL. Uma instrução cancelada deixa esta transação explícita abortada. Demonstre a diferença em relação a um resultado vazio comum:
SELECT 1;
Este é um erro esperado: current transaction is aborted, SQLSTATE 25P02. Recupere a transação em Desk B:
ROLLBACK;
Mude para Desk A e desfaça sua alteração de prática:
ROLLBACK;
Agora proteja a última vaga real da oficina 102. A condição WHERE verifica a existência de vagas suficientes como parte do próprio UPDATE. Após esperar por uma atualização concorrente, READ COMMITTED reavalia essa condição em relação à linha atualizada. RETURNING informa se esta transação realmente obteve a vaga.
Em 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 recebe 102 | 0: obteve a vaga. Apenas porque uma linha foi retornada, crie sua confirmação correspondente:
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('safe-last-A', 102, 1, 'A');
Deixe sem confirmar a transação. Mude para 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. Mude para Workshop Desk e consulte a espera real:
./desk waits
Um bloqueio de linha não deve parar oficinas não relacionadas. Ainda em Workshop Desk, abra uma terceira conexão curta. ./db --desk A usa o mesmo login preparado do banco de dados; esta execução temporária no shell é independente da aba Desk A já aberta. O here-document entre aspas envia todas as linhas até o SQL de fechamento a este cliente e depois o fecha; mantenha o marcador de fechamento sozinho em sua linha. O C armazenado na reserva registra esta terceira solicitação.
./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
Ela retorna 103 | 2 e confirma a transação enquanto a oficina 102 ainda está bloqueada. Mude para Desk A:
COMMIT;
Mude para Desk B. Seu UPDATE agora retorna zero linhas e UPDATE 0: o novo saldo zero não atende a places_left >= 1. Essa é uma instrução SQL bem-sucedida sem obtenção de vaga, não um erro SQL. Não insira uma confirmação. Encerre a transação de B:
ROLLBACK;
Mude para Workshop Desk:
./desk status
A oficina 102 tem uma reserva, safe-last-A|102|1|A, e zero vagas. A oficina 103 tem other-workshop|103|1|C e duas vagas. A oficina 104 ainda tem sua vaga original e nenhuma reserva; 101 mantém seu grupo normal e a família original. Todas as oficinas agora atendem à equação de saldo.
Para uma confirmação comum incorreta de novo teste, primeiro execute ROLLBACK em qualquer transação aberta nesse posto. Neste ambiente de prática, você pode remover apenas seu próprio código incorreto e recalcular apenas o saldo de sua oficina em uma transação. Por exemplo, se confirmou acidentalmente typo-trial na oficina 103, use o seguinte em Desk A e inspecione o status antes de repetir a solicitação pretendida. A subconsulta soma as vagas realmente reservadas; COALESCE(..., 0) usa zero quando não restam reservas. Esta é uma recuperação explícita de prática, não parte do 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;
O exemplo não faz alteração quando você não criou esse erro. Nunca use isso para excluir uma reserva original ou legitimamente solicitada. O próximo lab liga a obtenção da vaga e a confirmação em uma instrução reutilizável, para que o chamador não possa inserir acidentalmente após uma tentativa de obtenção sem linhas.
Resumo
Você criou uma reserva normal real, reproduziu uma reserva acima da capacidade causada por leitura desatualizada e protegeu a última vaga com um UPDATE condicional e uma confirmação na mesma transação. Observações nativas de bloqueio mostraram um posto concorrente esperando enquanto outra oficina continuava. Você também cancelou uma instrução em espera e recuperou sua transação abortada com ROLLBACK. Seus registros finais preservam as reservas originais e relacionam cada vaga confirmada à capacidade restante.



