DEV Community

Emanoel Carvalho
Emanoel Carvalho

Posted on

Transações em Banco de Dados - Entendendo o Funcionamento

Voltando ao Básico: O que é Atomicidade?

Antes de falar de transaction, precisamos entender o conceito que a sustenta: atomicidade.

O átomo, na física clássica, era considerado a menor unidade indivisível da matéria. Na computação, pegamos emprestado esse conceito para dizer:

"Esse conjunto de operações é indivisível, acontece tudo, ou não acontece nada."

É exatamente isso que uma transaction garante.


A Analogia do PIX

Imagine que você vai fazer um PIX de R$100 para um amigo. Por baixo dos panos, o banco precisa executar duas operações:

1. Debitar R$100 da sua conta
2. Creditar R$100 na conta do seu amigo
Enter fullscreen mode Exit fullscreen mode

Agora imagine que a operação 1 acontece, mas o sistema cai antes da operação 2. O que acontece?

  • ❌ Você perdeu R$100
  • ❌ Seu amigo não recebeu nada
  • ❌ O dinheiro simplesmente sumiu do sistema

Isso é uma inconsistência. E é exatamente o tipo de problema que uma transaction resolve.

Com uma transaction, o banco de dados entende que essas duas operações são uma coisa só. Se a segunda falhar, a primeira é desfeita automaticamente, como se nunca tivesse acontecido, claro, isso não resolve outros problemas como disponibilidade e escalabilidade, mas aqui estou tentando explicar o funcionamento de um conceito, em um sistema real, as coisas são sempre mais complexas.


O Ciclo de Vida de uma Transaction

INÍCIO DA TRANSACTION
        │
        ▼
  [Operação 1] ──── falhou? ──→ ROLLBACK (desfaz tudo)
        │
        ▼
  [Operação 2] ──── falhou? ──→ ROLLBACK (desfaz tudo)
        │
        ▼
  [Operação N] ──── falhou? ──→ ROLLBACK (desfaz tudo)
        │
        ▼
  Tudo certo?
        │
        ▼
     COMMIT ✅ (persiste tudo)
Enter fullscreen mode Exit fullscreen mode

Só existe um de dois desfechos possíveis: commit ou rollback. Nunca um estado no meio.


As 4 Propriedades ACID

Transactions seguem um conjunto de propriedades chamado ACID. Entender cada uma delas é entender por que transactions funcionam:

Propriedade Nome O que garante
A Atomicidade Tudo ou nada
C Consistência O banco sempre vai de um estado válido para outro estado válido
I Isolamento Transactions paralelas não se interferem
D Durabilidade Após o commit, os dados sobrevivem a falhas

💡 Quando um banco de dados diz que é ACID-compliant, ele está dizendo que respeita todas essas propriedades. PostgreSQL, MySQL e SQL Server são exemplos clássicos.


Isolamento — A Propriedade Mais Traiçoeira

O Isolamento merece atenção especial, porque é onde a maioria dos bugs sutis aparecem.

Imagine dois usuários comprando o último item do estoque ao mesmo tempo:

Transaction A                    Transaction B
─────────────────────────────────────────────────
Lê estoque: 1 unidade
                                 Lê estoque: 1 unidade
Decrementa: 0 unidades
                                 Decrementa: 0 unidades ← PROBLEMA
Commit ✅
                                 Commit ✅ ← vendeu algo que não existe
Enter fullscreen mode Exit fullscreen mode

Isso se chama Race Condition, e é um dos problemas que o nível de isolamento da transaction controla. Bancos de dados oferecem diferentes níveis:

Nível Proteção
READ UNCOMMITTED Nenhuma — lê dados ainda não commitados
READ COMMITTED Lê apenas dados já commitados
REPEATABLE READ Garante que releituras retornam o mesmo valor
SERIALIZABLE Máxima proteção — transactions executam como se fossem sequenciais

Na Prática: E-commerce com Sequelize

Agora que o conceito está claro, veja como isso se traduz em código:

const { sequelize, Pedido, Estoque, Sale } = require('./models');

async function finalizarCompra(usuarioId, produtoId, quantidade) {
  // 1. Abre a transaction — a partir daqui, tudo é atômico
  const transaction = await sequelize.transaction();

  try {
    // 2. Lê o estoque dentro da transaction
    const produto = await Estoque.findOne(
      { where: { produtoId } },
      { transaction } // ← vincula essa query à transaction
    );

    if (!produto || produto.quantidade < quantidade) {
      throw new Error('Estoque insuficiente');
    }

    // 3. Decrementa o estoque
    await produto.update(
      { quantidade: produto.quantidade - quantidade },
      { transaction }
    );

    // 4. Cria o pedido
    const pedido = await Pedido.create(
      { usuarioId, produtoId, quantidade, status: 'confirmado' },
      { transaction }
    );

    // 5. Registra a venda
    await Sale.create(
      { pedidoId: pedido.id, valor: produto.preco * quantidade },
      { transaction }
    );

    // 6. Tudo certo — persiste as alterações
    await transaction.commit();

  } catch (error) {
    // 7. Algo falhou — desfaz TUDO
    await transaction.rollback();
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

O que acontece se Sale.create falhar?

  • O estoque volta ao valor original ✅
  • O pedido é removido ✅
  • Nenhuma inconsistência no banco ✅

O Erro Clássico: O Hook Invisível

Aqui está o cenário que quebra sistemas de desenvolvedores experientes. Você escreve uma transaction perfeita, testa, e o sistema entra em deadlock. Você olha para todo o código e não acha o problema.

O culpado? Um hook no model que ninguém lembrou de vincular à transaction.

// ⚠️ Hook disparando FORA da transaction
Pedido.afterCreate(async (pedido) => {
  await LogAuditoria.create({
    entidade: 'Pedido',
    entidadeId: pedido.id,
    acao: 'criado',
  });
  // Essa query não sabe que existe uma transaction aberta
  // Ela tenta acessar o registro do pedido, que ainda está "preso"
  // no lock da transaction — e aí o deadlock acontece
});
Enter fullscreen mode Exit fullscreen mode

Por que o deadlock acontece?

Transaction principal          Hook (conexão separada)
──────────────────────────────────────────────────────
Cria Pedido (lock no registro)
                               Tenta ler o Pedido ← bloqueado pelo lock
Aguarda o hook terminar ←──── Aguarda a transaction liberar o lock
        │                              │
        └──────── DEADLOCK ────────────┘
                  (espera infinita)
Enter fullscreen mode Exit fullscreen mode

✅ A Correção

Pedido.afterCreate(async (pedido, options) => {
  await LogAuditoria.create(
    {
      entidade: 'Pedido',
      entidadeId: pedido.id,
      acao: 'criado',
    },
    { transaction: options.transaction } // ← participa da mesma transaction
  );
});
Enter fullscreen mode Exit fullscreen mode

Agora o hook faz parte da transaction. Se houver rollback, o log também é desfeito. Se houver commit, tudo é salvo junto.


Resumo Mental

Pense em uma transaction como uma caixa de vidro. Tudo que acontece dentro dela é visível apenas para ela mesma, até que você decida abrir a caixa (commit) ou jogar tudo fora (rollback). Qualquer coisa que aconteça fora da caixa, enquanto ela ainda está fechada, não enxerga o que está dentro, e é aí que os problemas aparecem.


No próximo nível: quando um único banco não é suficiente e você precisa de transactions distribuídas entre microsserviços, aí entram patterns como Saga e Outbox. Mas isso é história para outro artigo.

Top comments (0)