Replicação e Consistência
Uma fã paga dois ingressos às 10:00:41. A tela de confirmação diz "Pedido 981 confirmado". Ela toca em Meus ingressos — e a lista está vazia. Em pânico, ela compra de novo. Nada se perdeu e não houve bug no sentido usual: o pagamento foi gravado no primário do PostgreSQL, enquanto Meus ingressos foi servido por uma réplica de leitura que ainda não tinha recebido a escrita. O BoxOffice tinha escolhido, sem perceber, um modelo de consistência — e era o modelo errado para aquela tela.
Esta página explica como os dados são copiados entre nós (replicação) e o que um leitor pode ver enquanto as cópias discordam (modelos de consistência). Os dois são inseparáveis: o projeto da replicação decide quais modelos de consistência são baratos, quais são caros e quais são impossíveis.
Por que replicar?
Manter uma cópia do mesmo dado em vários nós atende a três objetivos diferentes, e vale dizer qual deles você quer:
| Objetivo | Como a replicação ajuda | BoxOffice |
|---|---|---|
| Disponibilidade / durabilidade | Se um nó morre, outro tem os dados | O banco do inventory sobrevive à perda de uma máquina ou de uma zona |
| Vazão de leitura | As leituras se espalham por muitas cópias | O catálogo de eventos é lido ~100× mais do que escrito (proporção ilustrativa) |
| Latência | Uma cópia perto do usuário responde mais rápido | Uma cópia do catálogo em uma região europeia para fãs em Lisboa |
Os três são fáceis se o dado nunca muda. A dificuldade está inteira em lidar com mudanças: cada escrita precisa chegar a todas as cópias e, enquanto viaja, as cópias discordam.
Modelos de replicação
Há três famílias, que se distinguem por quem aceita escritas1.
Com um único líder (primário–réplica)
Um nó, o líder (primário), aceita todas as escritas e envia um log de mudanças aos seguidores (réplicas). As leituras podem ir ao líder ou aos seguidores. É o padrão no PostgreSQL, MySQL, MongoDB, Redis e Kafka (um líder por partição).
flowchart LR
w([order: escrita]) -->|"INSERT pedido 981"| L[("líder<br/>(primário)")]
L -->|"stream do WAL"| F1[("seguidor 1")]
L -->|"stream do WAL"| F2[("seguidor 2")]
r1([Meus ingressos: leitura]) -->|"SELECT"| F1
r2([catálogo: leitura]) -->|"SELECT"| F2 Com vários líderes (multi-leader)
Vários nós aceitam escritas — tipicamente um líder por data center ou região — e replicam entre si de forma assíncrona. As escritas são rápidas e locais em todo lugar, mas dois líderes podem aceitar escritas conflitantes no mesmo registro, que precisam ser resolvidas depois. Usos típicos: implantações multirregião, apps offline-first (cada celular é um "líder"), edição colaborativa.
Sem líder (leaderless)
Qualquer réplica aceita leituras e escritas. O cliente (ou um coordenador) envia cada escrita a várias réplicas e cada leitura a várias réplicas, e quóruns decidem o que conta como sucesso (R + W > N, veja Consenso e Quóruns). É o projeto do artigo Dynamo, da Amazon2, do Cassandra e do ScyllaDB.
| Um líder | Vários líderes | Sem líder | |
|---|---|---|---|
| Quem aceita escritas | Um líder | Um líder por região/site | Qualquer réplica |
| Conflitos de escrita | Nenhum (o líder ordena as escritas) | Sim — precisam ser resolvidos | Sim — precisam ser resolvidos |
| Latência de escrita | Ida e volta até o líder (talvez distante) | Local | Ida e volta até W réplicas |
| Falha do líder | Precisa de failover (segundos) | Os outros líderes continuam | Não há líder a perder |
| Consistência forte possível? | Sim (leitura no líder, replicação síncrona) | Não entre líderes | Só aproximadamente (quóruns) |
| Sistemas típicos | PostgreSQL, MySQL, MongoDB, partições do Kafka | Montagens multirregião de MySQL/Postgres, CouchDB, Redis Active-Active | Cassandra, ScyllaDB, Riak, DynamoDB (internamente) |
| Uso no BoxOffice | inventory, order, payment | — (evite para assentos!) | Atividade dos fãs, contadores de visualização |
Síncrona ou assíncrona?
Independentemente de quem lidera, o líder precisa decidir quando dizer "OK" ao cliente:
sequenceDiagram
participant C as order
participant L as líder
participant F1 as seguidor 1 (síncrono)
participant F2 as seguidor 2 (assíncrono)
C->>L: INSERT pedido 981
L->>F1: replica
L->>F2: replica
F1-->>L: gravado em disco
L-->>C: OK (após 1 seguidor síncrono)
Note over F2: ainda aplicando...<br/>uma leitura aqui está desatualizada
F2-->>L: gravado (mais tarde) | Modo | O cliente recebe OK quando… | Se o líder morre logo após o OK | Latência de escrita | Disponibilidade de escrita |
|---|---|---|---|---|
| Assíncrono | o líder gravou localmente | Escritas recentes podem ser perdidas | A menor | Alta: seguidores podem estar fora |
| Síncrono (todos) | todos os seguidores gravaram | Nada se perde | O seguidor mais lento decide | Baixa: qualquer seguidor fora bloqueia as escritas |
| Semissíncrono | o líder + k seguidores gravaram | Nada se perde se um seguidor síncrono for promovido | Uma ida e volta a mais | Boa: tolera outros seguidores fora |
| Quórum (consenso) | uma maioria gravou | Nada se perde | Mediana da maioria mais rápida | Tolera uma minoria fora |
O PostgreSQL expõe essa escolha em synchronous_commit e synchronous_standby_names (por exemplo ANY 1 (replica_a, replica_b) — semissíncrono com uma de duas). Na prática, "totalmente síncrono" é raro: um seguidor lento travaria toda escrita. Replicação semissíncrona ou por quórum é o meio-termo usual.
Atraso de replicação
Com seguidores assíncronos, a demora entre uma escrita no líder e sua visibilidade em um seguidor é o atraso de replicação (replication lag). Em um sistema saudável costuma ser de milissegundos; sob carga pesada de escrita, durante uma transação longa ou depois de um soluço na rede, pode chegar a segundos ou minutos. O tempo entre uma escrita e a leitura seguinte decide se essa leitura está desatualizada:
Com um atraso (suposto) exponencial, a mesma tela que quase nunca está desatualizada em um dia tranquilo fica desatualizada para cerca de 69% das fãs que a abrem 300 ms depois de pagar durante a abertura das vendas, quando o atraso médio sobe para 800 ms. O bug da história de abertura não é raro sob carga — é o comportamento esperado.
Experimente. Carregue o cenário db-replicas, rode o perfil load e observe a métrica de atraso da réplica. Depois aumente a taxa de escrita (ou mude para spike) e veja o atraso — e a fração de leituras desatualizadas — crescer. Por fim, mande as leituras para o primário e compare a CPU e o p99 do primário.
Modelos de consistência: um contrato
Um modelo de consistência é um contrato entre um armazenamento de dados e quem programa contra ele: se você seguir estas regras, os resultados das leituras serão previsíveis desta forma3. Modelos mais fortes são mais fáceis de programar e custam mais coordenação (latência, disponibilidade); modelos mais fracos são mais baratos e empurram a complexidade para a aplicação.
flowchart TD
LIN["Linearizável<br/>(consistência forte)"] --> SEQ["Sequencial"]
SEQ --> CAU["Causal"]
CAU --> RYW["Read-your-writes"]
CAU --> MR["Leituras monotônicas"]
CAU --> CP["Prefixo consistente"]
RYW --> EV["Eventual"]
MR --> EV
CP --> EV Uma seta de A para B significa "A garante tudo o que B garante, e mais". Read-your-writes e leituras monotônicas são garantias de sessão: valem para a sessão de um cliente, não para todos; o prefixo consistente (consistent prefix) promete que nenhum leitor vê escritas fora da ordem em que aconteceram.
Linearizabilidade (consistência forte)
A linearizabilidade faz um sistema replicado se comportar como se houvesse uma única cópia dos dados, e cada operação tivesse efeito atômico em um instante entre seu início e seu fim4. Uma vez que alguma leitura devolveu um valor novo, toda leitura posterior (de qualquer pessoa, em qualquer réplica) precisa devolver esse valor ou um mais novo.
sequenceDiagram
participant A as Fã A
participant N1 as réplica 1
participant N2 as réplica 2
participant B as Fã B
A->>N1: reservar assento A-12
N1-->>A: OK (reserva confirmada)
Note over A,B: a escrita terminou
B->>N2: o assento A-12 está livre?
N2-->>B: sim, FREE (desatualizado)
Note over N2,B: viola a linearizabilidade:<br/>B começou depois que a escrita de A terminou
B->>N2: reservar assento A-12
Note over N2: reserva em dobro se N2 aceitar A linearizabilidade é o que você precisa quando a correção depende de uma resposta atualizada:
- unicidade e estoque — um assento, um nome de usuário, a última unidade em estoque;
- eleição de líder e locks — só um nó pode acreditar que é o líder;
- tempo entre canais — um fã paga e o serviço
notification(lendo em outro lugar) precisa ver o pedido pago.
Ela é obtida roteando todas as leituras e escritas de uma chave por um líder, que por sua vez é eleito por consenso (com cuidado com líderes desatualizados), ou por consenso em cada operação. O preço: toda operação paga pelo menos uma ida e volta até o líder ou até uma maioria, e nós que não alcançam o líder/a maioria precisam recusar — a escolha entre C e A do CAP.
Serializável não é linearizável
Serializabilidade é uma garantia de isolamento para transações sobre muitos objetos (o resultado equivale a alguma ordem serial das transações). Linearizabilidade é uma garantia de atualidade para objetos individuais (leituras veem a última escrita em tempo real). Um banco pode oferecer uma, as duas ("serializabilidade estrita", como no Spanner) ou nenhuma. Os níveis de isolamento estão em Bancos de dados — transações.
Consistência sequencial
Todos os nós veem todas as operações na mesma ordem, e as operações de cada nó aparecem na ordem do seu programa — mas essa ordem não precisa respeitar o tempo real. Uma leitura pode devolver um valor "antigo" em termos de relógio, desde que todos concordem com a sequência. É sobretudo um conceito de memória de hardware e de teoria; em system design raramente você o escolhe explicitamente.
Consistência causal
Operações causalmente relacionadas — uma poderia ter influenciado a outra — são vistas por todos nessa ordem; operações concorrentes podem ser vistas em ordens diferentes por nós diferentes.
O exemplo clássico é uma conversa: se Ana pergunta "A venda das 10:00 atrasou?" e o organizador responde "Não, começa no horário", ninguém pode ver a resposta sem a pergunta. No BoxOffice: um evento SeatReleased causado por um evento HoldExpired não pode ser aplicado antes dele.
A consistência causal é interessante porque é o modelo mais forte que continua disponível durante uma partição de rede5: os nós podem continuar aceitando leituras e escritas dos dois lados e ainda respeitar a causalidade. Ela é rastreada com relógios lógicos — veja Tempo e Relógios.
Garantias de sessão
A maioria dos bugs do tipo "meu dado sumiu" é a violação de uma destas quatro garantias, definidas para a sessão de um único cliente (Terry et al., projeto Bayou)6:
| Garantia | Promessa | Violação no BoxOffice |
|---|---|---|
| Read-your-writes (ler as próprias escritas) | Depois que você escreve, suas leituras veem essa escrita | A história de abertura: você pagou, Meus ingressos está vazio |
| Leituras monotônicas | Você nunca vê o dado voltar no tempo | Assentos restantes mostra 120, depois 340, depois 95, conforme você cai em réplicas diferentes |
| Escritas monotônicas | Suas escritas são aplicadas na ordem em que você as fez | "Trocar e-mail" e depois "enviar ingresso" — o ingresso vai para o endereço antigo |
| Escritas seguem leituras (causal) | Uma escrita que você faz depois de ler X é ordenada depois de X | Você responde a uma mensagem do suporte que outras réplicas ainda não receberam |
sequenceDiagram
participant F as App do fã
participant R1 as réplica 1 (atraso 10 ms)
participant R2 as réplica 2 (atraso 2 s)
F->>R1: GET /events/42/availability
R1-->>F: restam 95 assentos
F->>R2: atualizar (o load balancer escolhe R2)
R2-->>F: restam 340 assentos
Note over F: a contagem voltou no tempo:<br/>leituras monotônicas violadas Como oferecê-las em cima de réplicas assíncronas:
- Ler do líder depois de uma escrita: por uma janela curta (por exemplo, 1–5 s) depois que a fã escreve, roteie as leituras dela para o primário — ou sempre leia do primário os dados que pertencem ao usuário (Meus ingressos).
- Tokens de versão: a escrita devolve a posição no log do líder (no PostgreSQL, o LSN do WAL — Log Sequence Number); o cliente o envia na próxima leitura, e uma réplica só responde depois de ter aplicado pelo menos aquela posição (senão espera ou encaminha ao líder). Muitos bancos expõem isso como sessões causais ou tokens de read-after-write (sessões de consistência causal do MongoDB, consistência de sessão do Cosmos DB).
- Réplica fixa (sticky): prenda cada sessão a uma réplica (hash do id da sessão) — dá leituras monotônicas, não read-your-writes.
No código do BoxOffice: read-your-writes pelo roteamento (lab 6)
O Hands-on 6 — Réplicas acrescenta uma réplica por streaming ao banco do inventory e usa a primeira técnica acima. Um DataSource envolve dois pools; uma transação marcada @Transactional(readOnly = true) — o mapa de assentos, a disponibilidade por setor — recebe uma conexão da réplica, todas as outras recebem o primário. O HoldService.find não é somente leitura de propósito: o fã acabou de criar aquela reserva, então ela é lida do primário. O laboratório mediu por quê: um fã que reserva um assento e recarrega o mapa de assentos na hora, a partir da réplica, não viu a própria reserva em cerca de 6% das tentativas com a réplica na mesma máquina, e em 98% com 1 s de atraso artificial.
/**
* Two connection pools — primary and replica — behind one DataSource.
* A transaction marked @Transactional(readOnly = true) gets a replica connection;
* every other transaction gets a primary connection. The choice happens when the
* transaction runs its first statement (that is what "lazy" means here).
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("inventory.datasource.primary")
HikariDataSource primaryDataSource() {
return new HikariDataSource();
}
@Bean
@ConfigurationProperties("inventory.datasource.replica")
HikariDataSource replicaDataSource() {
return new HikariDataSource();
}
@Bean
@Primary
DataSource dataSource(HikariDataSource primaryDataSource, HikariDataSource replicaDataSource) {
var routing = new LazyConnectionDataSourceProxy(primaryDataSource);
routing.setReadOnlyDataSource(replicaDataSource); // Spring Framework 6.2+
return routing;
}
Consistência eventual
A consistência eventual só promete que, se as escritas pararem, todas as réplicas vão eventualmente convergir para o mesmo valor. Não diz quando, e não diz nada sobre o que você lê enquanto isso. É a garantia do DNS, da maioria dos caches e CDNs, das réplicas assíncronas e dos armazenamentos sem líder em configurações de quórum baixas.
Ela serve perfeitamente para muitos domínios — a descrição do evento, a imagem do cartaz, o contador "312 pessoas estão vendo este evento". A parte difícil é o que acontece quando réplicas aceitaram escritas concorrentes no mesmo item e precisam convergir:
| Estratégia | Como | Trade-off |
|---|---|---|
| Last-write-wins (LWW) | Fica a escrita com o maior timestamp | Simples; descarta silenciosamente escritas concorrentes e depende de relógios que derivam (Tempo e Relógios) |
| Vetores de versão / irmãos | Detecta versões concorrentes, guarda todas, deixa a aplicação mesclar | Nenhuma perda silenciosa; a aplicação precisa escrever a lógica de merge (o carrinho do Dynamo mesclava por união) |
| CRDTs | Tipos de dados cujo merge é comutativo, associativo e idempotente | Automático, convergência demonstrável; só para dados que cabem em um CRDT (contadores, conjuntos, registradores, texto) |
| Evitar conflitos | Rotear todas as escritas de uma chave para um líder (uma região "de origem") | Nenhum conflito; as escritas dessa chave pagam a latência até a origem |
Um CRDT (Conflict-free Replicated Data Type, tipo de dado replicado livre de conflitos)7 é projetado para que as réplicas possam trocar estado em qualquer ordem, quantas vezes for, e ainda terminar iguais. O mais simples é o contador que só cresce: cada réplica incrementa apenas a sua posição, e o merge pega o máximo elemento a elemento.
// Grow-only counter CRDT: e.g. "fans interested in event 42", counted in 3 regions
class GCounter {
private final long[] counts; // one slot per replica
private final int me; // this replica's index
GCounter(int replicas, int me) { this.counts = new long[replicas]; this.me = me; }
void increment() { counts[me]++; }
long value() { return java.util.Arrays.stream(counts).sum(); }
// merge = element-wise max: commutative, associative, idempotent
void merge(GCounter other) {
for (int i = 0; i < counts.length; i++)
counts[i] = Math.max(counts[i], other.counts[i]);
}
}
Se São Paulo contou [3, 0, 0] e Lisboa [0, 2, 0], ambas mesclam para [3, 2, 0] e informam 5 — nenhum incremento perdido, nenhuma coordenação necessária. Um único número compartilhado com LWW informaria 3 ou 2.
O espectro em um relance
| Modelo | Garantia | Custo de coordenação | Disponível durante uma partição? | Implementação típica |
|---|---|---|---|---|
| Linearizável | Cópia única, atualidade em tempo real | O maior: ida e volta ao líder ou à maioria por operação | Não (o lado minoritário precisa recusar) | Armazenamentos baseados em Raft/Paxos (etcd, Spanner, CockroachDB), um líder com leituras no líder |
| Sequencial | Uma ordem global para todos | Alto | Não | Sobretudo teoria / modelos de memória de hardware |
| Causal | Causa antes do efeito, para todos | Médio: rastrear dependências | Sim | Relógios lógicos/vetoriais, sessões causais |
| Read-your-writes, leituras monotônicas | Sanidade por sessão | Baixo: rotear ou esperar por sessão | Sim, se a sessão ficar do seu lado | Leituras no líder após escrita, tokens de LSN, réplicas fixas |
| Eventual | Converge se as escritas pararem | O menor | Sim | Replicação assíncrona, caches, DNS, sem líder em ONE |
BoxOffice: um modelo de consistência por domínio de dados
| Dado | Modelo | Por quê | Como |
|---|---|---|---|
| Estado dos assentos e reservas | Linearizável | Dois fãs nunca podem reservar o mesmo assento | Um único primário PostgreSQL por evento; UPDATE condicional/SELECT … FOR UPDATE |
| Pagamentos | Linearizável + idempotente | Uma cobrança em dobro é inaceitável | Só no primário; chave de idempotência repassada ao provedor |
| Meus ingressos, Meus pedidos | Read-your-writes | A fã precisa ver o que acabou de comprar | Ler do primário os dados da própria fã, ou token de LSN |
| Contador de assentos restantes na página do evento | Leituras monotônicas (eventual é tolerável) | Pode atrasar, mas não pode voltar para trás | Réplica fixa ou cache com TTL curto |
| Catálogo de eventos, imagens, descrições | Eventual | Segundos de atraso não prejudicam ninguém | Réplicas, cache, CDN |
| Contadores de "fãs interessados", analytics | Eventual (CRDT) | Aproximado basta, disponibilidade importa | Contadores por região mesclados |
O princípio de projeto: forte onde há dinheiro ou unicidade em jogo, fraco em todo o resto — porque consistência forte é paga em toda requisição.
Na entrevista
Quando o entrevistador disser "e o usuário deve ver a atualização imediatamente", traduza para um modelo: "isso é read-your-writes, não consistência forte global — vou ler os dados do próprio usuário no líder, ou passar a posição no log da escrita como token." Depois aponte o único lugar que precisa de linearizabilidade (estoque, saldos, unicidade) e explique como obtê-la (um líder + failover baseado em consenso, escritas condicionais). Entrevistadores recompensam vocabulário preciso e escolhas por domínio; penalizam "tudo é fortemente consistente" (lento, caro) e "tudo é eventualmente consistente" (reservas em dobro).
Boas práticas
- Escreva o modelo de consistência de cada domínio de dados no documento de design — uma linha por domínio (assentos, pagamentos, Meus ingressos, catálogo), com o mecanismo que o garante; os revisores conferem o código contra ela.
- Use o primário por padrão e opte por réplicas por transação — uma leitura só vai para uma réplica quando alguém a marcou como tolerante a defasagem. Na BoxOffice:
@Transactional(readOnly = true)roteia para a réplica no lab 6 (quadro acima). - Meça leituras defasadas com um teste de escrever-e-ler — escreva, leia de volta na hora a partir da réplica, conte as falhas; sob carga o número nunca é zero. Na BoxOffice:
load/read-your-writes.jsdo lab 6 (6% sem atraso, 98% com 1 s). - Use uma coluna
versionem edições de ler-modificar-escrever — umUPDATE … WHERE version = :vcondicional transforma uma atualização perdida num conflito visível. Na BoxOffice:inventory.seats.version(lab 6). - Mostre o frescor onde a defasagem é permitida — "atualizado há 3 s" num contador servido por uma réplica ou um cache ajusta a expectativa do fã e poupa um chamado no suporte.
Armadilhas
- Separar leitura/escrita por padrão: mandar todas as leituras para réplicas quebra read-your-writes na primeira tela depois de uma escrita.
- Caches também são réplicas: colocar Redis ou uma CDN na frente de um armazenamento linearizável torna essas leituras eventualmente consistentes (Caching).
- Load balancers quebram garantias de sessão: duas leituras seguidas podem cair em duas réplicas com atrasos diferentes.
- Replicação assíncrona + failover automático = escritas perdidas: o seguidor promovido pode não ter as últimas escritas confirmadas. Use replicação semissíncrona para dados que você não pode perder.
- Confundir "eventual" com "logo": consistência eventual não tem limite de tempo; sob carga o atraso pode chegar a minutos. Monitore o atraso de replicação e crie alertas.
Como se conecta
flowchart LR
W["escrita no líder"] --> REPL{"replicação<br/>síncrona ou assíncrona?"}
REPL -->|"síncrona / quórum"| LIN["leituras linearizáveis"]
LIN --> COST["paga em latência e disponibilidade<br/>→ CAP e PACELC"]
REPL -->|"assíncrona: atraso"| STALE["leituras desatualizadas"]
STALE --> SG["garantias de sessão<br/>read-your-writes, leituras monotônicas"]
W2["escritas em vários líderes"] --> CONF["escritas concorrentes"]
CONF --> RES["LWW · vetores de versão · CRDTs<br/>→ Tempo e Relógios"]
LIN --> CONS["líder escolhido por consenso<br/>→ Consenso e Quóruns"] Componentes da paleta que são réplicas (e, portanto, têm um modelo de consistência): sql (primário + réplicas de leitura, cenário db-replicas) · nosql (sem líder, cenário sharding) · cache (cenário read-heavy-cache) · cdn (cenário cdn-static) · stream (partições replicadas). Apoia-se em: modelos de falha. Continua em: CAP e PACELC, Bancos de dados — replicação, Caching — invalidação.
Pontos-chave
- A replicação serve a disponibilidade, a vazão de leitura e a latência; toda a dificuldade está em propagar as escritas.
- Um líder evita conflitos de escrita; vários líderes e sem líder aceitam escritas em todo lugar e precisam resolver conflitos (LWW, vetores de versão, CRDTs, ou evitá-los com um líder de origem).
- Replicação síncrona vs assíncrona troca latência e disponibilidade de escrita por durabilidade no failover; semissíncrona ou por quórum é o meio-termo usual.
- Os modelos de consistência vão de linearizável (ilusão de cópia única), passando por causal e pelas garantias de sessão (read-your-writes, leituras monotônicas), até eventual.
- Escolha por domínio de dados: linearizável para assentos e dinheiro, read-your-writes para os dados do próprio usuário, eventual para o catálogo e os contadores.
-
KLEPPMANN, M.; RICCOMINI, C. Designing Data-Intensive Applications, 2ª ed. O'Reilly. Capítulo "Replication". ↩
-
DECANDIA, G. et al. Dynamo: Amazon's Highly Available Key-value Store. SOSP, 2007. ↩
-
Consistency Models — Jepsen (Kyle Kingsbury), um mapa clicável dos modelos e de suas relações. ↩
-
HERLIHY, M.; WING, J. Linearizability: A Correctness Condition for Concurrent Objects. ACM TOPLAS, 1990. ↩
-
MAHAJAN, P.; ALVISI, L.; DAHLIN, M. Consistency, Availability, and Convergence. UT Austin TR-11-22, 2011. ↩
-
TERRY, D. et al. Session Guarantees for Weakly Consistent Replicated Data. PDIS, 1994. ↩
-
SHAPIRO, M. et al. Conflict-free Replicated Data Types. SSS, 2011. ↩