Przedmowa
Nasza aplikacja uruchamia kilka wątków, które wykonują DELETE
zapytania równolegle. Zapytania wpływają na izolowane dane, tzn. Nie powinno być możliwości, aby współbieżne DELETE
wystąpiły w tych samych wierszach z oddzielnych wątków. Jednak w dokumentacji MySQL używa tak zwanej blokady następnego klucza dla DELETE
instrukcji, która blokuje zarówno pasujący klucz, jak i pewną lukę. To prowadzi do martwych blokad, a jedynym rozwiązaniem, które znaleźliśmy, jest zastosowanie READ COMMITTED
poziomu izolacji.
Problem
Problem pojawia się przy wykonywaniu złożonych DELETE
instrukcji JOIN
zs ogromnych tabel. W konkretnym przypadku mamy tabelę z ostrzeżeniami, która ma tylko dwa wiersze, ale zapytanie musi usunąć wszystkie ostrzeżenia, które należą do niektórych konkretnych podmiotów z dwóch oddzielnych INNER JOIN
tabel ed. Zapytanie jest następujące:
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
Gdy tabela day_position jest wystarczająco duża (w moim przypadku testowym jest 1448 wierszy), wówczas każda transakcja, nawet w READ COMMITTED
trybie izolacji, blokuje cały proc_warnings
tabelę.
Problem jest zawsze odtwarzany na tych przykładowych danych - http://yadi.sk/d/QDuwBtpW1BxB9 zarówno w MySQL 5.1 (sprawdzony w 5.1.59), jak i MySQL 5.5 (sprawdzony w MySQL 5.5.24).
EDYCJA: Połączone przykładowe dane zawierają również schemat i indeksy dla tabel zapytań, odtworzone tutaj dla wygody:
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`)
);
Zapytania na transakcje są następujące:
Transakcja 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;
Transakcja 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;
Jedna z nich zawsze kończy się niepowodzeniem z błędem „Przekroczono limit czasu oczekiwania blokady ...”. information_schema.innodb_trx
Zawiera następujące wiersze:
| 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' |
Jak widzę oba zapytania chcą wyłącznej X
blokady wiersza z kluczem podstawowym = 53. Jednak żadne z nich nie musi usuwać wierszy z proc_warnings
tabeli. Po prostu nie rozumiem, dlaczego indeks jest zablokowany. Ponadto indeks nie jest blokowany, gdy proc_warnings
tabela jest pusta lub day_position
tabela zawiera mniejszą liczbę wierszy (tj. Sto wierszy).
Dalsze dochodzenie dotyczyło EXPLAIN
podobnego SELECT
zapytania. Pokazuje, że optymalizator zapytań nie używa indeksu do zapytania do proc_warnings
tabeli i to jedyny powód, dla którego mogę sobie wyobrazić, dlaczego blokuje cały indeks klucza podstawowego.
Uproszczona obudowa
Problem można również odtworzyć w prostszym przypadku, gdy istnieją tylko dwie tabele z kilkoma rekordami, ale tabela podrzędna nie ma indeksu w kolumnie odniesienia tabeli nadrzędnej.
Utwórz parent
tabelę
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Utwórz child
tabelę
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Wypełnij tabele
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Testuj w dwóch równoległych transakcjach:
Transakcja 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;
Transakcja 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;
Wspólną częścią w obu przypadkach jest to, że MySQL nie używa indeksów. Uważam, że to jest powód zablokowania całego stołu.
Nasze rozwiązanie
Jedynym rozwiązaniem, które możemy teraz zobaczyć, jest zwiększenie domyślnego limitu czasu oczekiwania na blokadę z 50 sekund do 500 sekund, aby umożliwić zakończenie czyszczenia nici. Następnie trzymajcie kciuki.
Każda pomoc doceniona.
day_position
zwykle zawiera tabela, gdy zaczyna ona działać tak wolno, że musisz podnieść limit czasu do 500 sekund? 2) Jak długo trwa uruchomienie, gdy masz tylko przykładowe dane?