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?