stamatios
← Voltar ao feed
Shopify troca Redis por MySQL em reservas de estoque durante Black Friday
Dev & Engenharia

Shopify troca Redis por MySQL em reservas de estoque durante Black Friday

resumo de ~3 min

Contexto e motivação

Durante o checkout na Shopify, quando o comprador clica em "Completar compra", o sistema precisa garantir que os itens ainda estão disponíveis. Um erro pode causar overselling (dois compradores levam o mesmo último item) ou underselling (o sistema diz que esgotou quando ainda há estoque). Na Black Friday de 2025, os lojistas da plataforma atingiram um recorde de US$ 5,1 milhões em vendas por minuto no pico.

Por anos, a proteção contra overselling rodou em Redis. O modelo funcionava para concorrência, mas reservas e o ledger de inventário ficavam em sistemas separados, impossibilitando atomicidade entre reservar e efetivar a dedução. Isso abria espaço para inconsistências (pagamento aprovado sem dedução, ou dedução sem liberação da reserva). Além disso, Redis não tinha suporte a inventário multi-localização e adicionava custo operacional de um cluster separado.

A solução: SKIP LOCKED com uma linha por unidade

A equipe migrou para MySQL usando o recurso SKIP LOCKED (MySQL 8), com um design de uma linha por unidade vendável em vez de uma linha por item com coluna de quantidade. Reservar três unidades significa selecionar e mover três linhas em uma única transação ACID.

Para evitar que itens com dezenas de milhares de unidades gerassem tabelas enormes, o pool de linhas disponíveis é limitado a 1.000 por combinação item/localização. Um processo de reposição reabastece o pool a partir do ledger. Se o pool esgota durante uma flash sale, a reposição é disparada inline com lock para evitar thundering herd.

Decisões técnicas-chave

  • Chave primária composta (shop_id, inventory_item_id, inventory_group_id, id): reduziu de dois locks por linha para um, eliminando o lock extra no índice secundário que o auto-increment causava.
  • READ COMMITTED: evitou gap locks (incluindo no pseudo-record supremum) que bloqueavam a reposição e causavam deadlocks.
  • Ordem consistente de locks: padronizou a sequência de operações entre reserve e claim para eliminar esperas circulares.
  • Batching com UNION ALL: múltiplos itens do carrinho são reservados em uma única ida ao banco.

O gargalo real: conexões, não CPU

O limite de throughput não vinha das queries nem da CPU. Threads ficavam em fila, CPU subia quando o trabalho acumulado executava, e conexões ao MySQL via ProxySQL se esgotavam. A equipe adicionou atribuição por chamador: cada SQL recebe um comentário como /* conn_tag:checkout_completion */, e o ProxySQL agrega tempo de conexão por processo de negócio.

Isso revelou que outras partes do checkout seguravam conexões mais tempo que o necessário. A limpeza removeu 50% das reads e 33% das transações no banco primário. Ajustes na concorrência de threads do InnoDB (configurada conservadoramente anos antes) eliminaram o teto restante. Após as mudanças, CPU de escrita ficou abaixo de 50% e de leitura abaixo de 16%.

Cutover e aprendizados

A migração foi feita em modo sombra: ambos os sistemas rodaram em paralelo, com Redis como fonte de verdade, até validação de correção e performance. O rollout foi gradual, pod por pod.

Lições principais: decisões técnicas antigas merecem reavaliação (SKIP LOCKED não existia antes); o gargalo real estava em código que ninguém estava olhando; e o objetivo não era só tornar reservas rápidas, mas garantir que fossem "vizinhas seguras" sem degradar o banco para carrinhos, pagamentos e criação de pedidos.