
Hoje eu gostaria de trazer uma seção extremamente interessante, mas frequentemente coberta de segredos para os programadores mortais comuns do banco de dados (DB) - níveis de isolamento de transações. Como mostra a prática, muitas pessoas associadas à TI, em particular ao trabalho com bancos de dados, têm pouco entendimento sobre por que esses níveis são necessários e como podem ser usados para seu próprio benefício.
Pouco de teoria
As transações em si não exigem explicações especiais; uma transação é N (N≥1) consultas ao banco de dados que serão executadas com êxito todas juntas ou não. O isolamento da transação mostra quanto as transações paralelas estão afetando uma à outra.
Escolhendo um nível de transação, estamos tentando chegar a um consenso na escolha entre alta consistência de dados entre as transações e a velocidade dessas mesmas transações.
Vale ressaltar que a velocidade mais alta de execução e a menor consistência são
lidas sem confirmação . A menor velocidade de execução e maior consistência é
serializável .
Preparação do ambiente
Por exemplo, o MySQL DBMS foi selecionado. O PostgreSQL também pode ser usado, mas não suporta o nível de isolamento
não confirmado de leitura e
usa o nível de
confirmação confirmada de leitura . E, como se viu, diferentes DBMSs percebem os níveis de isolamento de maneira diferente. Eles podem ter várias nuances para garantir o isolamento, ter níveis adicionais ou não serem bem conhecidos.
Crie um ambiente usando a imagem MySQL finalizada com o Docker Hub. E preencha o banco de dados.
docker-compose.yamlversion: '3.4' services: db: image: mysql:8 environment: - MYSQL_ROOT_PASSWORD=12345 command: --init-file /init.sql volumes: - data:/var/lib/mysql - ./init.sql:/init.sql expose: - "3306" ports: - "3309:3306" volumes: data:
Preenchendo o Banco de Dados create database if not exists bank; use bank; create table if not exists accounts ( id int unsigned auto_increment primary key, login varchar(255) not null, balance bigint default 0 not null, created_at timestamp default now() ) collate=utf8mb4_unicode_ci; insert into accounts (login, balance) values ('petya', 1000); insert into accounts (login, balance) values ('vasya', 2000); insert into accounts (login, balance) values ('mark', 500);
Vamos considerar como os níveis funcionam e seus recursos.
Executaremos exemplos em 2 transações executadas simultaneamente. Condicionalmente, uma transação na janela esquerda será chamada de
transação 1 (T1), na janela direita -
transação 2 (T2).
Leitura não confirmada
O nível com a pior consistência de dados, mas a maior velocidade de transação. O nome do nível fala por si - cada transação vê alterações não confirmadas em outra transação (o fenômeno da
leitura suja ). Vamos ver como essas transações se afetam.
Etapa 1. Começamos 2 transações paralelas.
Etapa 2. Examinamos quais informações temos no início.
Etapa 3. Agora, executamos as operações INSERT, DELETE, UPDATE em T1 e vemos o que outra transação agora vê.

T2 vê dados de outra transação que ainda não foi confirmada.
Etapa 4. E o T2 pode obter alguns dados.
Etapa 5. Quando você reverter as alterações para T1, os dados recebidos por T2 serão incorretos.

Nesse nível, é impossível usar dados com base nas conclusões e decisões críticas importantes para a aplicação, pois essas conclusões podem estar longe da realidade.
Este nível pode ser usado, por exemplo, para cálculos aproximados de alguma coisa. O resultado
COUNT (*) ou
MAX (*) pode ser usado em alguns relatórios não estritos.
Outro exemplo é o modo de depuração. Quando, durante uma transação, você deseja ver o que acontece com o banco de dados.
Leitura confirmada
Para esse nível, as transações executadas simultaneamente veem apenas alterações confirmadas de outras transações. Portanto, esse nível fornece proteção contra
leitura suja .
As etapas 1 e 2 são semelhantes ao exemplo anterior.
Etapa 3. Também realizamos 3 operações simples com a tabela de contas (T1) e fazemos uma seleção completa dessas tabelas nas duas transações.

E veremos que o fenômeno da
leitura suja está ausente em T2.
Etapa 4. Corrigimos as alterações no T1 e verificamos o que o T2 agora vê.

Agora o T2 vê tudo o que o T1 fez. Esse é o chamado fenômeno da
leitura não repetida quando vemos linhas atualizadas e excluídas (UPDATE, DELETE), e o fenômeno da
leitura de fantasmas quando vemos registros adicionados (INSERT).
Leitura repetida
Um nível para evitar o fenômeno da
leitura não repetida . I.e. não vemos registros alterados e excluídos em outra transação com outra transação. Mas ainda vemos os registros inseridos de outra transação.
Ler fantasmas não vai a lugar algum.
Repita as etapas 1 e 2 novamente.
Etapa 3. No T1, executamos as consultas INSERT, UPDATE e DELETE. Depois, em T2, tentamos atualizar a mesma linha que foi atualizada em T1.

E temos bloqueio: o T2 aguardará até que o T1 confirme as alterações ou retroceda.
Etapa 4. Corrija as alterações feitas pelo T1. E leia novamente os dados da tabela de contas em T2.

Como você pode ver, os fenômenos de
leitura não repetida e
fantasmas de leitura não
são observados. Como, por padrão, a
leitura repetível nos permite impedir apenas o fenômeno da
leitura não repetida ?
De fato, o MySQL não possui o efeito de
ler fantasmas para o nível de
leitura repetível . E no PostgreSQL, eles também se livraram dele para este nível. Embora na representação clássica desse nível, devemos observar esse efeito.
Um pequeno exemplo abstrato é o serviço de geração de certificados de presente (códigos) e seu uso. Por exemplo, um invasor gerou um código de certificado para ele mesmo e tenta ativá-lo, tentando enviar várias solicitações consecutivas para ativar um cupom. Nesse caso, iniciaremos várias transações executadas simultaneamente trabalhando com o mesmo cupom. E, em algumas situações, pode ocorrer a ativação dupla ou tripla do cupom (o usuário receberá bônus 2x / 3x). Com a
leitura repetível, nesse caso, ocorrerá um bloqueio e a ativação ocorrerá uma vez e, nos 2 níveis anteriores, é possível a ativação múltipla. Um problema semelhante também pode ser resolvido usando a consulta
SELECT FOR UPDATE , que também bloqueará o registro atualizado (cupom).
Serializable
O nível em que as transações se comportam como se nada mais existisse, não há influência uma sobre a outra. Na representação clássica, esse nível elimina o efeito de
ler fantasmas .
Etapa 1. Inicie a transação.
Etapa 2. T2, lemos a tabela de contas e, em seguida, T1, tentamos atualizar os dados lidos por T2.

Temos bloqueio: não podemos alterar os dados em uma transação lida em outra.
Etapa 3. INSERT e DELETE nos levam ao bloqueio em T1.

Até que T2 termine seu trabalho, não poderemos trabalhar com os dados que ele leu. Obtemos a máxima consistência dos dados, nenhum dado extra será gravado. O preço para isso é uma velocidade de transação lenta devido a bloqueios frequentes; portanto, com uma arquitetura de aplicativo ruim, isso pode ser um truque para você.
Conclusões
Na maioria das aplicações, o nível de isolamento raramente muda e o valor padrão é usado (por exemplo, no MySQL é de
leitura repetível , no PostgreSQL é de
leitura comprometida ).
Periodicamente, porém, existem problemas em que encontrar um melhor equilíbrio entre alta consistência dos dados ou velocidade da transação pode ajudar a resolver algum problema de aplicativo.