O que e concorrência em sistemas?

Concorrência e a capacidade de múltiplos processos acessarem o mesmo recurso ao mesmo tempo.

Em sistemas modernos, dezenas de milhares de requisições podem acessar os mesmos dados simultaneamente. A concorrência e necessária para escala, mas introduz problemas sutis e críticos quando múltiplos processos leem e modificam os mesmos dados sem coordenação adequada. O sistema precisa garantir que operações concorrentes produzam resultados corretos independentemente de como elas sejam intercaladas. Esses problemas aparecem em sistemas de pagamento, reservas de assentos, controle de estoque e qualquer cenário onde a condição atual de um dado precisa ser verificada antes de uma modificação. Entender concorrência e essencial para qualquer desenvolvedor que construa sistemas com usuários reais.

Race condition

Dois processos modificam o mesmo dado simultaneamente, o resultado depende de qual termina primeiro.

Uma race condition ocorre quando dois processos leem o mesmo valor, calculam uma modificação com base nele e escrevem de volta, sem que um saiba da operação do outro. O resultado final depende da ordem de execução, que pode variar a cada execução. Exemplo clássico: dois usuários tentam comprar o último item em estoque simultaneamente. Ambos leem estoque = 1, ambos verificam que e maior que zero, ambos decrementam e escrevem estoque = 0. O resultado correto seria um sucesso e um erro. O resultado com race condition podem ser dois sucessos com estoque = -1. Esse tipo de bug e difícil de reproduzir porque ocorre apenas sob alta concorrência.

Lock pessimista

Bloquear o recurso antes de modificar, garante exclusividade mas pode gerar deadlock.

Lock pessimista e a estrategia de adquirir um lock exclusivo sobre o recurso antes de le-lo e modifica-lo. No SQL, isso e feito com SELECT FOR UPDATE, que bloqueia as linhas selecionadas até o COMMIT ou ROLLBACK. Outros processos que tentarem acessar essas linhas ficam aguardando. Isso garante que apenas uma operação modifica o dado por vez, eliminando race conditions. O custo e throughput reduzido: operações que poderiam ser paralelas ficam serializadas. Em sistemas com alto volume de escrita nas mesmas linhas, lock pessimista pode criar gargalos serios. Também aumenta o risco de deadlock se múltiplos processos adquirem locks em ordens diferentes.

Lock otimista

Tentar modificar sem bloquear, verificar conflito no commit, ideal quando conflitos são raros.

Lock otimista assume que conflitos são raros e não bloqueia recursos antecipadamente. Em vez disso, registra o estado do dado no momento da leitura, geralmente via campo version, e ao tentar escrever, verifica se o estado ainda e o mesmo. Se outra transação modificou o dado enquanto isso, a escrita falha e a operação precisa ser repetida. A vantagem e que não há bloqueio: múltiplas operações podem ler simultaneamente sem se impedir. A desvantagem e que em cenários de alta contenção, muitos processos tentando modificar o mesmo dado, a taxa de retries pode ser alta. Lock otimista e ideal para dados que são lidos com frequência mas modificados raramente.

Versionamento otimista

Adicionar campo version ao registro, incrementar ao atualizar, rejeitar se version mudou.

A implementação mais comum de lock otimista e adicionar uma coluna version (ou timestamp) a cada tabela. Ao ler um registro, armazena-se o valor atual de version. Ao atualizar, a query inclui WHERE version = versao_lida no filtro. Se outra transação modificou o registro e incrementou a version entre a leitura e a escrita, o WHERE não encontra nenhuma linha e o update retorna 0 linhas afetadas. A aplicação detecta isso, re-le o dado com a versão mais recente e decide se repete a operação ou retorna conflito para o usuário. Em ORMs como Hibernate e Entity Framework, versionamento otimista e suportado nativamente com a annotation @Version.

Deadlock

Dois processos aguardam indefinidamente recursos que o outro possui.

Um deadlock e um ciclo de dependência entre transações. A transação T1 possui o lock do recurso A e espera pelo lock do recurso B. A transação T2 possui o lock do recurso B e espera pelo lock do recurso A. Nenhuma pode prosseguir. O banco detecta deadlocks via algoritmo de detecção de ciclo e mata a transação mais barata de refazer. A aplicação deve capturar o erro de deadlock e repetir a transação. Para prevenir deadlocks, a regra mais eficaz e garantir que todas as transações adquiram locks na mesma ordem. Se T1 e T2 sempre bloqueiam A antes de B, o ciclo nunca ocorre. Transações curtas também reduzem a janela de tempo em que locks são mantidos.

Operações atomicas no banco

UPDATE com condição e atômico por definição, evita race condition sem lock explícito.

Muitas race conditions podem ser resolvidas sem locks explicitos usando operações atomicas do banco. Por exemplo, para debitar saldo sem ir negativo: UPDATE contas SET saldo = saldo - 100 WHERE id = 1 AND saldo >= 100. Essa operação e atômica, a verificação e a modificação acontecem juntas no banco, sem janela para outro processo interferir. O número de linhas afetadas indica se a operação teve sucesso. Se retornou 0, o saldo era insuficiente. Essa abordagem e muito mais eficiente que SELECT + verificação + UPDATE separados. Para controle de estoque, reservas de ingressos e limitadores de taxa, esse padrão elimina a necessidade de locks sem comprometer a corretude.

Concorrência em APIs REST

Headers ETag e If-Match implementam lock otimista no nível do protocolo HTTP.

O protocolo HTTP oferece mecanismos nativos para lock otimista em APIs REST. O header ETag retorna um identificador do estado atual de um recurso. O header If-Match na requisição de modificação específica qual versão o cliente espera. Se o servidor verifica que a versão atual não corresponde ao If-Match, retorna 412 Precondition Failed. Isso e exatamente o padrão de lock otimista implementado no nível do protocolo, sem necessidade de infraestrutura adicional. O cliente recebe o 412, busca a versão mais recente e decide se repete a operação. Esse padrão e recomendado pela RFC 7232 e e usado em APIs como Google Calendar e Microsoft Graph.

Concorrência em sistemas de mensageria

Consumers concorrentes podem processar a mesma mensagem mais de uma vez.

Em sistemas baseados em mensageria como Kafka e RabbitMQ, múltiplos consumers podem processar mensagens em paralelo. Se um consumer falha após processar mas antes de confirmar, a mensagem pode ser reentregue a outro consumer. Isso e a semântica at-least-once: garante que a mensagem seja processada pelo menos uma vez, mas possivelmente mais. Para sistemas onde processar duas vezes causa problemas, como debitar duas vezes o mesmo pagamento, os consumers precisam ser idempotentes: detectar mensagens ja processadas e ignorar duplicatas. Isso geralmente e implementado com uma tabela de eventos processados com constraint UNIQUE no ID da mensagem.

Resumo final

Concorrência e inevitável em sistemas reais, a escolha e entre estrategias de controle.

Race conditions ocorrem quando operações concorrentes acessam dados compartilhados sem coordenação. Lock pessimista serializa o acesso e garante corretude ao custo de throughput. Lock otimista maximiza paralelismo ao custo de retries em caso de conflito. Versionamento otimista e a implementação mais prática do lock otimista. Operações atomicas no banco resolvem muitos casos sem precisar de locks. Deadlocks são resolvidos com ordem consistente de aquisição de locks e transações curtas. Em APIs, ETag e If-Match implementam lock otimista no nível HTTP. Em mensageria, consumers idempotentes protegem contra processamento duplicado. A estrategia certa depende da frequência de conflitos e do custo de cada abordagem no contexto específico.

Tutoriais em Video

Conceitos-chave

Race condition

Dois processos acessam e modificam o mesmo recurso simultaneamente, resultado depende de qual termina primeiro

Lock pessimista

Bloquear o recurso antes de modificar, garante exclusividade mas pode gerar deadlock e redução de throughput

Lock otimista

Tentar modificar sem bloquear, verificar conflito no commit, melhor quando conflitos são raros

Deadlock

Dois processos aguardam indefinidamente recursos um do outro, resolvido por timeout ou detecção de ciclo

Versionamento otimista

Adicionar campo version a cada registro, incrementar ao atualizar, rejeitar se version mudou

Operações atomicas

UPDATE com condição, ex: UPDATE SET saldo = saldo - 100 WHERE saldo >= 100, atômica por definição

Concorrência no Instagram

@bytebytego

Reels

@bytebytego

Facebook

Concorrência no X (Twitter)

@mjovanovictech

Software architecture patterns explained

Ver post completo no X →
@mjovanovictech

System design best practices

Ver post completo no X →
@mjovanovictech

Domain events and distributed systems

Ver post completo no X →
@mjovanovictech

Building resilient distributed systems

Ver post completo no X →
@mjovanovictech

Microservices vs monolith decisions

Ver post completo no X →
@mjovanovictech

Software design fundamentals

Ver post completo no X →

O que devs dizem

Felipe O. ★★★★★

Tinhamos um bug crítico de race condition no controle de estoque. Dois pedidos simultâneos passavam pela verificação e ambos eram aprovados com estoque negativo. Resolver com UPDATE WHERE quantidade > 0 foi simples e elegante.

Ana L. ★★★★★

Implementar versionamento otimista com o campo version no Entity Framework foi revelador. A maioria das operações nunca tem conflito, entao eliminar o lock pessimista melhorou drasticamente o throughput da API.

Bruno S. ★★★★☆

O padrão ETag e If-Match em APIs REST e subestimado. Quando você esta construindo uma API colaborativa onde vários usuários podem editar o mesmo recurso, isso e a solução mais limpa possível sem complicar o backend.