Cloud e plataforma

Replicação de Banco de Dados Multi-região: Estratégias para Aplicações Globais

Aplicações globais requerem proximidade de dados aos usuários mantendo consistência. Replicação de banco de dados multi-região habilita acesso de baixa latência através de geografias equilibrando requisitos de disponibilidade e consistência de dados.

13/03/20268 min de leituraCloud
Replicação de Banco de Dados Multi-região: Estratégias para Aplicações Globais

Resumo executivo

Aplicações globais requerem proximidade de dados aos usuários mantendo consistência. Replicação de banco de dados multi-região habilita acesso de baixa latência através de geografias equilibrando requisitos de disponibilidade e consistência de dados.

Ultima atualizacao: 13/03/2026

Introdução: O desafio de latência global

Aplicações modernas atendem usuários através de continentes, regiões e fusos horários. Usuários em São Paulo acessando um banco de dados em Frankfurt experimentam latência de 200ms+, enquanto usuários em San Francisco acessando um banco de dados em Tóquio enfrentam delays ainda maiores. Competição global significa que latência impacta diretamente experiência do usuário, engajamento e receita.

Replicação de banco de dados multi-região aborda esse desafio distribuindo dados mais próximos dos usuários mantendo o nível necessário de consistência. No entanto, replicação introduz complexidade: latência de rede entre regiões, resolução de conflitos para escritas concorrentes e overhead operacional para manter múltiplas instâncias de banco de dados.

A estratégia de replicação deve se alinhar com requisitos da aplicação: requisitos estritos de consistência ditam replicação síncrona, enquanto aplicações sensíveis à latência podem tolerar consistência eventual para melhor performance.

Topologias de replicação

Primary-replica (ativo-passivo)

Uma região primária trata todas as escritas, enquanto réplicas em outras regiões servem tráfego de leitura.

┌─────────────────────────────────────────────────────────────────┐
│                       Aplicação Global                    │
├─────────────────────────────────────────────────────────────────┤
│                                                            │
│  ┌─────────────┐      escritas     ┌─────────────┐      │
│  │    Região A │ ──────────────────> │   Primário   │      │
│  │  (Réplica)    │                    │   DB        │      │
│  └─────────────┘                    └─────────────┘      │
│        ▲ leituras                                    ▲     │
│        │                                              │     │
│        └──────────────────────────────────────────────────┘     │
│       leituras                                      escritas│
│                                                            │
│  ┌─────────────┐      leituras      ┌─────────────┐      │
│  │    Região B │ ◄───────────────── │   Réplica   │      │
│  │  (Réplica)    │                    │   (C)       │      │
│  └─────────────┘                    └─────────────┘      │
│                                                            │
│  ┌─────────────┐      leituras      ┌─────────────┐      │
│  │    Região C │ ◄───────────────── │   Réplica   │      │
│  │  (Réplica)    │                    │   (D)       │      │
│  └─────────────┘                    └─────────────┘      │
└─────────────────────────────────────────────────────────────────┘

Características:

  • Fonte única de verdade: Um primário elimina conflitos de escrita
  • Leituras rápidas: Réplica local fornece leituras de baixa latência
  • Latência de escrita: Todas as escritas vão para região primária
  • Lag de replicação: Leituras de réplicas podem estar desatualizadas
typescriptclass PrimaryReplicaRouter {
  private primaryConnection: DatabaseConnection;
  private replicaConnections: Map<string, DatabaseConnection> = new Map();
  private regionMapping: Map<string, string> = new Map();

  async routeRead(region: string, query: string, params: any[]): Promise<any> {
    const replica = this.replicaConnections.get(region);
    if (replica) {
      // Ler de réplica local
      return await replica.query(query, params);
    }

    // Fallback para primário se réplica local indisponível
    return await this.primaryConnection.query(query, params);
  }

  async routeWrite(query: string, params: any[]): Promise<any> {
    // Todas as escritas vão para primário
    return await this.primaryConnection.query(query, params);
  }

  getReadRegion(clientIP: string): string {
    // Determina região do cliente a partir de IP ou geolocalização
    return this.lookupRegion(clientIP);
  }
}

Multi-primary (ativo-ativo)

Múltiplas regiões podem aceitar escritas, requerendo resolução de conflitos quando os mesmos dados são modificados concorrentemente.

┌─────────────────────────────────────────────────────────────────┐
│                       Aplicação Global                    │
├─────────────────────────────────────────────────────────────────┤
│                                                            │
│  ┌─────────────┐  ◄─────────────────► ┌─────────────┐     │
│  │  Primário A  │       conflito        │  Primário B  │     │
│  │     DB       │ ◄─────────────────► │     DB       │     │
│  └─────────────┘        resolução     └─────────────┘     │
│        ▲ ▲                                         ▲ ▲     │
│        │ │                                         │ │     │
│        │ └─────────────────────────────────────────┘ │     │
│        │                replicação                 │     │
│        │ leituras/escritas                              │     │
│        │                                              │     │
│  ┌─────────────┐  ◄─────────────────► ┌─────────────┐     │
│  │  Primário C  │       conflito        │  Primário D  │     │
│  │     DB       │ ◄─────────────────► │     DB       │     │
│  └─────────────┘        resolução     └─────────────┘     │
│        ▲ ▲                                         ▲ ▲     │
│        │ │                                         │ │     │
│        │ └─────────────────────────────────────────┘ │     │
│        │                replicação                 │     │
│        │ leituras/escritas                                │     │
└───────────────────────────────────────────────────────────────┘

Características:

  • Baixa latência para escritas: Escritas vão para primário local
  • Complexidade de conflito: Escritas concorrentes requerem resolução
  • Consistência eventual: Leituras podem retornar dados desatualizados temporariamente
  • Complexidade operacional: Múltiplos primários para manter

Estratégias de replicação

Replicação síncrona

Réplica confirma escrita antes de confirmar ao cliente, garantindo consistência forte.

typescriptclass SynchronousReplication {
  private primaryConnection: DatabaseConnection;
  private replicaConnections: DatabaseConnection[];

  async write(query: string, params: any[]): Promise<any> {
    const transaction = await this.primaryConnection.beginTransaction();

    try {
      // Executa no primário
      const result = await this.primaryConnection.execute(query, params);

      // Replica para todas as réplicas síncronamente
      await Promise.all(
        this.replicaConnections.map(replica =>
          replica.execute(query, params)
        )
      );

      await transaction.commit();
      return result;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
}

Trade-offs:

AspectoReplicação Síncrona
ConsistênciaForte
Latência de escritaAlta (espera por todas as réplicas)
DisponibilidadeBaixa (qualquer falha de réplica bloqueia escritas)
Complexidade de implementaçãoAlta
Melhor paraDados financeiros, contas de usuário, estado crítico

Replicação assíncrona

Primário confirma escrita imediatamente, replicação acontece em background.

typescriptclass AsynchronousReplication {
  private primaryConnection: DatabaseConnection;
  private replicaConnections: DatabaseConnection[];
  private replicationQueue: Queue<ReplicationTask>;

  constructor() {
    this.replicationQueue = new Queue<ReplicationTask>();
    this.startReplicationWorker();
  }

  async write(query: string, params: any[]): Promise<any> {
    // Executa no primário e confirma imediatamente
    const result = await this.primaryConnection.execute(query, params);

    // Enfileira tarefa de replicação
    this.replicationQueue.add({
      query,
      params,
      timestamp: Date.now()
    });

    return result;
  }

  private startReplicationWorker(): void {
    setInterval(async () => {
      const task = this.replicationQueue.dequeue();
      if (task) {
        await this.replicateToAll(task);
      }
    }, 100); // Processa a cada 100ms
  }

  private async replicateToAll(task: ReplicationTask): Promise<void> {
    await Promise.all(
      this.replicaConnections.map(replica =>
        this.replicateWithRetry(replica, task)
      )
    );
  }

  private async replicateWithRetry(
    replica: DatabaseConnection,
    task: ReplicationTask
  ): Promise<void> {
    const maxRetries = 3;
    let attempt = 0;

    while (attempt < maxRetries) {
      try {
        await replica.execute(task.query, task.params);
        return;
      } catch (error) {
        attempt++;
        if (attempt >= maxRetries) {
          console.error(`Replicação falhou após ${maxRetries} tentativas:`, error);
        } else {
          await this.delay(1000 * attempt); // Backoff exponencial
        }
      }
    }
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Trade-offs:

AspectoReplicação Assíncrona
ConsistênciaEventual
Latência de escritaBaixa (confirmação imediata)
DisponibilidadeAlta (falhas de réplica não bloqueiam escritas)
Complexidade de implementaçãoMédia
Melhor paraConteúdo, feeds sociais, analytics, dados não críticos

Estratégias de resolução de conflitos

Last-write-wins (baseado em timestamp):

typescriptinterface TimestampedRecord {
  id: string;
  value: any;
  updatedAt: Date;
  region: string;
}

async function resolveConflict(
  recordA: TimestampedRecord,
  recordB: TimestampedRecord
): Promise<TimestampedRecord> {
  // Resolução simples baseada em timestamp
  return recordA.updatedAt > recordB.updatedAt ? recordA : recordB;
}

// Uso em configuração multi-primary
class ConflictAwareRepository {
  async upsert(record: TimestampedRecord): Promise<void> {
    const currentRecord = await this.findById(record.id);

    if (!currentRecord) {
      // Novo registro, conflito não possível
      return await this.insert(record);
    }

    // Verifica conflito
    const conflict = currentRecord.region !== record.region &&
                      currentRecord.updatedAt > record.updatedAt;

    if (conflict) {
      // Modificação concorrente detectada
      const resolved = await this.resolveConflict(currentRecord, record);
      return await this.update(resolved);
    }

    return await this.update(record);
  }

  private async resolveConflict(
    existing: TimestampedRecord,
    incoming: TimestampedRecord
  ): Promise<TimestampedRecord> {
    // Lógica de negócio para resolução de conflito
    return await mergeRecords(existing, incoming);
  }
}

CRDT (Tipos de Dados Replicadas Livres de Conflito):

typescript// Exemplo: Register Last-Write-Wins (LWW)
class LWWRegister<T> {
  private value: T;
  private timestamp: number;
  private replicaId: string;

  constructor(initialValue: T, replicaId: string) {
    this.value = initialValue;
    this.timestamp = Date.now();
    this.replicaId = replicaId;
  }

  set(newValue: T, replicaId: string): void {
    const now = Date.now();

    // Aceita escrita se timestamp é mais novo ou replica ID é maior
    if (now > this.timestamp || replicaId > this.replicaId) {
      this.value = newValue;
      this.timestamp = now;
      this.replicaId = replicaId;
    }
  }

  get(): T {
    return this.value;
  }

  merge(other: LWWRegister<T>): LWWRegister<T> {
    const winner = this.timestamp >= other.timestamp ? this : other;
    return new LWWRegister(winner.value, winner.replicaId);
  }
}

Estratégias de otimização de latência

Consistência read-your-writes

Garanta que usuários vejam suas próprias escritas imediatamente:

typescriptclass ReadYourWritesRepository {
  private primaryConnection: DatabaseConnection;
  private localReplica: DatabaseConnection;

  async read(userId: string, query: string, params: any[]): Promise<any> {
    // Verifica se usuário escreveu recentemente
    const recentWrite = await this.getRecentWrite(userId);

    if (recentWrite && recentWrite.isPending) {
      // Ler do primário para ver próprias escritas
      return await this.primaryConnection.query(query, params);
    }

    // Ler de réplica local
    return await this.localReplica.query(query, params);
  }

  async write(userId: string, query: string, params: any[]): Promise<any> {
    const result = await this.primaryConnection.query(query, params);

    // Marca usuário como tendo escrita recente
    await this.markRecentWrite(userId);

    // Após lag de replicação, limpa a flag
    setTimeout(() => this.clearRecentWrite(userId), 5000);

    return result;
  }

  private async getRecentWrite(userId: string): Promise<{isPending: boolean} | null> {
    // Implementação para verificar escritas recentes
    return null;
  }

  private async markRecentWrite(userId: string): Promise<void> {
    // Implementação para marcar usuário como tendo escrita recente
  }

  private async clearRecentWrite(userId: string): Promise<void> {
    // Implementação para limpar flag de escrita recente
  }
}

Caching no cliente

Cache dados acessados frequentemente para reduzir latência de banco de dados:

typescriptclass CachedRepository<T> {
  private cache: Map<string, {data: T, expiry: number}> = new Map();
  private repository: Repository<T>;
  private ttl: number;

  constructor(repository: Repository<T>, ttl: number = 60000) {
    this.repository = repository;
    this.ttl = ttl;
  }

  async get(id: string): Promise<T | null> {
    // Verifica cache primeiro
    const cached = this.cache.get(id);

    if (cached && cached.expiry > Date.now()) {
      return cached.data;
    }

    // Cache miss, busca do banco de dados
    const data = await this.repository.findById(id);

    if (data) {
      // Atualiza cache
      this.cache.set(id, {
        data,
        expiry: Date.now() + this.ttl
      });
    }

    return data;
  }

  async update(id: string, data: T): Promise<void> {
    // Atualiza banco de dados
    await this.repository.update(id, data);

    // Invalida cache
    this.cache.delete(id);
  }

  // Cleanup de entradas de cache expiradas
  private startCleanup(): void {
    setInterval(() => {
      const now = Date.now();

      for (const [id, entry] of this.cache.entries()) {
        if (entry.expiry < now) {
          this.cache.delete(id);
        }
      }
    }, 60000); // Cleanup a cada minuto
  }
}

Failover e recuperação de desastre

Failover automático

typescriptclass FailoverManager {
  private primaryConnection: DatabaseConnection;
  private replicaConnections: Map<string, DatabaseConnection>;
  private currentPrimary: string;
  private healthCheckInterval: number = 5000;

  constructor() {
    this.primaryConnection = this.connectTo('primario');
    this.replicaConnections = new Map([
      ['regiao-a', this.connectTo('replica-a')],
      ['regiao-b', this.connectTo('replica-b')],
      ['regiao-c', this.connectTo('replica-c')],
    ]);

    this.startHealthChecks();
  }

  private startHealthChecks(): void {
    setInterval(async () => {
      await this.checkPrimaryHealth();
      await this.checkReplicaHealth();
    }, this.healthCheckInterval);
  }

  private async checkPrimaryHealth(): Promise<void> {
    const isHealthy = await this.checkHealth(this.primaryConnection);

    if (!isHealthy) {
      console.warn('Primário não saudável, iniciando failover');
      await this.failoverToReplica();
    }
  }

  private async checkHealth(connection: DatabaseConnection): Promise<boolean> {
    try {
      await connection.execute('SELECT 1');
      return true;
    } catch (error) {
      console.error('Health check falhou:', error);
      return false;
    }
  }

  private async failoverToReplica(): Promise<void> {
    // Encontra réplica saudável com dados mais atualizados
    const healthyReplica = await this.findHealthyReplica();

    if (!healthyReplica) {
      throw new Error('Nenhuma réplica saudável disponível para failover');
    }

    // Promove réplica para primário
    this.currentPrimary = healthyReplica.region;
    this.primaryConnection = this.replicaConnections.get(healthyReplica.region)!;

    console.log(`Failed over para ${healthyReplica.region}`);

    // Notifica outras réplicas do novo primário
    await this.notifyPrimaryChange(healthyReplica.region);
  }

  private async findHealthyReplica(): Promise<{region: string, connection: DatabaseConnection} | null> {
    for (const [region, connection] of this.replicaConnections.entries()) {
      if (await this.checkHealth(connection)) {
        return { region, connection };
      }
    }
    return null;
  }
}

Recuperação de dados após partição

typescriptclass PartitionRecovery {
  async recoverAfterPartition(): Promise<void> {
    // 1. Identifica escritas que aconteceram durante partição
    const divergentWrites = await this.identifyDivergentWrites();

    // 2. Merge de escritas divergentes
    for (const write of divergentWrites) {
      await this.mergeDivergentWrite(write);
    }

    // 3. Resolve conflitos
    await this.resolveConflicts();

    // 4. Restabelece replicação
    await this.reconfigureReplication();
  }

  private async identifyDivergentWrites(): Promise<DivergentWrite[]> {
    // Compara dados através de regiões para encontrar diferenças
    // Isso é específico de implementação
    return [];
  }

  private async mergeDivergentWrite(write: DivergentWrite): Promise<void> {
    // Merge de escritas divergentes usando estratégia configurada
    // (last-write-wins, merge de lógica de negócio, etc.)
  }

  private async resolveConflicts(): Promise<void> {
    // Resolve quaisquer conflitos restantes após merge
  }

  private async reconfigureReplication(): Promise<void> {
    // Restabelece topologia de replicação após partição curar
  }
}

Considerações operacionais

Monitoramento de bancos de dados multi-região

typescriptclass MultiRegionMetrics {
  private metrics: Map<string, RegionMetrics> = new Map();

  recordQuery(region: string, latency: number, success: boolean): void {
    const regionMetrics = this.metrics.get(region) || {
      queries: 0,
      totalLatency: 0,
      failures: 0,
      lastUpdate: Date.now()
    };

    regionMetrics.queries++;
    regionMetrics.totalLatency += latency;
    if (!success) {
      regionMetrics.failures++;
    }

    this.metrics.set(region, {
      ...regionMetrics,
      lastUpdate: Date.now()
    });
  }

  getRegionMetrics(region: string): RegionMetricsSnapshot {
    const metrics = this.metrics.get(region);

    if (!metrics) {
      return {
        region,
        averageLatency: 0,
        successRate: 0,
        queriesPerSecond: 0
      };
    }

    const averageLatency = metrics.totalLatency / metrics.queries;
    const successRate = (metrics.queries - metrics.failures) / metrics.queries;
    const timeSinceUpdate = Date.now() - metrics.lastUpdate;

    return {
      region,
      averageLatency,
      successRate,
      queriesPerSecond: timeSinceUpdate > 0 ? metrics.queries / (timeSinceUpdate / 1000) : 0
    };
  }

  getGlobalMetrics(): GlobalMetricsSnapshot {
    const regionSnapshots = Array.from(this.metrics.keys())
      .map(region => this.getRegionMetrics(region));

    const totalQueries = regionSnapshots.reduce((sum, r) => sum + r.queriesPerSecond, 0);
    const averageLatency = regionSnapshots.reduce((sum, r) => sum + r.averageLatency, 0) / regionSnapshots.length;
    const lowestLatency = regionSnapshots.reduce((min, r) => r.averageLatency < min.averageLatency ? r : min, regionSnapshots[0]);
    const highestLatency = regionSnapshots.reduce((max, r) => r.averageLatency > max.averageLatency ? r : max, regionSnapshots[0]);

    return {
      totalQueries,
      averageLatency,
      lowestLatencyRegion: lowestLatency.region,
      highestLatencyRegion: highestLatency.region,
      regions: regionSnapshots
    };
  }
}

Otimização de custos

typescriptclass CostOptimizedReplication {
  private primaryRegion: string;
  private secondaryRegions: string[];
  private trafficPatterns: Map<string, TrafficPattern> = new Map();

  async optimizeRouting(): Promise<void> {
    const patterns = await this.analyzeTrafficPatterns();

    for (const [region, pattern] of patterns.entries()) {
      if (pattern.writeRatio > 0.5) {
        // Alta razão de escrita, considera promover para primário
        await this.considerPrimaryPromotion(region);
      } else if (pattern.readRatio > 0.9) {
        // Alta razão de leitura, considera adicionar réplica
        await this.considerReplicaAddition(region);
      }
    }
  }

  private async analyzeTrafficPatterns(): Promise<Map<string, TrafficPattern>> {
    // Analisa padrões de leitura/escrita por região
    // Isso é específico de implementação
    return new Map();
  }

  private async considerPrimaryPromotion(region: string): Promise<void> {
    // Avalia se região deve se tornar um primário
    // Considera: volume de escrita, requisitos de latência, custo
  }

  private async considerReplicaAddition(region: string): Promise<void> {
    // Avalia se região deve receber uma réplica
    // Considera: volume de leitura, melhoria de latência, custo
  }
}

Framework de decisão

Escolha estratégia de replicação

RequisitoSíncronoAssíncrono
Requisito de consistênciaConsistência forteConsistência eventual
Tolerância de latência de escritaAlta (100-200ms+)Baixa (<50ms)
Volume de escritaBaixo a médioMédio a alto
Tolerância a conflitoBaixa (fonte única de verdade)Média (resolução necessária)
Tolerância de custoMaior (mais banda)Menor
Casos de usoFinanceiro, contas, inventárioSocial, analytics, conteúdo

Escolha topologia

CenárioTopologia RecomendadaRacional
Leitura intensiva, escrita levePrimary-replicaOtimiza latência de leitura
Alto volume de escritaMulti-primaryDistribui carga de escrita
Consistência estrita necessáriaPrimary-replicaFonte única de verdade
Base de usuários globalMulti-primaryBaixa latência em todo lugar
Sensível a custoPrimary-replicaCustos de banda menores

Conclusão

Replicação de banco de dados multi-região habilita aplicações globais a fornecer acesso de baixa latência para usuários em todo o mundo mantendo níveis apropriados de consistência. A estratégia ótima depende de requisitos específicos: necessidades de consistência, tolerância a latência, volume de escrita e capacidades operacionais.

Replicação síncrona fornece consistência forte ao custo de latência de escrita e disponibilidade. Replicação assíncrona oferece baixa latência de escrita e alta disponibilidade com consistência eventual. Topologias multi-primary distribuem carga de escrita mas requerem resolução de conflitos.

Monitore performance regional continuamente, failover automaticamente quando problemas são detectados e otimize roteamento baseado em padrões de tráfego observados. O objetivo não é pureza arquitetural—construir um sistema que fornece excelente experiência ao usuário permanecendo gerenciável operacionalmente.

Pergunta prática de fechamento: Qual é o requisito primário de consistência para sua aplicação, e qual estratégia de replicação melhor equilibra esse requisito com necessidades de latência e disponibilidade?


Construindo uma aplicação global e precisa de orientação especializada sobre arquitetura de banco de dados multi-região? Fale com especialistas em banco de dados da Imperialis sobre projetar uma estratégia de replicação que entrega acesso de baixa latência atendendo seus requisitos de consistência.

Fontes

Leituras relacionadas