As consultas SQL acabam com a escalabilidade do site

Você já projetou um sistema que permanece ativo mesmo quando o banco de dados cai?

Eu tenho.

O número de pessoas que estão fazendo fila para me criticar neste post provavelmente se parece com o início da Maratona de Boston. Mas é verdade. Consultas e junções são ruins para o desempenho do seu site. Assim como conectar-se a um banco de dados em primeiro lugar.

Provavelmente, podemos todos concordar que os sites mais rápidos que você pode escrever não usariam bancos de dados.

Ninguém nunca disse “Acho que vou adicionar algumas consultas de banco de dados a esta página para acelerar.”

Assumindo um site típico baseado em db, quase sempre é o db que faz com que o site trave quando há um pico de carga. Se você pudesse remover todas as conexões externas de sua camada de aplicativo (bancos de dados, pools de cache externos, etc.), você realmente poderia escalar horizontalmente a um grau ilimitado, porque não há subsistemas de bloqueio impedindo você de construir páginas. Você só precisa acender quantos servidores web forem necessários para lidar com sua carga.

Você está sempre ciente de quais páginas estão fazendo conexões db? Em caso afirmativo, você sabe o que as consultas de banco de dados estão fazendo? Provavelmente não, devido ao fato de que geralmente não há indicadores padrão claros durante o desenvolvimento indicando a utilização do banco de dados. Por exemplo, em PHP, uma estratégia comum de armazenamento de sessão é o banco de dados. Quando ativado, o PHP se conecta automaticamente ao banco de dados para executar uma combinação de consultas SELECT, INSERT ou UPDATE em cada carregamento de página. Você não notaria isso a menos que monitore o log de consulta. Muitos criadores de perfil nem mesmo pegariam essas consultas, já que a conexão é gerenciada por uma conexão db diferente daquela que o resto do site usa.

Uma abordagem melhor aqui seria não usar o banco de dados para gerenciamento de sessão. Hm ..

“Mas meus usuários precisam se registrar. É claro que preciso escrever registros de banco de dados para que as chaves de registro geradas não colidam com outros registros.”

Acho que a maioria dos desenvolvedores pensa assim porque essa é a boa e velha maneira padrão de pensar. Afinal, como seu novo registro obteria uma identificação se o banco de dados estivesse fora do ar? Você precisa de algum tipo de gerador de chave exclusivo sem colisão.

Mas e se você acabou de criar as chaves em seu código? Entre na boa e velha função UUID (ou qualquer função altamente exclusiva para esse assunto. Sim, UUIDs são maiores do que INTs, mas é o preço que você paga pela durabilidade). Cada uma das minhas tabelas sempre tem um valor UUID e é por esse UUID que me refiro às minhas informações persistentes. Nunca escrevo sites que se referem a objetos por meio de teclas numéricas como este:

GET / users / show / 1

Evitar o banco de dados significa que você pode continuar aceitando registros de usuários, mesmo se o banco de dados estiver inativo.

Grave os dados em algum outro back-end mais robusto e escalonável, como um arquivo local e / ou cache e / ou fila. Contanto que você eventualmente grave os dados no banco de dados mais tarde, você está pronto para prosseguir. Se você gosta de chaves numéricas primárias de banco de dados, ainda pode deixar que o banco de dados as defina mais tarde. Você não precisa deles porque você codificou seus registros com UUIDs.

“Mas na próxima página, eu precisaria SELECIONAR o usuário do sistema que acabou de ser criado.”

Assim como um ORM faz a recuperação de dados relacionais para você, não é tão difícil escrever algo que recupere os dados armazenados para você. Por exemplo, em PHP, algum pseudocódigo pode ter a seguinte aparência:

$ userModel = $ userService-> getByUuid ($ uuid);

Se o banco de dados estiver inativo, o que esse getter pode fazer para recuperar o objeto de usuário que escrevemos na última etapa?

Se, durante a operação de gravação, escrevêssemos os dados do usuário no cache (entre outros back-ends), eles seriam localizáveis, mesmo se o banco de dados estivesse inativo . A melhor cópia disponível está no cache! Não nos importamos como o serviço obtém a melhor versão disponível deste registro, apenas sabemos que ele a obtém para nós da maneira mais rápida possível. Essa mágica durável de acesso a dados é encapsulada em um serviço que se parece e funciona como muitos outros sistemas que você já viu.

Esse padrão é a premissa de um sistema de acesso a dados que uma vez ajudei a projetar e que chamei de ” Camada de Dados “. O funcionamento da camada de dados será assunto para outra postagem.

Voltando ao SQL, qual é o meu problema com o JOIN’s

  • JOIN’s é como dizer a um garçom para receber vários pedidos de uma vez. Faz parte do trabalho deles, mas se você tentar dar mil pedidos ao garçom, tudo ao mesmo tempo, o garçom fará um péssimo trabalho.

Para lidar com isso, os aplicativos usam um padrão de cache de resultado de consulta. Se o seu aplicativo tem 1.000 consultas diferentes, você pode ter centenas de cópias em cache diferentes dos resultados da consulta.

No entanto, quando você tem muitos resultados de consulta em cache diferentes, qualquer alteração nos dados subjacentes fará com que todas as consultas que armazenaram em cache os dados alterados se tornem obsoletas.

  • Conforme a carga aumenta, o JOIN causa contenção de bloqueio e deadlocks. Quando você escala e, eventualmente, começa a obter contenção de bloqueio e deadlocks de consultas que está fazendo em tempo de execução, não há quase nada que você possa fazer além de reprojetar seu código.

Cache de resultados de consulta SQL está bem. Qual é esse problema de cache de que você fala?

Digamos que temos um site de filmes que tem, entre outras coisas, uma página de detalhes do filme e também outra página que mostra uma lista dos filmes favoritos dos anos 80 de um usuário.

No início da minha carreira, ao codificar a página de filmes favoritos, eu teria sugerido que você unisse os usuários a filmes favoritos do usuário e novamente a filmes em que o ID do usuário é o cara da lista que você está vendo. “Dessa forma, você obtém tudo o que deseja em um resultado de banco de dados e pode até armazenar em cache o resultado da consulta.”

Passei a odiar essa abordagem.

Por quê? Porque provavelmente você tem outras páginas no site com consultas diferentes que se juntam à tabela de filmes também. Essas consultas também têm seus caches de resultados de consulta com sua própria cópia privada das informações do banco de dados.

O que acontece quando as informações subjacentes sobre um filme mudam?

Neste cenário, você tem três opções básicas:

  • Não faça nada e deixe seu site veicular diferentes informações inconsistentes sobre seus filmes de página em página até que o TTL expire. Essa opção é horrível.
  • Tente remover todas as consultas em cache que possam se referir a esse filme. Essa opção também é horrível porque em um local movimentado, você precisa de uma camada de cache hidratada ou as coisas podem parar.
  • Escreva o aplicativo de maneira diferente, sem junções e com cache inteligente de nível de registro.

Na minha opinião, a melhor solução é aquela sem JOIN’s, e também com pouca ou nenhuma conexão de banco de dados, e também onde os dados estão atualizados.

A API de pseudocódigo PHP seria assim:

$ userModel = $ userService-> getByUuid ($ uuid);

$ favoriteMoviesCollection = $ userService-> getUserFavoriteMovies ($ userModel);

O código acima deve ser bem padrão. Exceto que agora você deve começar a imaginar como e por que está funcionando sem JOINs e conexões de banco de dados.

Sob o capô, getUserFavoriteMovies () está puxando uma lista de IDs em cache que foi pré-aquecida da última vez que Johnny atualizou seus filmes favoritos. Essa lista de IDs é então iterada, uma de cada vez, puxando cada registro de filme do sistema elástico de acesso a dados apoiado pelo memcache. Com essa abordagem, o banco de dados não foi usado e todos os registros são armazenados em cache e todos os dados são atuais.

Além disso, esse padrão permite que um site permaneça ativo mesmo quando o banco de dados cair.

O melhor de tudo é que esse padrão de acessar registros individualmente também suporta o shard de um banco de dados onde, como em um sistema baseado em JOIN, um servidor MySQL não pode se juntar a registros que estão em outro shard.

Por último, o uso de um padrão de acesso a dados no estilo CRUD de registro individual permite que você integre mais perfeitamente outros back-ends de forma que a lógica do aplicativo não se importe se o registro do filme que você puxou foi armazenado em MongoDB, MySQL, arquivos ou algo mais. Você seria capaz de usar qualquer tipo de armazenamento de back-end porque não escreveu seu código para depender de um recurso JOIN que só é compatível com um RDBMS.

Esse é o fim do artigo. Se você realmente gosta de escrever consultas, tente meu desafio de codificação sql .