Transactions e Concorrência
Aula 7 de 8
Transactions
BEGIN; -- ou START TRANSACTION
INSERT INTO pedidos (cliente_id, total) VALUES (1, 150.00);
INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, preco)
VALUES (LASTVAL(), 1, 2, 75.00);
UPDATE produtos SET estoque = estoque - 2 WHERE id = 1;
COMMIT; -- confirma tudo ou
ROLLBACK; -- desfaz tudo
-- Savepoints (ponto de rollback parcial)
BEGIN;
INSERT INTO logs (acao) VALUES ('inicio_processo');
SAVEPOINT meu_savepoint;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
-- Se algo der errado:
ROLLBACK TO SAVEPOINT meu_savepoint;
INSERT INTO logs (acao) VALUES ('processo_finalizado');
COMMIT;
ACID
Atomicity: ou tudo acontece, ou nada (COMMIT/ROLLBACK)
Consistency: dados sempre válidos (constraints, triggers)
Isolation: transações concorrentes não interferem
Durability: dados persistidos (não perdem com crash)
Isolation Levels
-- Níveis de isolamento (do mais fraco ao mais forte)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- sujo
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- padrão PostgreSQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- padrão MySQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- mais forte
| Nível | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | Sim | Sim | Sim |
| READ COMMITTED | Não | Sim | Sim |
| REPEATABLE READ | Não | Não | Sim |
| SERIALIZABLE | Não | Não | Não |
-- Fenômenos de concorrência:
-- Dirty Read: ler dados não commitados (transação A escreve, B lê, A rollback)
-- Non-repeatable Read: mesma query, resultados diferentes (linha alterada)
-- Phantom Read: mesma query, linhas diferentes (nova linha inserida)
Locks
-- Lock explícito (evitar race conditions)
BEGIN;
SELECT * FROM contas WHERE id = 1 FOR UPDATE;
-- Outra transação que tentar UPDATE ou SELECT FOR UPDATE na mesma linha vai esperar
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
COMMIT;
-- Lock NOWAIT (não espera, lança erro imediatamente)
SELECT * FROM contas WHERE id = 1 FOR UPDATE NOWAIT;
-- SKIP LOCKED (pula linhas lockadas)
SELECT * FROM fila_processamento
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;
Deadlocks
-- Deadlock: Transação A locks recurso 1, espera recurso 2
-- Transação B locks recurso 2, espera recurso 1
-- Prevenção: sempre lock na mesma ordem!
-- ❌ Ruim
Transação A: UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
Transação B: UPDATE contas SET saldo = saldo - 100 WHERE id = 2;
Transação A: UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
Transação B: UPDATE contas SET saldo = saldo + 100 WHERE id = 1;
-- ✅ Bom: ordenar por ID
Transação A: UPDATE contas SET ... WHERE id IN (1, 2) ORDER BY id;
Transação B: UPDATE contas SET ... WHERE id IN (1, 2) ORDER BY id;
Lab: Transferência Bancária
CREATE TABLE contas (
id SERIAL PRIMARY KEY,
titular VARCHAR(100),
saldo DECIMAL(15,2) NOT NULL CHECK (saldo >= 0)
);
INSERT INTO contas VALUES
(1, 'João', 1000.00),
(2, 'Maria', 500.00);
CREATE OR REPLACE PROCEDURE transferir(
p_origem INT, p_destino INT, p_valor DECIMAL
)
LANGUAGE plpgsql AS $$
BEGIN
-- Ordenar IDs para evitar deadlock
IF p_origem < p_destino THEN
SELECT id INTO p_origem FROM contas WHERE id = p_origem FOR UPDATE;
SELECT id INTO p_destino FROM contas WHERE id = p_destino FOR UPDATE;
ELSE
SELECT id INTO p_destino FROM contas WHERE id = p_destino FOR UPDATE;
SELECT id INTO p_origem FROM contas WHERE id = p_origem FOR UPDATE;
END IF;
IF (SELECT saldo FROM contas WHERE id = p_origem) < p_valor THEN
RAISE EXCEPTION 'Saldo insuficiente';
END IF;
UPDATE contas SET saldo = saldo - p_valor WHERE id = p_origem;
UPDATE contas SET saldo = saldo + p_valor WHERE id = p_destino;
INSERT INTO transferencias (origem, destino, valor)
VALUES (p_origem, p_destino, p_valor);
COMMIT;
END;
$$;
Transactions garantem atomicidade. SELECT FOR UPDATE previne race conditions. Sempre lock recursos na mesma ordem para evitar deadlocks. Isolamento SERIALIZABLE é o mais seguro mas reduz concorrência.