O MySQL InnoDB bloqueia a chave primária na exclusão, mesmo em READ COMMITTED

11

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.

vitalidze
fonte
Tenho uma pergunta: você executou COMMIT em alguma das transações?
RolandoMySQLDBA
Claro. O problema é que todas as outras transações devem esperar até que uma delas confirme as alterações. O caso de teste simples não contém uma declaração de confirmação para mostrar como reproduzir o problema. Se você executar commit ou rollback em uma transação que não espera, ele libera o bloqueio simultaneamente e a transação em espera conclui seu trabalho.
vitalidze
Quando você diz que o MySQL não usa índices em nenhum dos casos, é porque não há nenhum no cenário real? Se houver índices, você poderia fornecer o código para eles? É possível tentar alguma das sugestões de índice postadas abaixo? Se não houver índices e não for possível tentar adicionar nenhum, o MySQL não poderá restringir o conjunto de dados processados ​​por cada thread. Se esse for o caso, os N threads simplesmente multiplicarão a carga de trabalho do servidor por N vezes, e seria mais eficiente permitir que um thread seja executado com uma lista de parâmetros como {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
JM Hicks
Ok, localizei os índices no arquivo de dados de amostra vinculado.
JM Hicks
mais algumas perguntas: 1) quantas linhas a 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?
JM Hicks

Respostas:

3

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_warningsentradas ivehicle_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:

CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN

    SELECT @idstring:= '';
    SELECT @idnum:= 0;
    SELECT @del_stmt:= '';

    SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
    FROM proc_warnings
    WHERE EXISTS (
        SELECT 0
        FROM day_position
        WHERE day_position.transaction_id = proc_warnings.transaction_id
        AND day_position.dirty_data = 1
        AND EXISTS (
            SELECT 0
            FROM ivehicle_days
            WHERE ivehicle_days.id = day_position.ivehicle_day_id
            AND ivehicle_days.ivehicle_id = input_vid
        )
    )
    ORDER BY idnum_col DESC
    LIMIT 1;

    IF (@idnum > 0) THEN
        SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');

        PREPARE del_stmt_hndl FROM @del_stmt;
        EXECUTE del_stmt_hndl;
        DEALLOCATE PREPARE del_stmt_hndl;
    END IF;
END;

Esta é a sintaxe para chamar o programa de dentro de uma transação:

CALL DeleteEntries(2);

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:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)

) ;


CREATE TABLE  `ivehicle_days` (
    ...,
    KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);

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:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;

CREATE TABLE  `ivehicle_days` (
    ...,
    UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);

CREATE TABLE  `proc_warnings` (

    .., /*rename primary key*/
    CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
    UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);

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):

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;

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):

DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
    SELECT 0
    FROM day_position
    FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)  
    WHERE day_position.transaction_id = proc_warnings.transaction_id
    AND day_position.dirty_data = 1
    AND EXISTS (
        SELECT 0
        FROM ivehicle_days
        FORCE INDEX (`ivehicle_days__vid_id_cvrng`)  
        WHERE ivehicle_days.id = day_position.ivehicle_day_id
        AND ivehicle_days.ivehicle_id = ?
    )
);

Em relação ao item 2, comportamento inesperado de travamento.

Como posso ver, as duas consultas desejam um bloqueio X exclusivo em uma linha com chave primária = 53. No entanto, nenhuma delas deve excluir as linhas da tabela proc_warnings. Só não entendo por que o índice está bloqueado.

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

... um DELETE geralmente define bloqueios de registro em todos os registros de índice que são varridos no processamento da instrução SQL. Não importa se existem condições WHERE na instrução que excluiriam a linha. O InnoDB não se lembra da condição exata WHERE, mas apenas sabe quais intervalos de índice foram verificados.

Você também mencionou acima:

... quanto a mim, a principal característica do READ COMMITTED é como ele lida com bloqueios. Ele deve liberar os bloqueios de índice de linhas não correspondentes, mas não o faz.

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:

Além disso, bloqueios de registro para linhas não correspondentes são liberados após o MySQL avaliar a condição WHERE.

O que também é reiterado nesta página de manual http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html

Também existem outros efeitos do uso do nível de isolamento READ COMMITTED ou da ativação do innodb_locks_unsafe_for_binlog: Bloqueios de registro para linhas não correspondentes são liberados após o MySQL avaliar a condição WHERE.

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.

Além disso, o índice não está bloqueado quando a tabela proc_warnings está vazia

O banco de dados não pode bloquear registros no índice se não houver nenhum.

Além disso, o índice não está bloqueado quando ... a tabela day_position contém menos número de linhas (ou seja, cem linhas).

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.

JM Hicks
fonte
A WHEREcondiçã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 na proc_warningstabela. Eu acho que vou registrar um bug no MySQL. Obrigado pela ajuda.
vitalidze
Eu também não esperaria que eles evitassem o comportamento de bloqueio. Eu esperaria que ele bloqueie, porque acho que a documentação diz que é o esperado, independentemente de ser dessa maneira que queremos que ela processe a consulta. Eu esperaria que se livrar do problema de desempenho impediria que a consulta simultânea bloqueasse por um período obviamente tão longo (mais de 500 segundos).
JM Hicks
Embora seu {WHERE} pareça que ele possa ser usado durante o processo de junção para restringir quais linhas estão incluídas no cálculo da junção, não vejo como sua cláusula {WHERE} poderia ser avaliada por linha bloqueada até que todo o conjunto de junções seja computado também. Dito isto, para nossa análise, suspeito que você esteja certo de que devemos suspeitar "A condição WHERE é avaliada quando a consulta é concluída". No entanto, isso me leva à mesma conclusão geral de que o desempenho precisa ser resolvido e o grau aparente de simultaneidade aumentará proporcionalmente.
JM Hicks
Lembre-se de que os índices adequados podem potencialmente eliminar qualquer verificação de tabela completa que ocorre na tabela proc_warnings. Para que isso aconteça, precisamos que o otimizador de consultas funcione bem para nós e que nossos índices, consultas e dados sejam úteis para trabalhar com ele. Os valores dos parâmetros precisam avaliar no final as linhas na tabela de destino que não se sobrepõem entre as duas consultas. Os índices precisam fornecer ao otimizador de consulta um meio de pesquisar com eficiência essas linhas. Precisamos do otimizador para perceber essa potencial eficiência de pesquisa e selecionar esse plano.
JM Hicks
Se tudo correr bem entre valores de parâmetro, índices, resultados não sobrepostos na tabela proc_warnings e seleção do plano do otimizador, mesmo que bloqueios possam ser gerados pelo tempo necessário para executar a consulta para cada encadeamento, esses bloqueios, se não houver sobreposição, não entrará em conflito com as solicitações de bloqueio dos outros threads.
JM Hicks
3

Eu posso ver como READ_COMMITTED pode causar essa situação.

READ_COMMITTED permite três coisas:

  • Visibilidade de alterações confirmadas por outras transações usando o nível de isolamento READ_COMMITTED .
  • Leituras Não Repetíveis: Transação executando a mesma recuperação com a possibilidade de obter um resultado diferente a cada vez.
  • Fantasmas: as transações podem exibir linhas onde não eram visíveis anteriormente.

Isso cria um paradigma interno para a própria transação porque a transação deve manter contato com:

  • Buffer Pool do InnoDB (enquanto a confirmação ainda não foi liberada)
  • Chave primária da tabela
  • Possivelmente
    • o buffer de gravação dupla
    • Desfazer espaço de tabela
  • Representação pictórica

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

    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;

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_locksvocê forneceu na pergunta

| 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' |

Com exceção de lock_id, lock_trx_ido 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:

RolandoMySQLDBA
fonte
Eu li sobre as coisas que você descreve nos documentos do MySQL. Mas, para mim, a principal característica do READ COMMITTED é como ele lida com bloqueios . Ele deve liberar os bloqueios de índice de linhas não correspondentes, mas não o faz.
181312 vitalidze
Se apenas uma única instrução SQL for revertida como resultado de um erro, alguns dos bloqueios definidos pela instrução poderão ser preservados. Isso acontece porque o InnoDB armazena bloqueios de linhas em um formato que não pode saber posteriormente qual bloqueio foi definido por qual instrução: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA
Observe que mencionei a possibilidade de duas linhas existirem na mesma página para o bloqueio (Veja Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA 13/12/12 /
Sobre a reversão da instrução única - entendo isso como se a instrução única falhar em uma única transação, ela ainda pode conter os bloqueios. Isso está ok. Minha grande pergunta é por que ele não libera bloqueios de linha não correspondentes após o processamento da DELETEinstrução com êxito .
vitalidze
Com dois bloqueios completos, um deve ser revertido. É possível que os bloqueios permaneçam. TEORIA DE TRABALHO: a transação que retrocedeu pode tentar novamente e pode encontrar um bloqueio antigo da transação anterior que o mantinha.
RolandoMySQLDBA
2

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:

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;

O SELECT equivalente é:

SELECT pw.id
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=16 AND dp.dirty_data=1;

Se você olhar para a explicação, verá que o plano de execução começa com a proc_warningstabela. 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=1e juntá-los na proc_warningstabela.

Ou seja, você precisará corrigir um dos índices:

ALTER TABLE `day_position`
 DROP INDEX `day_position__id`,
 ADD INDEX `day_position__id`
   USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);

e reescreva a consulta de exclusão:

DELETE pw
FROM (
  SELECT DISTINCT dp.transaction_id
  FROM ivehicle_days vd
  JOIN day_position dp ON dp.ivehicle_day_id = vd.id
  WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;
newtover
fonte
Infelizmente, isso não ajuda, ou seja, as linhas proc_warningsainda ficam bloqueadas. Obrigado mesmo assim.
vitalidze
2

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:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
    p.id = c.parent_id
WHERE p.id = 1;

Você pode verificar em que nível de isolamento está consultando

SELECT @@tx_isolation;
Franck
fonte
Isso não é verdade. Por que o SET AUTOCOMMIT=0deve 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óximo START TRANSACTIONou BEGINdeclaração não é necessário. Meu objetivo de desativar a confirmação automática é deixar a transação aberta após a DELETEexecução da instrução.
18712 vitalitze
1
@SqlKiwi este era o caminho para editar este post, e este foi o único a comentar ;-)
jcolebrand