kb.erickguedes.com
SQL: Domínio de Bancos Relacionais

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ívelDirty ReadNon-repeatable ReadPhantom Read
READ UNCOMMITTEDSimSimSim
READ COMMITTEDNãoSimSim
REPEATABLE READNãoNãoSim
SERIALIZABLENãoNãoNã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.