eu tenho uma transação simples com nível de isolamento READ COMMITTED
A configuração da tabela é simples
create table example.parent_tbl(
id int auto_increment primary key
);
create table example.child_tbl(
id int auto_increment primary key,
parent_id int not null,
update_dt timestamp default current_timestamp ON UPDATE current_timestamp,
foreign key (parent_id) references parent_tbl(id),
unique (parent_id)
);
select
p.*
from example.parent_tbl p
where p.id not in (select parent_id from example.child_tbl)
limit 1
for update
skip locked;
depois disso, inserirei um novo registro com parent_tbl.id
into child_tbl
.
A coluna ID child_tbl
é única. Tenho vários processos em execução em paralelo e, ocasionalmente, um deles gera uma exceção de integridade, pois o mesmo ID child_tbl
já existe.
PERGUNTA
por que uma transação pode selecionar um ID que aparece em child_tbl
?
A lógica parece trivial.
Ou a transação A mantém o bloqueio de linha parent_tbl
antes da confirmação, então a transação B irá ignorá-lo.
OU
A transação A foi confirmada e a cláusula where da transação B filtrará a linha e a ignorará.
Mas a verdade é que não é bem assim!!!
O log de consulta geral mostra o comportamento contraintuitivo deste mecanismo de bloqueio
Há dois processos em execução em paralelo, fazendo a mesma coisa. Neste caso, eles são os threads 60 e 61, respectivamente.
Dois resultados são observados.
A thread 61 inseriu o ID 113 na tabela filha e liberou o bloqueio de linha. A thread 60 executa a consulta, mas não consegue detectar a linha com o ID 113 recém-inserida pela thread 61.
o thread 61 inseriu o id 113 na child_tbl e logo antes de confirmar e liberar o bloqueio de linha, outro thread 60 executa a consulta e adquire o mesmo bloqueio de linha da parent_tbl .
Adicional
Fiz os mesmos testes com o PostgreSQL e também encontrei uma exceção.
Se você observar a
EXPLAIN
saída da sua instrução, provavelmente verá a subconsultachild_tbl
em um InitPlan, enquanto o Lock Rows está no topo. Isso pode lhe dar uma pista de que a subconsulta é executada antes do bloqueio das linhas.Agora, pode acontecer que uma transação mantenha um bloqueio em uma
parent_tbl
linha e insira uma nova linha emchild_tbl
, enquanto outra transação em uma sessão diferente executa a subconsulta. Nossa primeira transação ainda não foi confirmada, então a segunda transaçãochild_tbl
ainda não pode ver essas linhas. Ela tenta bloquear a mesmaparent_tbl
linha e obtém sucesso, porque a primeira transação já foi confirmada no momento em que a segunda transação tenta bloquear a linha. Uma inserção subsequente emchild_tbl
pode então acionar uma violação de restrição.A propósito: é melhor usar
NOT EXISTS (...)
instead ofNOT IN (...)
. Primeiro, sechild_tbl
contiver umparent_id
thatIS NULL
, a consulta nunca retornará um resultado, o que provavelmente não é o que você pretende. Segundo,NOT EXISTS
pode ser executado como um anti-join com melhor desempenho.