Pesquisar este blog

sábado, 5 de setembro de 2020

Banco de dados e suas "Chaves"

O título deste post começa com uma brincadeira, com chaves não queria dizer dos relacionamentos e chaves primárias coisas que banco de dados SQL fazem muito bem, aqui vamos conversar a respeito das travas (trancas ou cadeados) que estes bancos possuem para lhe dar com o paralelismo das transações.

Banco de dados Relacionais RDBMS (Relationship Database Management System) são bancos consistentes e este é um fator importante em como ele trabalha com os dados.

Introdução:

Aqui no blog, existem alguns posts falando sobre banco de dados SQL, como este que mostra como controlar transações nos banco de dados usando Spring e Java. Um banco de dados é composto de um servidor, onde podemos criar conexões para executar operações.

Estes bancos organizam os dados em tabelas, com schema definido e isso significa que antes de inserir dados, você precisa criar a estrutura por trás dos dados. Existem uma série de organizações possíveis para os dados, como banco de dados, schemas, tabelas, chaves e relacionamentos e cada um deles possui um nível de abstração.

Existem várias teorias de como armazenar os dados e como organizar/dividir os dados em cada um destas interfaces. Geralmente, os banco de dados são únicos, no começo do desenvolvimento do sistema, já que geralmente não temos muitas tabelas. A partir do momento em que existirem muitas tabelas, estes bancos podem ser divididos e eles geralmente agrupam dados de entidades, sistemas, ou departamentos de uma empresa. É muito comum existir um banco de dados de cliente, com as tabelas dos dados dos clientes. A forma como cada lugar organiza os dados está mais relacionado com como é a organização dentro de uma empresa, do que com alguma teoria em si.

Distribuição e organização dos dados

O importante é saber que, nas tabelas, é preciso setar os nomes de cada coluna, e as propriedades de cada uma delas, como qual é o tipo daquele campo, como numérico, texto e etc. Uma outra propriedade que pode ser setada é, se aquele campo pode ou não ser nulo, se ele é uma chave, o que significa que ele deve conter valores únicos naquela tabela. Isto já embute no banco uma certa inflexibilidade nestes bancos, pois não será possível inserir dados, com uma estrutura diferente da pré definida. Por outro lado, isto é muito bom, pois os dados inseridos ali já estão, de certa maneira, pré formatados em linhas e colunas.

Programando no seu banco de dados:

De uma maneira mais avançada, é possível adicionar funções, no seu banco de dados, que serão executadas a partir de uma operação. Aqui é possível fazer coisas simples como, verificar se o valor é nulo e substituí-lo para um valor padrão, caso o seja. Até coisas mais complexas como realizar cálculos e disparar alterações, ou inserções de dados em outras tabelas de acordo com a sua lógica. Isso pode ser feito tanto através de Triggers (gatilhos), quanto em Stored Procedures (procedimentos pré compilados). Algumas pessoas amam stored procedures, pois elas, por estarem no banco, executam bem rápido, mas até hoje não conheço um banco de dados que permite uma administração limpa e fácil dos códigos e do versionamento dos códigos que estão sendo executado nestas funções e isso se torna uma bela desvantagem quando estas ferramentas são usadas muito intensivamente, podendo provocar reações do banco inesperadas ou não mapeadas, por simplesmente desconhecer o que acontecerá com o dado após entrar no ali.

Conexões/Sessões:

Ao se conectar a um banco de dados, abre-se uma sessão de comunicação com ele, nesta sessão será possível executar comandos no banco. Este comandos incluem desde criação dos componentes do banco, até a inserção/modificação dos dados.

Qualquer  tipo de conexão realizada com o banco de dados, será feita através de uma sessão.

Agora imagine que dois usuários estão conectados ao banco de dados e cada um deles tenta alterar um mesmo valor ao mesmo tempo, devido a consistência forte, dos dados de um banco SQL isso não será possível e estes dados deverão ser alterados um após o outro. Pros usuários até pode parecer que a operação foi feita em paralelo, mas na verdade elas aconteceram em sequência.

Mas o que são estas travas ou cadeados de um banco SQL?

Internamente, quando uma transação de alteração de um dado é iniciada , o banco de dados trava aquele dado. Ou seja, o acesso aquele dado fica travado, até a transação completar, ou ser abortada, com isso não é possível que nenhuma outra operação de alteração seja realizada naqueles dados, enquanto aquela transação estiver aberta.

Assim, as alterações que, supostamente são em paralelo, na verdade rodaram em sequência, mas devido a velocidade dos processadores, a impressão é que ela aconteceu em paralelo. Para mostrar o impacto de uma alteração em uma linha, fiz um código para demonstrar isso, e este código está aqui neste repositório, em um código bem simples. Antes de executá-lo lembre-se de criar a estrutura de dados e inserir dados na tabela, o que pode ser feito executando os comandos deste arquivo.

O código mostrado aqui é bem simples, existem três classes, sendo duas principais, onde podemos executar alterações no banco de dados. Neste projeto foi feito usando o banco de dados MariaDb, e aqui tem um post que ensina a instalá-lo em sua máquina, de maneira bem simples.

No projeto, será mostrado qual é o impacto de ter uma alteração de linha, sendo realizada no exato momento em que um select é feito. Para fazer isso o código executa o mesmo select diversas vezes, e o mesmo update diversas vezes também. Aqui não era a intenção demonstrar isso em um ambiente complexo, a tabela utilizada como teste tem apenas 5 linhas, mesmo assim, já é possível perceber uma diminuição da performance em 30%, com apenas duas Threads executando.

Caso de Exemplo

Os selects rodam 12000 vezes, e os updates 1000 vezes, e estes números foram feitos, baseando-se na performance da minha máquina, sem mais nada executando em paralelo. O ideal é fazer estes testes sem mais nada executando na sua máquina, assim conseguimos isolar a execução do código, sem alguma interferência externa. Sabemos que um sistema operacional roda diversas coisas ao mesmo tempo, e isso pode gerar cargas de processamento de tempos em tempos, e assim interferir na execução do nosso teste. Executar os selects 12000 vezes é interessante para evitar que alguns fatores influenciem na medida da performance, quando mais vezes executarmos os selects, menos interferência a média de tempo geral sofrerá de alguns corner cases, que são casos em que a query demora muito mais, ou muito menos tempo, para ser executada.

Para executar este teste, de maneira mais honesta possível, usei a mesma classe, para executar os updates e os selects, quando fazia os selects, eu simplesmente commentei esta linha, que é a responsável por executar os updates. Repare que para assegurar que os selects e os updates estão rodando em paralelo, foram usadas duas Threads, e aqui existe um antigo post ensinando como fazer isso no java. A criação de uma Thread, quase não impacta na performance de um programa Java, ela só executará, quando o método start for chamado. 

Como resultado temos os seguintes números:

Média de tempo update108052,25
Média de tempo Select79167,75
Differença %26,73%
Média de acesso ao dado com update2066
Média de acesso ao dado com select1391,75
Differença %32,64%

 Foram levantadas duas métricas, o tempo, em nanosegundos, que uma query demora para executar, e depois da query, quanto tempo demora para um valor ser acessado. Repare que, nos dois casos houve perda de performance quando os updates são executados em paralelo. No geral, podemos assumir que o banco ficou 30% mais lento, quando um valor estava sendo alterado, enquanto os selects rodavam.

Isso em um banco simples, com poucas linhas e tabelas, agora imagine isso em um ambiente empresarial com diversas tabelas/sistemas acessando os dados de uma vez. Com o tempo, conforme os sistemas vão crescendo, isso acaba impactando na performance do banco, até que chega uma hora em que o banco, não suporta mais a demanda daquela empresa, ai nasce a necessidade de haver outros sistemas de armazenamento para complementarem o banco de dados tradicional.

Tipos de travas

Até agora vimos apenas os locks (travas) das transações, existem vários outros tipos, incluindo travas exclusivas, travas distribuídas, DML locks e locks de backup ou recuperação de dados. No exemplo também só foi citado a trava de uma linha, mas ele também pode acontecer em outros níveis como:

  • Tabelas
  • Linhas
  • Bloco de dados
  • Itens cacheados
  • Conexões
  • Sistema Todo
O que vimos aqui foi a trava em uma transação, mas isso pode gerar um impacto muito maior, quando a transação continua fazendo outras operações, e isso mantem a trava naquela linha alterada, quando ela fica esperando alguma operação e isso é muito perigoso, já que temos uma linha travada e isso pode gerar complicações no sistema.
É natural que as travas sejam pequenas, no começo, mas vão aumentando quanto maior for a quantidade de acessos aquele banco de dados e a concorrência aumenta. Com isso, coisas que inicialmente eram uma trava de uma linha, acabam virando uma trava de blocos de dados, depois de tabelas, até chegar ao sistema todo.

Existem diversas configurações, e ações dos desenvolvedores que podem aliviar, ou até acabar com as travas mais graves dos bancos, mas isso foge ao escopo deste post e não será mostrado aqui.

Apresentação

Nenhum comentário:

Postar um comentário