Prefácio
Nosso aplicativo executa vários threads que executam DELETEconsultas em paralelo. As consultas afetam dados isolados, ou seja, não deve haver possibilidade de ocorrência simultânea DELETEnas mesmas linhas de threads separados. No entanto, por documentação, o MySQL usa o chamado bloqueio 'next-key' para DELETEinstruções, que bloqueiam tanto a chave correspondente quanto alguma lacuna. Isso leva a impasses e a única solução que encontramos é usar o READ COMMITTEDnível de isolamento.
O problema
Problema surge ao executar DELETEinstruções complexas com JOINs 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 JOINtabelas 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 COMMITTEDmodo 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_trxconté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 Xbloqueio exclusivo em uma linha com chave primária = 53. No entanto, nenhuma delas deve excluir as linhas da proc_warningstabela. Só não entendo por que o índice está bloqueado. Além disso, o índice não é bloqueado quando a proc_warningstabela está vazia ou a day_positiontabela contém menos número de linhas (ou seja, cem linhas).
Uma investigação mais aprofundada foi executada EXPLAINsobre a SELECTconsulta semelhante . Ele mostra que o otimizador de consulta não usa o índice para consultar a proc_warningstabela 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 parenttabela
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Criar childtabela
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_positiontabela 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?