Melhores práticas do Redis, parte 2

A segunda parte do ciclo de conversão Redis Best Practices do Redis Labs, e discute padrões de interação e padrões de armazenamento de dados.

A primeira parte está aqui .

Padrões de interação


O Redis pode funcionar não apenas como um DBMS tradicional, mas também suas estruturas e comandos podem ser usados ​​para trocar mensagens entre microsserviços ou processos. O uso generalizado dos clientes Redis, a velocidade e a eficiência do servidor e do protocolo, bem como as estruturas clássicas integradas, permitem que você crie seus próprios fluxos de trabalho e mecanismos de eventos. Neste capítulo, abordaremos os seguintes tópicos:

  • fila de eventos;
  • bloqueio com Redlock;
  • Pub / Sub;
  • eventos distribuídos.

Fila de eventos


As listas no Redis são listas de linhas ordenadas, muito semelhantes às listas vinculadas com as quais você pode estar familiarizado. Adicionar um valor a uma lista (push) e excluir um valor de uma lista (pop) são operações muito leves. Como você pode imaginar, essa é uma estrutura muito boa para gerenciar uma fila: adicione elementos ao início e leia-os do final (FIFO). O Redis também fornece recursos adicionais que tornam esse padrão mais eficiente, confiável e fácil de usar.

As listas possuem um subconjunto de comandos que permitem executar o comportamento de "bloqueio". O termo "bloqueio" refere-se a uma conexão com apenas um cliente. De fato, esses comandos não permitem que o cliente faça nada até que um valor apareça na lista ou até o tempo limite expirar. Isso elimina a necessidade de pesquisar Redis, aguardando o resultado. Como o cliente não pode fazer nada enquanto espera um valor, precisaremos de dois clientes abertos para ilustrar isso:
#Cliente 1Cliente 2
1 1
> BRPOP my-q 0
[valor esperado]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[cliente desbloqueado, pronto para aceitar comandos]
3
> BRPOP my-q 0
[valor esperado]

Neste exemplo, na etapa 1, vemos que o cliente bloqueado não retorna nada imediatamente, pois não contém nada. O argumento final é o tempo de espera. Aqui 0 significa expectativa eterna. Na segunda linha , um valor é inserido em my-q e o primeiro cliente sai imediatamente do estado de bloqueio. Na terceira linha, o BRPOP é chamado novamente (você pode fazer isso em um loop no aplicativo) e o cliente também espera pelo próximo valor. Pressionando “Ctrl + C”, você pode quebrar o bloqueio e sair do cliente.

Vamos reverter o exemplo e ver como o BRPOP funciona com uma lista não vazia:
#Cliente 1Cliente 2
1 1
> LPUSH my-q hello
(integer) 1
2
> LPUSH my-q hej
(integer) 2
3
> LPUSH my-q bonjour
(integer) 3
4
> BRPOP my-q 0
1) "my-q"
2) "hello"
5
> BRPOP my-q 0
1) "my-q"
2) "hej"
6
> BRPOP my-q 0
1) "my-q"
2) "bonjour"
7
> BRPOP my-q 0
[valor esperado]

Nas etapas 1 a 3, adicionamos 3 valores à lista e vemos que a resposta aumenta, indicando o número de elementos na lista. A etapa 4, apesar de chamar o BRPOP, retorna o valor imediatamente. Isso ocorre porque o comportamento de bloqueio ocorre apenas quando não há valores na fila. Podemos ver a mesma resposta instantânea nas etapas 5 a 6, pois isso é feito para cada item da fila. Na etapa 7, o BRPOP não encontra nada na fila e bloqueia o cliente até que algo seja adicionado.

Geralmente, as filas representam algum trabalho que precisa ser realizado em outro processo (trabalhador). Nesse tipo de carga de trabalho, é importante que o trabalho não desapareça se o trabalhador cair por algum motivo durante a execução. O Redis suporta esse tipo de fila. Para fazer isso, use o comando BRPOPLPUSH em vez de BRPOP. Ela espera um valor em uma lista e, assim que aparece lá, coloca-o em outra lista. Isso é feito atomicamente, portanto, é impossível para dois trabalhadores alterar o mesmo valor. Vamos ver como isso funciona:
#Cliente 1Cliente 2
1 1
> LINDEX worker-q 0
(nil)
2[Se o resultado não for nulo, processe-o de alguma forma e vá para a etapa 4]
3
> LREM worker-q -1 [   1]
(integer) 1
[retornar ao passo 1]
4
> BRPOPLPUSH my-q worker-q 0
[valor esperado]
5
> LPUSH my-q hello
"hello"
[cliente desbloqueado, pronto para aceitar comandos]
6[lidar com oi]
7
> LREM worker-q -1 hello
(integer) 1
8[retornar ao passo 1]

Nas etapas 1 a 2, não fazemos nada porque o worker-q está vazio. Se algo retornou, então processamos e excluímos e, novamente, retornamos à etapa 1 para verificar se algo entrou na fila. Assim, primeiro limpamos a fila do trabalhador e executamos o trabalho existente. Na etapa 4, esperamos até que o valor apareça em my-q e, quando aparecer , ele seja transferido atomicamente para worker-q . Então, de alguma forma, processamos “olá” , depois disso, o excluímos do worker-q e retornamos à etapa 1. Se o processo morrer na etapa 6, o valor ainda permanecerá na worker-q . Após reiniciar o processo, excluiremos imediatamente tudo o que não foi excluído na etapa 7.

Esse padrão reduz bastante a probabilidade de perda de emprego, mas apenas se o trabalhador morrer entre as etapas 2 e 3 ou 5 e 6, o que é improvável, mas as práticas recomendadas levarão isso em consideração na lógica do trabalhador.

Bloquear com redlock


Às vezes, no sistema, é necessário bloquear algum recurso. Isso pode ser necessário para aplicar mudanças importantes que não podem ser resolvidas em um ambiente competitivo. Objetivos de bloqueio:

  • permitir que um e apenas um trabalhador capture o recurso;
  • ser capaz de liberar com segurança o objeto de bloqueio;
  • Não bloqueie o recurso firmemente (deve ser desbloqueado após um certo período de tempo).

O Redis é uma boa opção para implementar o bloqueio, pois possui um modelo de dados baseado em chave simples e cada shard é de thread único e bastante rápido. Existe uma excelente implementação de bloqueio usando Redis chamado Redlock.
Os clientes Redlock estão disponíveis para quase todos os idiomas; no entanto, é importante saber como o Redlock funciona para usá-lo com segurança e eficácia.

Primeiro, você precisa entender que o Redlock foi projetado para rodar em pelo menos três máquinas com instâncias Redis independentes. Isso elimina o ponto único de falha no seu mecanismo de bloqueio, o que pode levar a um impasse de todos os recursos. Outro ponto a entender é que, embora os relógios nas máquinas não devam ser 100% sincronizados, eles devem funcionar da mesma maneira - o tempo se move na mesma velocidade: um segundo na máquina E o mesmo que um segundo na máquina B.

A configuração de um objeto de bloqueio com o Redlock começa obtendo um carimbo de data / hora com precisão de milissegundos. Você também deve indicar antecipadamente o tempo de bloqueio. Em seguida, o objeto de bloqueio é definido definindo (SET) a chave com um valor aleatório (apenas se essa chave ainda não existir) e definindo o tempo limite da chave. Isso é repetido para cada instância independente. Se a instância cair, será imediatamente ignorada. Se o objeto de bloqueio foi instalado com sucesso na maioria das instâncias antes que o tempo limite expire, ele será considerado capturado. O tempo para instalar ou atualizar o objeto de bloqueio é o tempo necessário para atingir o estado de bloqueio, menos o tempo de bloqueio predefinido. No caso de um erro ou tempo limite, desbloqueie todas as instâncias e tente novamente.

Para liberar um objeto de bloqueio, é melhor usar um script Lua que verifique se o valor aleatório esperado está no conjunto de chaves. Se houver, é possível excluí-lo, caso contrário, é melhor deixar as chaves, pois esses podem ser objetos de bloqueio mais recentes.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

O processo Redlock fornece boas garantias e a ausência de um único ponto de falha, para que você possa ter certeza absoluta de que os objetos de bloqueio único serão distribuídos e que nenhum bloqueio mútuo ocorrerá.

Pub / Sub


Além do armazenamento de dados, o Redis também pode ser usado como plataforma Pub / Sub (editora / assinante). Nesse padrão, um editor pode emitir mensagens para qualquer número de assinantes de canais. Essas são mensagens baseadas no princípio de “atirar e esquecer”, ou seja, se a mensagem for liberada e o assinante não existir, a mensagem desaparecerá sem a possibilidade de recuperação.
Ao se inscrever no canal, o cliente entra no modo de assinante e não pode mais chamar comandos - ele se torna somente leitura. O editor não tem essas restrições.

Você pode se inscrever em mais de um canal. Começamos assinando os dois canais climáticos e esportivos usando o comando SUBSCRIBE:

> SUBSCRIBE weather sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "weather"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2

Em um cliente separado (outra janela do terminal, por exemplo), podemos publicar mensagens em qualquer um desses canais usando o comando PUBLISH:

> PUBLISH sports oilers/7:leafs/1
(integer) 1

O primeiro argumento é o nome do canal, o segundo é a mensagem. A mensagem pode ser qualquer, neste caso, é uma conta codificada no jogo. O comando retorna o número de clientes para os quais a mensagem será entregue. No cliente do assinante, vemos imediatamente a mensagem:

1) "message"
2) "sports"
3) "oilers/7:leafs/1"

A resposta contém três elementos: uma indicação de que esta é uma mensagem, um canal de assinatura e, de fato, uma mensagem. O cliente imediatamente após receber volta a ouvir o canal.

Voltando ao editor, podemos postar outra mensagem:

> PUBLISH weather snow/-4c
(integer) 1

No assinante, veremos o mesmo formato, mas com um canal diferente com a mensagem:

1) "message"
2) "weather"
3) "snow/-4c"

Vamos postar uma mensagem em um canal onde não há inscritos:

> PUBLISH currency CADUSD/0.787
(integer) 0

Como ninguém ouve o canal de moeda , a resposta será 0. Esta mensagem foi enviada e os clientes que posteriormente se inscreverem neste canal não receberão uma notificação sobre essa mensagem - ela foi enviada e esquecida.

Além de assinar um único canal, o Redis permite assinar canais por máscara. A máscara de estilo glob é passada para o comando PSUBSCRIBE:

> PSUBSCRIBE sports:*

O cliente irá receber mensagens de todos os canais, começando com esportes: . Em outro cliente, chame os seguintes comandos:

> PUBLISH sports:hockey oilers/7:leafs/1
(integer) 1
> PUBLISH sports:basketball raptors/33:pacers/7
(integer) 1
> PUBLISH weather:edmonton snow/-4c
(integer) 0

Observe que as duas primeiras equipes retornam 1, enquanto as últimas retornam 0. E, embora não se inscrevam diretamente nos esportes: hóquei ou esportes: basquete , o cliente recebe mensagens através de uma assinatura por máscara. Na janela cliente-assinante, podemos ver que existem resultados apenas para os canais correspondentes à máscara.

1) "pmessage"
2) "sports:*"
3) "sports:hockey"
4) "oilers/7:leafs/1"
1) "pmessage"
2) "sports:*"
3) "sports:basketball"
4) "raptors/33:pacers/7"

Essa saída é um pouco diferente da saída do comando SUBSCRIBE porque contém a própria máscara, bem como o nome real do canal.

Eventos Distribuídos


O esquema de mensagens Pub / Sub da Redis pode ser expandido para criar eventos distribuídos interessantes. Digamos que temos uma estrutura que é armazenada em uma tabela de hash, mas queremos atualizar os clientes apenas quando um único campo exceder o valor numérico definido pelo assinante. Ouviremos os canais por máscara e extrairemos o status do hash . Neste exemplo, estamos interessados ​​em update_status com os valores 5-9.

> PSUBSCRIBE update_status:[5-9]
1) "psubscribe"
2) "update_status:[5-9]"
3) (integer) 1
...

Para alterar o valor status / error_level , precisamos de dois comandos que podem ser executados sequencialmente ou no bloco MULTI / EXEC. O primeiro comando define o nível e o segundo publica uma notificação com o valor codificado no próprio canal.

> HSET status error_level 5
(integer) 1
> PUBLISH update_status:5 0
(integer) 1

Na primeira janela, vemos que a mensagem foi recebida e, depois disso, você pode mudar para outro cliente e chamar o comando HGETALL:

...
1) "pmessage"
2) "update_status:[5-9]"
3) "update_status:5"
4) "0"

> HGETALL status
1) "error_level"
2) "5"

Também podemos usar esse método para atualizar a variável local de algum processo demorado. Isso pode permitir que várias instâncias do mesmo processo troquem dados em tempo real.

Por que esse padrão é melhor do que usar Pub / Sub? Quando o processo é reiniciado, ele pode obter todo o estado e começar a ouvir. As alterações serão sincronizadas entre qualquer número de processos.

Padrões de armazenamento de dados


Existem vários padrões para armazenar dados estruturados no Redis. Neste capítulo, consideraremos o seguinte:

  • armazenamento de dados em JSON;
  • instalações de armazenamento.

Armazenamento de dados JSON


Existem várias opções para armazenar dados JSON no Redis. A forma mais comum é serializar o objeto com antecedência e salvá-lo em uma chave especial:

> SET car "{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"
OK
> GET car
"{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"

Parece simples, mas tem algumas desvantagens muito sérias:

  • a serialização requer recursos de computação do cliente para ler e escrever;
  • O formato JSON aumenta o tamanho dos dados;
  • O Redis possui apenas uma maneira indireta de manipular dados em JSON.

O primeiro par de pontos pode ser insignificante em pequenas quantidades de dados, mas os custos aumentam à medida que os dados aumentam. No entanto, o terceiro ponto é o mais crítico.

Antes do Redis 4.0, a única maneira de trabalhar com JSON dentro do Redis era usar um script Lua no módulo cjson. Isso resolveu parcialmente o problema, embora ainda permanecesse um gargalo e criou problemas adicionais com o aprendizado de Lua. Além disso, muitos aplicativos simplesmente receberam toda a cadeia JSON, desserializaram, trabalharam com os dados, serializaram novamente e os salvaram. Este é um antipadrão. Existe um grande risco de perda de dados dessa maneira.

#Instância do aplicativo # 1Instância do aplicativo # 2
1 1
> GET my-car
2[desserialize, mude a cor da máquina e serialize novamente]
> GET my-car
3
> SET my-car

[novo valor da instância # 1]
[desserialize, mude o modelo da máquina e serialize novamente]
4
> SET my-car

[novo valor da instância # 2]
5
> GET my-car

O resultado na linha 5 mostrará alterações apenas na instância 2 e a alteração de cor na instância 1 será perdida.

O Redis versão 4.0 e superior tem a capacidade de usar módulos. ReJSON é um módulo que fornece um tipo de dados e comandos especiais para interação direta com ele. ReJSON salva os dados em formato binário, o que reduz o tamanho dos dados armazenados, fornece acesso mais rápido aos elementos sem perder tempo com a des serialização.

Para usar o ReJSON, é necessário instalá-lo em um servidor Redis ou ativá-lo no Redis Enterprise.

O exemplo anterior usando ReJSON ficaria assim:

#Instância do aplicativo # 1Instância do aplicativo # 2
1 1
> JSON.SET car2 . '{"colour": "blue",  "make":"saab", "model":93,  "features": ["powerlocks",  "moonroof"]}‘
OK
2
> JSON.SET car2 colour '"red"'
OK
3
> JSON.SET car2 model '95'
OK
> JSON.GET car2 .
"{\"colour\":\"red",\"make\":\"saab\",\"model\":95,\"features\":[\"powerlocks\",\"moonroof\"]}"

O ReJSON fornece uma maneira mais segura, rápida e intuitiva de trabalhar com dados JSON no Redis, especialmente nos casos em que são necessárias alterações atômicas nos elementos aninhados.

Armazenamento de Objetos


À primeira vista, o tipo de dados padrão da “tabela de hash” Redis pode parecer muito semelhante a um objeto JSON ou outro tipo. É muito mais fácil criar campos uma sequência ou um número e impedir estruturas aninhadas. No entanto, após calcular o "caminho" para cada campo, você pode "achatar" o objeto e salvá-lo na tabela de hash Redis.

{
    "colour": "blue",
    "make": "saab",
    "model": {
        "trim": "aero",
        "name": 93
    },
    "features": ["powerlocks", "moonroof"]
}

Usando JSONPath (XPath para JSON), podemos representar cada elemento no mesmo nível da tabela de hash:

> HSET car3 colour blue
> HSET car3 make saab
> HSET car3 model.trim aero
> HSET car3 model.name 93
> HSET car3 features[0] powerlocks
> HSET car3 features[1] moonroof

Para maior clareza, os comandos são listados separadamente, mas muitos parâmetros podem ser passados ​​para o HSET.

Agora você pode solicitar o objeto inteiro ou seu campo individual:

> HGETALL car3
 1) "colour"
 2) "blue"
 3) "make"
 4) "saab"
 5) "model.trim"
 6) "aero"
 7) "model.name"
 8) "93"
 9) "features[0]"
10) "powerlocks"
11) "features[1]"
12) "moonroof"

> HGET car3 model.trim
"aero"

Embora isso forneça uma maneira rápida e útil de recuperar um objeto armazenado no Redis, ele tem suas desvantagens:

  • em diferentes idiomas e bibliotecas, a implementação do JSONPath pode ser diferente, causando incompatibilidade. Nesse caso, vale a pena serializar e desserializar os dados com uma ferramenta;
  • suporte de matriz:
    • matrizes esparsas podem ser problemáticas;
    • é impossível executar muitas operações, como inserir um elemento no meio de uma matriz.

  • Consumo desnecessário de recursos nas chaves JSONPath.

Esse padrão é praticamente o mesmo que ReJSON. Se o ReJSON estiver disponível, na maioria dos casos, é melhor usá-lo. No entanto, o armazenamento de objetos da maneira acima tem uma vantagem sobre o ReJSON: integração com a equipe Redis SORT. No entanto, esse comando é computacionalmente complexo e é um tópico complexo separado além do escopo desse padrão.

A próxima seção final cobrirá padrões de séries temporais, padrões de limite de velocidade, padrões de filtro Bloom, contadores e o uso de Lua em Redis.

PS: Tentei adaptar o texto desses artigos em inglês "bárbaro" o máximo possível para o russo, mas se você acha que em algum lugar a ideia é incompreensível ou incorreta, corrija-me nos comentários.

Source: https://habr.com/ru/post/undefined/


All Articles