P: Um servidor de serviço, um banco de dados, operação: consultar o saldo atual do usuário, deduzir 3% do saldo atual como taxa de tratamento
sincronizado fechadura Bloqueio de dB
P: Dois servidores de serviço, um banco de dados, operação: consultar o saldo atual do usuário, deduzir 3% do saldo atual como taxa de tratamento Bloqueios distribuídos
Que tipo de fechadura distribuída precisamos? Ele pode garantir que, em um cluster de aplicações distribuído, o mesmo método só possa ser executado por uma thread em uma máquina ao mesmo tempo.
Se essa eclusa for uma eclusa reentrante (evite deadlocks)
Essa fechadura é melhor para ser uma fechadura de bloqueio (considere se você quer essa de acordo com as necessidades do seu negócio)
Essa fechadura é melhor para ser justa (considere se você quer essa ou não de acordo com as necessidades do negócio)
Existem funções de aquisição e liberação de trava altamente disponíveis
O desempenho dos bloqueios de aquisição e liberação é melhor
1. Fechaduras distribuídas baseadas em bancos de dados
Bloqueios distribuídos baseados em implementações baseadas em tabelas
Quando quisermos travar um método, execute o seguinte SQL: inserir em methodLock(method_name,desc) valores ('method_name','desc')
Como estabelecemos uma restrição de unicidade no method_name, se múltiplas requisições forem enviadas ao banco de dados ao mesmo tempo, o banco garantirá que apenas uma operação possa ter sucesso, então podemos assumir que a thread que obteve com sucesso o bloqueio do método e pode executar o conteúdo do corpo do método.
Quando o método é executado, se você quiser liberar o bloqueio, precisa executar o seguinte SQL: delete from methodLock onde method_name ='method_name'
Essa implementação simples acima apresenta os seguintes problemas:
- Esse bloqueio depende da disponibilidade do banco de dados, que é um ponto único e fará com que o sistema de negócios fique indisponível assim que o banco de dados for desligado.
- Esse bloqueio não tem tempo de expiração e, uma vez que a operação de desbloqueio falha, o registro do bloqueio permanecerá no banco de dados e outras threads não poderão mais obter o bloqueio.
- Esse bloqueio só pode ser não bloqueante, pois a operação de inserção de dados reportará diretamente um erro após a falha da inserção. Threads que não adquirem locks não entrarão na fila e precisarão acionar novamente a operação de aquisição de lock para obter o lock novamente.
- A trava não é reentrante, e a mesma rosca não pode obter a fechadura novamente até que ela seja liberada. Porque os dados já existem.
- Essa fechadura é injusta, e todas as fias que aguardam a fechadura competem por sorte.
Claro, também podemos resolver os problemas acima de outras maneiras.
- O banco de dados é um ponto único? Construa dois bancos de dados, e os dados serão sincronizados em ambas as direções. Depois de desligar, mude rapidamente para a biblioteca de backup.
- Sem prazo de validade? Basta fazer uma tarefa agendada para limpar os dados de timeout no banco de dados em intervalos regulares.
- Não bloqueando? Faça um loop de tempo até que o insert tenha sucesso e então retorne o sucesso.
- Não reentrante? Adicione um campo à tabela do banco de dados para registrar as informações do host e das threads da máquina que atualmente recebe o lock, e da próxima vez que conseguir o lock, consulte o banco de dados primeiro; se as informações do host e do thread da máquina atual estiverem encontradas no banco de dados, você pode atribuir o lock diretamente a ele.
- Injusto? Crie outra tabela intermediária para registrar todas as threads que aguardam o lock e as organize de acordo com o tempo de criação, e apenas a primeira criada pode adquirir o lock
Bloqueios distribuídos baseados em bloqueios exclusivos
Além de adicionar e excluir registros na tabela de dados, bloqueios distribuídos também podem ser implementados com a ajuda dos bloqueios que vêm com os dados.
Também usamos a tabela de banco de dados que acabamos de criar. Bloqueios distribuídos podem ser implementados por meio de bloqueios exclusivos em bancos de dados. O motor InnoDB baseado em MySQL pode usar os seguintes métodos para implementar operações de bloqueio:
Se você adicionar para atualização após a instrução de consulta, o banco de dados adicionará um bloqueio exclusivo à tabela durante o processo de consulta. Quando um bloqueio exclusivo é adicionado a um registro, outros threads não podem mais adicionar um bloqueio exclusivo ao registro naquela linha.
Podemos pensar que a thread que obtém o lock exclusivo pode obter o lock distribuído, e quando o lock é obtido, a lógica de negócios do método pode ser executada e então desbloqueada pelos seguintes métodos:
desbloqueio de vazio público(){ connection.commit(); }
via connection.commit(); operação para abrir a fechadura.
Esse método pode resolver efetivamente os problemas mencionados acima sobre a incapacidade de liberar e bloquear a fechadura.
Bloqueando fechaduras? A instrução for update retorna imediatamente após a execução bem-sucedida e permanece bloqueada até que tenha sucesso.
Serviço caído após o bloqueio, não pode ser liberado? Dessa forma, o banco de dados libera o bloqueio sozinho depois que o serviço cai.
No entanto, ainda não resolve diretamente o problema do ponto único do banco de dados, reentrância e bloqueio justo.
Para resumir a forma de usar o banco de dados para implementar bloqueios distribuídos, ambos dependentes de uma tabela no banco de dados, um é determinar se há um bloqueio pela existência de registros na tabela, e o outro é implementar bloqueios distribuídos através do bloqueio exclusivo do banco de dados.
Vantagens de Bancos de Dados Distribuídos com Travamento
Diretamente com a ajuda do banco de dados, é fácil de entender.
Desvantagens de implementar bloqueios distribuídos em bancos de dados
Haverá vários problemas, e toda a solução se tornará cada vez mais complexa no processo de resolução do problema.
Operar o banco de dados exige certa sobrecarga, e questões de desempenho precisam ser consideradas.
2. Bloqueios distribuídos baseados em cache
Comparado à solução de bloqueio distribuído baseada em banco de dados, a implementação baseada em cache terá melhor desempenho em termos de desempenho.
Atualmente, existem muitos produtos de cache maduros, incluindo Redis, memcached, etc. Aqui tomamos o Redis como exemplo para analisar o esquema de usar cache para implementar bloqueios distribuídos.
Existem muitos artigos relacionados na Internet sobre a implementação de bloqueios distribuídos baseados em Redis, e o principal método de implementação é usar o método Jedis.setNX.
Há também vários problemas com a implementação acima:
1. Problema de ponto único.
2. Esse bloqueio não tem tempo de expiração; uma vez que a operação de desbloqueio falha, ele fará com que o registro do bloqueio fique em redis o tempo todo, e outras threads não conseguem mais obter o bloqueio.
3. Esse bloqueio só pode ser não bloqueante, e retornará diretamente independentemente do sucesso ou fracasso.
4. Essa fechadura é não reentrante, após uma fiação obter a fechadura, ela não pode recuperá-la antes de liberá-la, porque a chave usada já existe em redis. as operações setNX não podem mais ser executadas.
5. Esse bloqueio é injusto, todas as threads em espera iniciam operações setNX ao mesmo tempo, e threads sortudos podem conseguir o lock.
Claro, também existem maneiras de resolver isso.
- Hoje em dia, os serviços de cache tradicionais suportam a implantação de clusters para resolver problemas de ponto único por meio de clustering.
- Sem prazo de validade? O método setExpire do redis suporta o tempo de expiração recebido, e os dados são automaticamente excluídos após esse tempo ser atingido.
- Não bloqueando? enquanto executado repetidamente.
- Não é possível reentrar? Depois que uma thread adquirir o bloqueio, salve as informações atuais do host e do thread, e verifique se você é o proprietário do lock atual antes de obtê-lo da próxima vez.
- Injusto? Coloque todos os threads em espera em uma fila antes que um thread adquira um bloqueio, e então adquira o bloqueio em base a quem entra primeiro, primeiro a sair.
A política de sincronização do cluster Redis leva tempo, e é possível que a thread A fique bloqueada após definir o NX com sucesso, mas esse valor não foi atualizado para o servidor onde a thread B executa o setNX, o que causará problemas de concorrência.
Salvatore Sanfilippo, autor do redis, propôs o algoritmo Redlock, que implementa gerenciamento distribuído de bloqueios (DLM) mais seguro e confiável do que um único nó.
O algoritmo Redlock assume que existem N nós redis independentes entre si, geralmente definidos como N=5, e esses N nós rodam em máquinas diferentes para manter a independência física.
Os passos do algoritmo são os seguintes:
1. O cliente obtém o tempo atual em milissegundos. 2. O cliente tenta obter o bloqueio de N nós (cada nó recebe o bloqueio da mesma forma que o bloqueio de cache mencionado anteriormente), e N nós recebem o bloqueio com a mesma chave e valor. O cliente precisa definir o tempo de espera de acesso à interface, e o tempo de espera da interface precisa ser muito menor que o tempo de espera do bloqueio, por exemplo, o tempo liberado automaticamente do bloqueio é de 10s, e então o tempo de espera da interface é definido para cerca de 5-50ms. Isso permite que você use um tempo de espera o mais rápido possível ao acessar um nó redis após ele cair, e reduz o uso normal do bloqueio. 3. O cliente calcula quanto tempo leva para obter o bloqueio, subtraindo o tempo obtido no passo 1 pelo tempo atual, somente quando o cliente obtém mais de 3 nós do bloqueio, e o tempo para adquirir o bloqueio é menor que o tempo de expiração do bloqueio, o cliente obtém o bloqueio distribuído. 4. O tempo do cliente para adquirir o bloqueio é o tempo de encerramento definido menos o tempo gasto para obter o bloqueio calculado no passo 3. 5. Se o cliente não conseguir obter o bloqueio, o cliente apagará todos os bloqueios por sua vez. Usando o algoritmo Redlock, é possível garantir que o serviço de bloqueio distribuído ainda pode funcionar ao desligar até 2 nós, o que melhora muito a disponibilidade em comparação com o bloqueio anterior do banco de dados e do bloqueio de cache.
No entanto, um especialista distribuído escreveu um artigo chamado "Como fazer travamento distribuído" questionando a correção do Redlock.
O especialista mencionou que há dois aspectos a considerar ao considerar fechaduras distribuídas: desempenho e correção.
Se você usar um bloqueio distribuído de alto desempenho e a correção não for necessária, então usar um bloqueio de cache é suficiente.
Se você usa uma fechadura distribuída altamente confiável, então deve considerar questões rigorosas de confiabilidade. Redlock, por outro lado, não atinge a correção. Por que não? Especialistas listam vários aspectos.
Hoje em dia, muitas linguagens de programação usam máquinas virtuais com funções GC; no GC completo, o programa para de processar o GC, às vezes o GC completo demora muito, e até o programa tem alguns minutos de atraso. O artigo lista o exemplo do HBase, às vezes o HBase GC por alguns minutos, o que faz com que o contrato de locação expire. Por exemplo, na figura abaixo, o cliente 1 recebe um bloqueio e está prestes a processar um recurso compartilhado, e quando está prestes a processar um recurso compartilhado, o GC completo ocorre até que o bloqueio expire. Dessa forma, o cliente 2 recebe o bloqueio novamente e começa a trabalhar no recurso compartilhado. Quando o cliente 2 está processando, o cliente 1 completa a GC completa e começa a processar recursos compartilhados, de modo que ambos os clientes processam recursos compartilhados.
Especialistas apresentaram uma solução, como mostrado na figura abaixo, parece que o MVCC traz um token para o bloqueio, token é o conceito de versão, toda vez que o lock de operação é concluído, o token será adicionado 1, traga o token ao processar recursos compartilhados, apenas a versão especificada do token pode lidar com o recurso compartilhado.
Depois, o especialista também disse que o algoritmo depende do tempo local, e que o Redis depende do método getTimeOfDay para obter a hora em vez do relógio monótono ao lidar com a expiração da chave, o que também leva a imprecisões no tempo. Por exemplo, em um cenário, dois clientes 1 e cliente 2 possuem 5 nós redis (A, B, C, D e E).
1. O Cliente 1 adquire com sucesso o bloqueio de A, B e C, e obtém o timeout da rede de bloqueio de D e E. 2. O clock do nó C é impreciso, causando o tempo limite do bloqueio. 3. o cliente 2 adquire com sucesso o bloqueio de C, D e E, e obtém o timeout da rede de bloqueio de A e B. 4. Dessa forma, tanto o cliente 1 quanto o cliente 2 têm um bloqueio. Para resumir os dois pontos que especialistas dizem sobre a indisponibilidade de Redlock:
1. O GC e outros cenários podem ocorrer a qualquer momento, fazendo com que o cliente obtenha um bloqueio, e o tempo de processamento faça com que outro cliente obtenha o bloqueio. Especialistas também sugeriram uma solução para o uso de tokens auto-incrementáveis. 2. O algoritmo depende do horário local, e o relógio será impreciso, resultando em dois clientes recebendo bloqueios ao mesmo tempo. Portanto, a conclusão dada pelos especialistas é que o Redlock só pode funcionar normalmente na faixa de atraso de rede limitada, interrupção de programa limitada e erro de clock limitado, mas os limites desses três cenários não podem ser confirmados, portanto os especialistas não recomendam o uso do Redlock. Para cenários com altos requisitos de correção, especialistas recomendam o Zookeeper, que será discutido posteriormente usando o Zookeeper como um cadeado distribuído.
Resposta do autor de Redis
O autor da Redis respondeu escrevendo um blog após ver o artigo do especialista. O autor agradeceu educadamente ao especialista e então expressou sua discordância com a visão do especialista.
A discussão do autor do REDIS sobre o uso de tokens para resolver o problema do tempo limite do bloqueio pode ser resumida nos seguintes cinco pontos:
Ponto 1, o uso de bloqueios distribuídos geralmente significa que você não tem outra forma de controlar recursos compartilhados, especialistas usam tokens para garantir o processamento dos recursos compartilhados, então não há necessidade de bloqueios distribuídos. Ponto 2: Para geração de tokens, para garantir a confiabilidade dos tokens obtidos por diferentes clientes, o serviço que gera tokens ainda precisa de bloqueios distribuídos para garantir a confiabilidade do serviço. Ponto 3, para a forma como especialistas dizem que tokens auto-incrementáveis, o autor do redis acredita que é completamente desnecessário, cada cliente pode gerar um uuid único como token e definir o recurso compartilhado para um estado que somente o cliente com o uuid pode lidar, de modo que outros clientes não possam processar o recurso compartilhado até que o cliente que obtém o bloqueio libere o bloqueio. Como mostrado na figura acima, se o cliente do token 34 enviar GC durante o processo de escrita e causar o tempo limite do bloqueio, outro cliente pode obter o bloqueio do token 35 e começar a escrever novamente, resultando em um conflito de lock. Portanto, a ordem dos tokens não pode ser combinada com recursos compartilhados. Ponto 5, o autor do redis acredita que, na maioria dos cenários, bloqueios distribuídos são usados para lidar com problemas de atualização em cenários não transacionais. O autor deve querer dizer que existem alguns cenários em que é difícil combinar tokens para lidar com recursos compartilhados, então você precisa depender de bloqueios para bloquear recursos e processá-los. Outro problema de relógio que especialistas mencionam, os autores do Redis também dão uma explicação. Se o tempo necessário para adquirir o cadeado for muito longo e exceder o tempo padrão de tempo limite do bloqueio, então o cliente não poderá obter o cadeado nesse momento, e não haverá exemplos propostos por especialistas.
Sentimentos pessoais
O primeiro problema que resumo é que, após um cliente obter um bloqueio distribuído, o bloqueio pode ser liberado após um tempo limite durante o processamento do cliente. Anteriormente, ao falar do timeout definido pelo bloqueio do banco de dados de 2 minutos, se uma tarefa ocupa um bloqueio de ordem por mais de 2 minutos, então o outro centro de negociação pode obter esse bloqueio de ordens, para que os dois centros possam processar a mesma ordem ao mesmo tempo. Em circunstâncias normais, a tarefa é processada em segundos, mas às vezes, o timeout estabelecido ao entrar em uma requisição RPC é muito longo, e há múltiplas requisições de timeout desse tipo em uma tarefa, então é provável que o tempo de desbloqueio automático seja ultrapassado. Se escrevermos em Java, pode haver Full GC no meio, então depois que o bloqueio é desbloqueado após o timeout do bloqueio, o cliente não consegue percebê-lo, o que é algo muito sério. Não acho que isso seja um problema do bloqueio em si, desde que qualquer bloqueio distribuído mencionado acima tenha características de liberação de timeout, esse problema vai ocorrer. Se você usar a função de tempo limite do bloqueio, o cliente deve definir o tempo limite do bloqueio e agir de acordo, em vez de continuar processando o recurso compartilhado. O algoritmo do Redlock retorna o tempo de bloqueio que o cliente pode ocupar após o cliente adquirir o bloqueio, e o cliente deve processar esse tempo para interromper a tarefa após esse tempo.
O segundo problema é que especialistas distribuídos não entendem Redlock. Uma característica chave do Redlock é que o tempo para adquirir o bloqueio é o tempo total em que o bloqueio expira menos o tempo necessário para adquiri-lo, de modo que o tempo que o cliente leva para processar é relativo, independentemente do horário local.
Sob esse ponto de vista, a correção do Redlock pode ser bem garantida. Análise cuidadosa do Redlock, comparado ao redis de um nó, a principal característica proporcionada pelo Redlock é a maior confiabilidade, o que é um recurso importante em alguns cenários. Mas acho que a Redlock gastou dinheiro demais para alcançar confiabilidade.
- Primeiro, 5 nós devem ser implantados para tornar o Redlock mais confiável.
- Depois, você precisa solicitar 5 nós para obter o bloqueio, e pelo método Futuro, você pode primeiro solicitar para 5 nós simultaneamente e depois reunir o resultado da resposta, o que pode encurtar o tempo de resposta, mas ainda leva mais tempo do que um lock-rede de nó único.
- Então, como mais de 3 dos 5 nós precisam ser obtidos, pode haver um conflito de bloqueio, ou seja, todos obtiveram 1 ou 2 bloqueios, e como resultado, ninguém consegue obter o bloqueio; esse problema, o autor do Redis toma emprestado a essência do algoritmo de raft, por meio da colisão em tempo aleatório, o tempo de conflito pode ser muito reduzido, mas esse problema não pode ser evitado muito bem, especialmente quando o bloqueio é adquirido pela primeira vez, então o custo de tempo para adquirir o bloqueio aumenta.
- Se 2 dos 5 nós estiverem fora do ar, a disponibilidade do bloqueio será muito reduzida; primeiro, você deve esperar que os resultados desses dois nós caídos expirem antes de retornar, e há apenas 3 nós, e o cliente deve obter os bloqueios de todos os 3 nós para ter o bloqueio, o que também é mais difícil.
- Se houver uma partição de rede, pode haver uma situação em que o cliente nunca conseguirá obter o bloqueio.
Depois de analisar tantos motivos, acho que o ponto mais crítico do problema do Redlock é que o Redlock exige que os clientes garantam a consistência das escritas, e os 5 nós do backend são completamente independentes, e todos os clientes precisam operar esses 5 nós. Se houver um líder entre 5 nós, o cliente pode sincronizar os dados do líder desde que o cliente obtenha o bloqueio do líder, para que não haja problemas como particionamento, timeouts e conflitos. Portanto, para garantir a correção dos bloqueios distribuídos, acredito que usar um serviço de coordenação distribuído com forte consistência pode resolver melhor o problema.
A pergunta surge novamente: quanto tempo devo definir o tempo de validade? Como definir o tempo de invalidação é muito curto, e o bloqueio é liberado automaticamente antes da execução do método, então haverá problemas de concorrência. Se demorar muito, outras roscas que bloqueiam podem ter que esperar bastante.
Esse problema também existe com o uso de bancos de dados para implementar locks distribuídos.
A abordagem atual para esse problema é definir um tempo de tempo de espera curto para cada trava obtida e iniciar uma thread para atualizar o tempo de tempo de fechamento toda vez que estiver prestes a atingir esse tempo. Termine este tópico ao mesmo tempo em que se libera a trava. Por exemplo, o redisson, o componente oficial de bloqueio distribuído do redis, usa essa solução.
Vantagens de usar cache para implementar bloqueios distribuídos Boa performance.
Desvantagens de usar cache para implementar bloqueios distribuídos A implementação é muito responsável, há muitos fatores a serem considerados.
Bloqueios distribuídos baseados na implementação do Zookeeper
Bloqueios distribuídos baseados em nós ordenados temporários do Zookeeper.
A ideia geral é que, quando cada cliente bloqueia um método, um nó ordenado instantâneo único é gerado no diretório do nó especificado correspondente ao método no zookeeper. A forma de determinar se é necessário obter um bloqueio é simples, você só precisa determinar aquele com o menor número de série no nó ordenado. Quando o bloqueio for liberado, basta excluir o nó instantâneo. Ao mesmo tempo, pode evitar o problema de bloqueios causados por interrupções de serviço que não podem ser liberadas.
Vamos ver se o Zookeeper consegue resolver os problemas mencionados anteriormente.
- A fechadura não solta? Usar o Zookeeper pode resolver efetivamente o problema de bloqueios não serem liberados, porque ao criar um bloqueio, o cliente criará um nó temporário em ZK, e uma vez que o cliente obtém o bloqueio e o trava de repente (a conexão da sessão está quebrada), o nó temporário será automaticamente excluído. Outros clientes podem conseguir a fechadura novamente.
- Fechaduras que não bloqueiam? Uma vez que o nó muda, o Zookeeper notificará o cliente, e o cliente poderá verificar se o nó criado é o menor número ordinal entre todos os nós.
- Não pode reentrar? Quando o cliente cria um nó, ele escreve diretamente as informações do host e do thread do cliente atual para o nó, e da próxima vez que você quiser obter o lock, pode compará-lo com os dados do menor nó atual. Se a informação for igual à sua, então você pode obter o bloqueio diretamente e, se for diferente, criar um nó sequencial temporário para participar da fila.
A questão surge novamente: sabemos que o Zookeeper precisa ser implantado em clusters, haverá problemas de sincronização de dados como nos clusters do Redis?
O Zookeeper é um componente distribuído que garante consistência fraca, ou seja, consistência eventual.
O Zookeeper emprega um protocolo de sincronização de dados chamado Protocolo Baseado em Quórum. Se houver N servidores Zookeeper no cluster Zookeeper (N geralmente é ímpar, 3 podem manter confiabilidade de dados e alto desempenho de leitura e escrita, e 5 têm o melhor equilíbrio entre confiabilidade de dados e desempenho de leitura e escrita), então uma operação de escrita do usuário é primeiro sincronizada com N/2 + 1 servidores e então retornada ao usuário, solicitando que o usuário escreva com sucesso. O protocolo de sincronização de dados baseado no Protocolo Baseado em Quórum determina a consistência da força que o Zookeeper pode suportar.
Em um ambiente distribuído, o armazenamento de dados que mantém forte consistência é basicamente inexistente, e requer que todos os nós sejam atualizados de forma síncrona ao atualizar os dados de um nó. Essa estratégia de sincronização aparece no banco de dados de replicação síncrona mestre-escravo. No entanto, essa estratégia de sincronização tem impacto excessivo no desempenho da escrita e raramente é vista na prática. Como o Zookeeper escreve N/2+1 nós de forma síncrona, e N/2 nós não são atualizados de forma síncrona, o Zookeeper não é fortemente consistente.
A operação de atualização de dados do usuário não garante que leituras subsequentes leam o valor atualizado, mas eventualmente mostrará consistência. Sacrificar consistência não significa ignorar completamente a consistência dos dados, caso contrário os dados ficam caóticos, então, não importa quão alta seja a disponibilidade do sistema, não importa quão boa seja a distribuição, ela não tem valor. Sacrificar consistência é apenas que uma consistência forte em bancos de dados relacionais não é mais necessária, mas enquanto o sistema conseguir alcançar consistência eventual.
Uma pergunta de ponto único? Usar o Zookeeper pode resolver efetivamente um problema de ponto único, o ZK é implantado em clusters, desde que mais da metade das máquinas do cluster sobreviva, o serviço pode ser fornecido ao mundo exterior.
Questões de justiça? Usar o Zookeeper pode resolver o problema dos bloqueios justos, os nós temporários criados pelo cliente em ZK são ordenados, e toda vez que o bloqueio é liberado, ZK pode notificar o menor nó para obter o bloqueio, garantindo justiça.
A questão surge novamente: sabemos que o Zookeeper precisa ser implantado em clusters, haverá problemas de sincronização de dados como nos clusters do Redis?
O Zookeeper é um componente distribuído que garante consistência fraca, ou seja, consistência eventual.
O Zookeeper emprega um protocolo de sincronização de dados chamado Protocolo Baseado em Quórum. Se houver N servidores Zookeeper no cluster Zookeeper (N geralmente é ímpar, 3 podem manter confiabilidade de dados e alto desempenho de leitura e escrita, e 5 têm o melhor equilíbrio entre confiabilidade de dados e desempenho de leitura e escrita), então uma operação de escrita do usuário é primeiro sincronizada com N/2 + 1 servidores e então retornada ao usuário, solicitando que o usuário escreva com sucesso. O protocolo de sincronização de dados baseado no Protocolo Baseado em Quórum determina a consistência da força que o Zookeeper pode suportar.
Em um ambiente distribuído, o armazenamento de dados que mantém forte consistência é basicamente inexistente, e requer que todos os nós sejam atualizados de forma síncrona ao atualizar os dados de um nó. Essa estratégia de sincronização aparece no banco de dados de replicação síncrona mestre-escravo. No entanto, essa estratégia de sincronização tem impacto excessivo no desempenho da escrita e raramente é vista na prática. Como o Zookeeper escreve N/2+1 nós de forma síncrona, e N/2 nós não são atualizados de forma síncrona, o Zookeeper não é fortemente consistente.
A operação de atualização de dados do usuário não garante que leituras subsequentes leam o valor atualizado, mas eventualmente mostrará consistência. Sacrificar consistência não significa ignorar completamente a consistência dos dados, caso contrário os dados ficam caóticos, então, não importa quão alta seja a disponibilidade do sistema, não importa quão boa seja a distribuição, ela não tem valor. Sacrificar consistência é apenas que uma consistência forte em bancos de dados relacionais não é mais necessária, mas enquanto o sistema conseguir alcançar consistência eventual.
Se o Zookeeper atende à consistência causal depende de como o cliente é programado.
Práticas que não satisfazem a consistência causal
- O Processo A grava um dado no /z do Zookeeper e retorna com sucesso
- O processo A informa o processo B que A modificou os dados de /z
- B lê os dados do /z do Zoológico
- Como o servidor do Zookeeper conectado a B pode não ter sido atualizado com os dados escritos de A, B não poderá ler os dados escritos de A
Práticas que atendem à consistência causal
- O Processo B escuta mudanças de dados em /z no Zookeeper
- O Processo A grava um dado no /z do Zookeeper e, antes que retorne com sucesso, o Zookeeper precisa ligar para o ouvinte registrado em /z, e o líder notificará B sobre a alteração dos dados
- Depois que o método de resposta a eventos do processo B é respondido, ele recebe os dados alterados, então B definitivamente conseguirá obter o valor alterado
- Consistência causal aqui refere-se à consistência causal entre o Líder e B, ou seja, o líder notifica os dados de uma mudança
O segundo mecanismo de escuta de eventos também é o método que deve ser usado para programar corretamente o Zookeeper, então o Zookeeper deve cumprir a consistência causal
Portanto, quando implementamos bloqueios distribuídos baseados no Zookeeper, devemos usar a prática de satisfazer a consistência causal, ou seja, os threads que aguardam o lock ouvem as mudanças no lock do Zookeeper, e quando o lock é liberado, o Zookeeper notificará o thread em espera que atenda às condições de lock justo.
Você pode usar diretamente o cliente de biblioteca de terceiros Zookeeper, que encapsula um serviço de trava reentrante.
Bloqueios distribuídos implementados com ZK parecem se encaixar exatamente no que esperávamos de um bloqueio distribuído no início deste artigo. No entanto, não é, e o bloqueio distribuído implementado pelo Zookeeper tem uma desvantagem, ou seja, o desempenho pode não ser tão alto quanto o do serviço de cache. Porque toda vez no processo de criar e liberar um bloqueio, nós instantâneos precisam ser criados e destruídos dinamicamente para realizar a função de bloqueio. A criação e exclusão de nós no ZK só pode ser realizada pelo servidor líder, e então os dados são compartilhados com todas as máquinas seguidoras.
Vantagens de usar o Zookeeper para implementar bloqueios distribuídos Resolver efetivamente problemas de ponto único, problemas de não reentrada, problemas de não bloqueio e falha de liberação do bloqueio. É relativamente simples de implementar.
Desvantagens de usar o Zookeeper para implementar bloqueios distribuídos O desempenho não é tão bom quanto usar cache para implementar locks distribuídos. É necessário compreender os princípios do ZK.
Comparação das três opções
Do ponto de vista da facilidade de entendimento (do baixo para o alto) Banco de > Cache > Zookeeper
Do ponto de vista da complexidade da implementação (de baixo para alto) Bancos de dados > cache > do Zookeeper
Do ponto de vista da performance (do alto para o baixo) Cache > Zookeeper >= banco de dados
Do ponto de vista da confiabilidade (do alto para o baixo) Bancos de dados > cache > do Zookeeper
|