Padrão / algoritmo de sincronização cliente-servidor?

224

Tenho a sensação de que deve haver padrões de sincronização cliente-servidor por aí. Mas eu totalmente falhei no google um.

A situação é bastante simples - o servidor é o nó central, ao qual vários clientes se conectam e manipulam os mesmos dados. Os dados podem ser divididos em átomos, em caso de conflito, o que estiver no servidor, tem prioridade (para evitar que o usuário resolva conflitos). A sincronização parcial é preferida devido a quantidades potencialmente grandes de dados.

Existem padrões / boas práticas para essa situação ou se você não souber de qual - qual seria sua abordagem?

Abaixo está como penso agora em resolvê-lo: Paralelamente aos dados, um diário de modificação será realizado, com todas as transações com carimbo de data e hora. Quando o cliente se conecta, ele recebe todas as alterações desde a última verificação, de forma consolidada (o servidor passa por listas e remove adições que são seguidas por exclusões, mescla atualizações para cada átomo, etc.). Et voila, estamos atualizados.

A alternativa seria manter a data de modificação de cada registro e, em vez de executar exclusões de dados, apenas marque-as como excluídas.

Alguma ideia?

tm_lv
fonte
27
concordaram há muito pouca conversa de padrões para esse tipo de coisa ... mesmo que este cenário é bastante comum
Jack Ukleja

Respostas:

88

Você deve observar como funciona o gerenciamento de alterações distribuídas. Veja SVN, CVS e outros repositórios que gerenciam o trabalho deltas.

Você tem vários casos de uso.

  • Sincronize as alterações. Sua abordagem do log de alterações (ou histórico delta) parece ser boa para isso. Clientes enviam seus deltas para o servidor; O servidor consolida e distribui os deltas para os clientes. Este é o caso típico. Os bancos de dados chamam isso de "replicação de transação".

  • O cliente perdeu a sincronização. Através de um backup / restauração ou devido a um erro. Nesse caso, o cliente precisa obter o estado atual do servidor sem passar pelos deltas. Esta é uma cópia do mestre para os detalhes, deltas e desempenho sejam condenados. É uma coisa única; o cliente está quebrado; não tente otimizar isso, apenas implemente uma cópia confiável.

  • Cliente é suspeito. Nesse caso, você precisa comparar o cliente com o servidor para determinar se o cliente está atualizado e precisa de deltas.

Você deve seguir o padrão de design do banco de dados (e SVN) de numerar sequencialmente todas as alterações. Dessa forma, um cliente pode fazer uma solicitação trivial ("Que revisão devo ter?") Antes de tentar sincronizar. E mesmo assim, a consulta ("Todos os deltas desde 2149") é deliciosamente simples para o cliente e o servidor processarem.

S.Lott
fonte
O senhor pode explicar o que é exatamente um delta? Meu palpite seria que é uma combinação de hash / timestamp ... Eu gostaria de ouvir de você, senhor.
Anis
Um delta refere-se à alteração entre duas revisões. Por exemplo, se o nome de um usuário mudou, o delta pode ser algo como {revisão: 123, nome: "John Doe"}
dipole_moment
31

Como parte da equipe, fiz vários projetos que envolviam sincronização de dados, por isso devo ser competente para responder a essa pergunta.

A sincronização de dados é um conceito bastante amplo e há muito para discutir. Abrange uma variedade de abordagens diferentes, com suas vantagens e desvantagens. Aqui está uma das classificações possíveis com base em duas perspectivas: Síncrono / Assíncrono, Cliente / Servidor / Ponto a Ponto. A implementação da sincronização depende muito desses fatores, da complexidade do modelo de dados, da quantidade de dados transferidos e armazenados e de outros requisitos. Portanto, em cada caso específico, a escolha deve ser a implementação mais simples que atende aos requisitos do aplicativo.

Com base em uma análise das soluções disponíveis no mercado, podemos delinear várias classes principais de sincronização, diferentes na granularidade dos objetos sujeitos à sincronização:

  • A sincronização de um documento ou banco de dados inteiro é usada em aplicativos baseados na nuvem, como Dropbox, Google Drive ou Yandex.Disk. Quando o usuário edita e salva um arquivo, a nova versão do arquivo é carregada na nuvem completamente, substituindo a cópia anterior. Em caso de conflito, as duas versões do arquivo são salvas para que o usuário possa escolher qual versão é mais relevante.
  • A sincronização de pares de valores-chave pode ser usada em aplicativos com uma estrutura de dados simples, onde as variáveis ​​são consideradas atômicas, ou seja, não divididas em componentes lógicos. Essa opção é semelhante à sincronização de documentos inteiros, pois o valor e o documento podem ser substituídos completamente. No entanto, da perspectiva do usuário, um documento é um objeto complexo composto por várias partes, mas um par de valores-chave é apenas uma sequência curta ou um número. Portanto, neste caso, podemos usar uma estratégia mais simples de resolução de conflitos, considerando o valor mais relevante, caso tenha sido o último a mudar.
  • A sincronização de dados estruturados como uma árvore ou um gráfico é usada em aplicativos mais sofisticados, em que a quantidade de dados é grande o suficiente para enviar o banco de dados na íntegra a cada atualização. Nesse caso, os conflitos devem ser resolvidos no nível de objetos, campos ou relacionamentos individuais. Nosso foco principal é esta opção.

Portanto, reunimos nosso conhecimento neste artigo, que acho que pode ser muito útil para todos os interessados ​​no tópico => Sincronização de dados em aplicativos iOS baseados em dados básicos ( http://blog.denivip.ru/index.php/2014/04 / sincronização de dados no ios-apps com base em dados do núcleo /? lang = pt-br )

Denis Bulichenko
fonte
3
^^^^^^ esta é de longe a melhor resposta, pessoal!
precisa saber é
Eu concordo, Denis trouxe muito para o tópico + os links dos artigos são incríveis. Também fala sobre o AT mencionado por DanielPaull. Responder por S.Lott é bom, mas isso é muito mais profundo.
Krystian
28

O que você realmente precisa é de Operational Transform (OT). Isso pode até atender os conflitos em muitos casos.

Essa ainda é uma área ativa de pesquisa, mas existem implementações de vários algoritmos de OT. Estou envolvido nessa pesquisa há vários anos, então, deixe-me saber se essa rota lhe interessa e teremos prazer em colocá-lo em recursos relevantes.

Daniel Paull
fonte
7
Daniel, um indicador de recursos relevantes seria apreciado.
Parand
4
Acabei de reler o artigo da Wikipedia. Já percorreu um longo caminho e tem muitas referências relevantes na parte inferior dessa página. Eu indicaria o trabalho de Chengzheng Sun - o trabalho dele é referenciado na wikipedia. en.wikipedia.org/wiki/Operational_transformation . Espero que ajude!
Daniel Paull
13

A questão não é clara, mas eu consideraria o bloqueio otimista se fosse você. Pode ser implementado com um número de sequência que o servidor retorna para cada registro. Quando um cliente tenta salvar o registro novamente, ele incluirá o número de sequência recebido do servidor. Se o número de sequência corresponder ao conteúdo do banco de dados no momento em que a atualização for recebida, a atualização será permitida e o número de sequência será incrementado. Se os números de sequência não coincidirem, a atualização não será permitida.

erikkallen
fonte
2
Os números de sequência são seus amigos aqui. Pense em filas de mensagens persistentes.
Daniel Paull
7

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.

arlomedia
fonte
1

Para sincronização delta (alteração), você pode usar o padrão pubsub para publicar as alterações novamente em todos os clientes inscritos, serviços como o empurrador podem fazer isso.

Para o espelho do banco de dados, algumas estruturas da Web usam um minibanco de dados local para sincronizar o banco de dados do lado do servidor para o local no banco de dados do navegador. A sincronização parcial é suportada. Verifique o medidor .

fuyi
fonte