Criei um sistema como esse para um aplicativo há cerca de 8 anos e posso compartilhar algumas maneiras pelas quais ele evoluiu à medida que o uso do aplicativo aumentou.
Comecei registrando todas as alterações (inserir, atualizar ou excluir) de qualquer dispositivo em uma tabela "histórico". Portanto, se, por exemplo, alguém alterar seu número de telefone na tabela "contato", o sistema editará o campo contact.phone e também adicionará um registro de histórico com action = update, field = phone, record = [ID do contato], valor = [novo número de telefone]. Então, sempre que um dispositivo é sincronizado, ele baixa os itens do histórico desde a última sincronização e os aplica ao seu banco de dados local. Isso soa como o padrão de "replicação de transação" descrito acima.
Um problema é manter os IDs exclusivos quando itens podem ser criados em dispositivos diferentes. Como eu não sabia sobre UUIDs quando comecei isso, usei IDs de incremento automático e escrevi um código complicado que é executado no servidor central para verificar as novas IDs carregadas dos dispositivos, alterá-las para uma ID exclusiva, se houver um conflito, e diga ao dispositivo de origem para alterar o ID em seu banco de dados local. Alterar as IDs dos novos registros não era tão ruim, mas se eu criar, por exemplo, um novo item na tabela de contatos, criar um novo item relacionado na tabela de eventos, agora tenho chaves estrangeiras que também serão necessárias. verifique e atualize.
Eventualmente, aprendi que os UUIDs poderiam evitar isso, mas meu banco de dados estava ficando muito grande e eu tinha medo de que uma implementação completa do UUID pudesse criar um problema de desempenho. Portanto, em vez de usar UUIDs completos, comecei a usar chaves alfanuméricas de 8 caracteres geradas aleatoriamente como IDs e deixei meu código existente no local para lidar com conflitos. Em algum lugar entre minhas chaves atuais de 8 caracteres e os 36 caracteres de um UUID, deve haver um ponto ideal que elimine conflitos sem inchaços desnecessários, mas como eu já tenho o código de resolução de conflitos, não tem sido uma prioridade experimentar isso. .
O próximo problema era que a tabela de histórico era cerca de 10 vezes maior que o restante do banco de dados. Isso torna o armazenamento caro, e qualquer manutenção na tabela de histórico pode ser dolorosa. Manter essa tabela inteira permite que os usuários recuperem qualquer alteração anterior, mas isso começou a parecer um exagero. Então, adicionei uma rotina ao processo de sincronização, onde, se o item do histórico baixado pela última vez do dispositivo não existir mais na tabela de histórico, o servidor não fornecerá os itens recentes do histórico, mas sim um arquivo contendo todos os dados para essa conta. Em seguida, adicionei um cronjob para excluir itens do histórico com mais de 90 dias. Isso significa que os usuários ainda podem reverter as alterações com menos de 90 dias e, se sincronizarem pelo menos uma vez a cada 90 dias, as atualizações serão incrementais como antes. Mas se eles esperarem mais de 90 dias,
Essa alteração reduziu o tamanho da tabela de histórico em quase 90%, portanto, agora a manutenção da tabela de histórico apenas torna o banco de dados duas vezes maior do que dez vezes maior. Outro benefício desse sistema é que a sincronização ainda pode funcionar sem a tabela de histórico, se necessário - como se eu precisasse fazer alguma manutenção que o deixasse temporariamente offline. Ou eu poderia oferecer diferentes períodos de reversão para contas com preços diferentes. E se houver mais de 90 dias de alterações para baixar, o arquivo completo geralmente é mais eficiente que o formato incremental.
Se eu estivesse começando de novo hoje, pularia a verificação de conflito de ID e visaria apenas um comprimento de chave suficiente para eliminar conflitos, com algum tipo de verificação de erro apenas por precaução. Mas a tabela de histórico e a combinação de downloads incrementais para atualizações recentes ou um download completo, quando necessário, estão funcionando bem.