Prefácio
Nosso aplicativo executa vários threads que executam DELETE
consultas em paralelo. As consultas afetam dados isolados, ou seja, não deve haver possibilidade de ocorrência simultânea DELETE
nas mesmas linhas de threads separados. No entanto, por documentação, o MySQL usa o chamado bloqueio 'next-key' para DELETE
instruções, que bloqueiam tanto a chave correspondente quanto alguma lacuna. Isso leva a impasses e a única solução que encontramos é usar o READ COMMITTED
nível de isolamento.
O problema
Problema surge ao executar DELETE
instruções complexas com JOIN
s de tabelas enormes. Em um caso específico, temos uma tabela com avisos que possui apenas duas linhas, mas a consulta precisa eliminar todos os avisos que pertencem a algumas entidades em particular de duas INNER JOIN
tabelas separadas . A consulta é a seguinte:
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=? AND dp.dirty_data=1
Quando a tabela day_position é grande o suficiente (no meu caso de teste, existem 1448 linhas), qualquer transação, mesmo com o READ COMMITTED
modo de isolamento, bloqueia a proc_warnings
tabela inteira .
O problema é sempre reproduzido nesses dados de amostra - http://yadi.sk/d/QDuwBtpW1BxB9, tanto no MySQL 5.1 (verificado em 5.1.59) quanto no MySQL 5.5 (verificado no MySQL 5.5.24).
EDIT: Os dados de amostra vinculados também contêm esquema e índices para as tabelas de consulta, reproduzidas aqui por conveniência:
CREATE TABLE `proc_warnings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`warning` varchar(2048) NOT NULL,
PRIMARY KEY (`id`),
KEY `proc_warnings__transaction` (`transaction_id`)
);
CREATE TABLE `day_position` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_day_id` int(10) unsigned DEFAULT NULL,
`dirty_data` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `day_position__trans` (`transaction_id`),
KEY `day_position__is` (`ivehicle_day_id`,`sort_index`),
KEY `day_position__id` (`ivehicle_day_id`,`dirty_data`)
) ;
CREATE TABLE `ivehicle_days` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`d` date DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ivehicle_days__is` (`ivehicle_id`,`sort_index`),
KEY `ivehicle_days__d` (`d`)
);
As consultas por transação são as seguintes:
Transação 1
set transaction isolation level read committed; set autocommit=0; begin; DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
Transação 2
set transaction isolation level read committed; set autocommit=0; begin; DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1;
Um deles sempre falha com o erro 'Bloqueio do tempo de espera excedido ...'. O information_schema.innodb_trx
contém as seguintes linhas:
| trx_id | trx_state | trx_started | trx_requested_lock_id | trx_wait_started | trx_wait | trx_mysql_thread_id | trx_query |
| '1A2973A4' | 'LOCK WAIT' | '2012-12-12 20:03:25' | '1A2973A4:0:3172298:2' | '2012-12-12 20:03:25' | '2' | '3089' | 'DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1' |
| '1A296F67' | 'RUNNING' | '2012-12-12 19:58:02' | NULL | NULL | '7' | '3087' | NULL |
information_schema.innodb_locks
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
Como posso ver, as duas consultas querem um X
bloqueio exclusivo em uma linha com chave primária = 53. No entanto, nenhuma delas deve excluir as linhas da proc_warnings
tabela. Só não entendo por que o índice está bloqueado. Além disso, o índice não é bloqueado quando a proc_warnings
tabela está vazia ou a day_position
tabela contém menos número de linhas (ou seja, cem linhas).
Uma investigação mais aprofundada foi executada EXPLAIN
sobre a SELECT
consulta semelhante . Ele mostra que o otimizador de consulta não usa o índice para consultar a proc_warnings
tabela e é a única razão pela qual posso imaginar por que ele bloqueia todo o índice da chave primária.
Caso simplificado
O problema também pode ser reproduzido em um caso mais simples, quando há apenas duas tabelas com alguns registros, mas a tabela filho não possui um índice na coluna ref da tabela pai.
Criar parent
tabela
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Criar child
tabela
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Preencher tabelas
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Teste em duas transações paralelas:
Transação 1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET AUTOCOMMIT=0; BEGIN; DELETE c FROM child c INNER JOIN parent p ON p.id = c.parent_id WHERE p.id = 1;
Transação 2
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET AUTOCOMMIT=0; BEGIN; DELETE c FROM child c INNER JOIN parent p ON p.id = c.parent_id WHERE p.id = 2;
A parte comum em ambos os casos é que o MySQL não usa índices. Acredito que esse seja o motivo do bloqueio de toda a tabela.
Nossa solução
A única solução que podemos ver no momento é aumentar o tempo limite de espera de bloqueio padrão de 50 segundos para 500 segundos para permitir que o encadeamento termine de limpar. Então mantenha os dedos cruzados.
Qualquer ajuda apreciada.
day_position
tabela normalmente contém, quando começa a ficar tão lenta que você precisa aumentar o tempo limite para 500 segundos? 2) Quanto tempo leva para executar quando você possui apenas os dados de amostra?Respostas:
NOVA RESPOSTA (SQL dinâmico no estilo MySQL): Ok, este aborda o problema da maneira como um dos outros pôsteres é descrito - revertendo a ordem na qual os bloqueios exclusivos incompatíveis entre si são adquiridos, de modo que, independentemente de quantos ocorram, eles ocorrem apenas por a menor quantidade de tempo no final da execução da transação.
Isso é feito separando a parte de leitura da instrução em sua própria instrução select e gerando dinamicamente uma instrução de exclusão que será forçada a executar por último devido à ordem de aparência da instrução e que afetará apenas a tabela proc_warnings.
Uma demonstração está disponível no sql fiddle:
Este link mostra o esquema com dados de amostra e uma consulta simples para as linhas correspondentes
ivehicle_id=2
. Resultado de 2 linhas, pois nenhuma delas foi excluída.Esse link mostra o mesmo esquema, dados de amostra, mas passa um valor 2 para o programa armazenado DeleteEntries, informando ao SP para excluir
proc_warnings
entradasivehicle_id=2
. A consulta simples para linhas não retorna resultados, pois todos foram excluídos com êxito. Os links de demonstração demonstram apenas que o código funciona conforme a intenção de excluir. O usuário com o ambiente de teste adequado pode comentar se isso resolve o problema do encadeamento bloqueado.Aqui está o código também por conveniência:
Esta é a sintaxe para chamar o programa de dentro de uma transação:
RESPOSTA ORIGINAL (ainda acho que não é muito ruim) Parece com 2 problemas: 1) consulta lenta 2) comportamento inesperado de bloqueio
No que diz respeito ao problema nº 1, as consultas lentas geralmente são resolvidas pelas mesmas duas técnicas na simplificação de instruções de consulta em conjunto e em adições ou modificações úteis nos índices. Você mesmo já fez a conexão com os índices - sem eles, o otimizador não pode procurar um conjunto limitado de linhas para processar, e cada linha de cada tabela que se multiplica por linha extra verifica a quantidade de trabalho extra que deve ser feito.
REVISADO APÓS VER POSTO DE ESQUEMA E ÍNDICES: Mas imagino que você obtenha o maior benefício de desempenho para sua consulta, garantindo uma boa configuração de índice. Para fazer isso, é possível obter melhor desempenho de exclusão e, possivelmente, ainda melhor, com troca de índices maiores e desempenho de inserção talvez notavelmente mais lento nas mesmas tabelas nas quais a estrutura de índice adicional é adicionada.
UM POUCO MELHOR:
REVISADO AQUI TAMBÉM: Como leva o tempo necessário para executar, eu deixaria o dirty_data no índice e errei também com certeza ao colocá-lo após o ivehicle_day_id na ordem do índice - deve ser o primeiro.
Mas se eu tivesse minhas mãos nisso, a essa altura, já que deve haver uma boa quantidade de dados para levar tanto tempo, eu iria apenas para todos os índices de cobertura apenas para garantir que eu estivesse obtendo a melhor indexação possível. meu tempo de solução de problemas poderia comprar, se nada mais excluir essa parte do problema.
MELHORES / ÍNDICES DE COBERTURA:
Existem duas metas de otimização de desempenho buscadas pelas duas últimas sugestões de alteração:
1) Se as chaves de pesquisa para tabelas acessadas sucessivamente não forem iguais aos resultados da chave em cluster retornados para a tabela acessada no momento, eliminamos o que seria necessário fazer um segundo conjunto de operações de busca de índice com varredura no índice em cluster
2) Se não for o caso, ainda há pelo menos a possibilidade de o otimizador selecionar um algoritmo de junção mais eficiente, pois os índices manterão o chaves de junção necessárias na ordem classificada.
Sua consulta parece tão simplificada quanto possível (copiada aqui, caso seja editada posteriormente):
A menos que haja algo na ordem de junção por escrito que afete a maneira como o otimizador de consulta prossegue; nesse caso, você pode tentar algumas das sugestões de reescrita fornecidas por outras pessoas, incluindo talvez esta com dicas de índice (opcional):
Em relação ao item 2, comportamento inesperado de travamento.
Eu acho que seria o índice bloqueado porque a linha de dados a ser bloqueada está em um índice clusterizado, ou seja, a única linha de dados reside no índice.
Ele estaria bloqueado porque:
1) de acordo com http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html
Você também mencionou acima:
e forneceu a seguinte referência para isso:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed
Que afirma o mesmo que você, exceto que, de acordo com a mesma referência, existe uma condição na qual uma fechadura deve ser liberada:
O que também é reiterado nesta página de manual http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html
Portanto, somos informados de que a condição WHERE deve ser avaliada antes que o bloqueio possa ser liberado. Infelizmente, não somos informados quando a condição WHERE é avaliada e provavelmente algo está sujeito a alterações de um plano para outro criado pelo otimizador. Mas ele nos diz que a liberação do bloqueio depende de alguma forma do desempenho da execução da consulta, cuja otimização, como discutimos acima, depende da escrita cuidadosa da declaração e do uso criterioso dos índices. Também pode ser melhorado com um melhor design da tabela, mas isso provavelmente seria melhor para uma pergunta separada.
O banco de dados não pode bloquear registros no índice se não houver nenhum.
Isso pode significar várias coisas, como provavelmente não se limitando a: um plano de execução diferente devido a uma alteração nas estatísticas, um bloqueio muito breve para ser observado devido a uma execução muito mais rápida devido a um conjunto de dados muito menor / operação de junção.
fonte
WHERE
condição é avaliada quando a consulta é concluída. Não é? Eu pensei que o bloqueio é liberado logo após a execução de algumas consultas simultâneas. Esse é o comportamento natural. No entanto, isso não acontece. Nenhuma das consultas sugeridas neste encadeamento ajuda a evitar o bloqueio de índice em cluster naproc_warnings
tabela. Eu acho que vou registrar um bug no MySQL. Obrigado pela ajuda.Eu posso ver como READ_COMMITTED pode causar essa situação.
READ_COMMITTED permite três coisas:
Isso cria um paradigma interno para a própria transação porque a transação deve manter contato com:
Se duas transações READ_COMMITTED distintas estiverem acessando as mesmas tabelas / linhas que estão sendo atualizadas da mesma maneira, esteja pronto para esperar não um bloqueio de tabela, mas um bloqueio exclusivo dentro do gen_clust_index (também conhecido como Índice de Cluster) . Dadas as consultas do seu caso simplificado:
Transação 1
Transação 2
Você está bloqueando o mesmo local no gen_clust_index. Pode-se dizer, "mas cada transação possui uma chave primária diferente". Infelizmente, este não é o caso aos olhos do InnoDB. Acontece que os códigos 1 e 2 residem na mesma página.
Revise o que
information_schema.innodb_locks
você forneceu na perguntaCom exceção de
lock_id
,lock_trx_id
o restante da descrição do bloqueio é idêntico. Como as transações estão no mesmo campo de jogo (mesmo isolamento de transação), isso deve realmente acontecer .Acredite, eu já falei sobre esse tipo de situação antes. Aqui estão meus posts anteriores sobre isso:
Nov 05, 2012
: Como analisar o status do innodb no deadlock na inserção de consulta?Aug 08, 2011
: Os InnoDB Deadlocks são exclusivos para INSERT / UPDATE / DELETE?Jun 14, 2011
: Motivos para consultas lentas ocasionalmente?Jun 08, 2011
: Essas duas consultas resultarão em um impasse se executadas em sequência?Jun 06, 2011
: Problemas ao decifrar um deadlock em um log de status do innodbfonte
Look back at information_schema.innodb_locks you supplied in the Question
)DELETE
instrução com êxito .Eu olhei para a consulta e a explicação. Não tenho certeza, mas tenho um pressentimento de que o problema é o seguinte. Vamos dar uma olhada na consulta:
O SELECT equivalente é:
Se você olhar para a explicação, verá que o plano de execução começa com a
proc_warnings
tabela. Isso significa que o MySQL verifica a chave primária na tabela e para cada linha verifica se a condição é verdadeira e se é - a linha é excluída. Ou seja, o MySQL precisa bloquear toda a chave primária.O que você precisa é inverter a ordem JOIN, ou seja, encontrar todos os IDs de transação
vd.ivehicle_id=16 AND dp.dirty_data=1
e juntá-los naproc_warnings
tabela.Ou seja, você precisará corrigir um dos índices:
e reescreva a consulta de exclusão:
fonte
proc_warnings
ainda ficam bloqueadas. Obrigado mesmo assim.Quando você define o nível de transação sem o modo que o faz, aplica a Leitura confirmada apenas à próxima transação, portanto (defina a confirmação automática). Isso significa que após a confirmação automática = 0, você não está mais na leitura confirmada. Eu escreveria assim:
Você pode verificar em que nível de isolamento está consultando
fonte
SET AUTOCOMMIT=0
deve redefinir o nível de isolamento para a próxima transação? Acredito que inicia uma nova transação se nenhuma foi iniciada antes (que é o meu caso). Portanto, para ser mais preciso, o próximoSTART TRANSACTION
ouBEGIN
declaração não é necessário. Meu objetivo de desativar a confirmação automática é deixar a transação aberta após aDELETE
execução da instrução.