Kata pengantar
Aplikasi kami menjalankan beberapa utas yang menjalankan DELETE
kueri secara paralel. Kueri mempengaruhi data yang terisolasi, yaitu seharusnya tidak ada kemungkinan yang DELETE
terjadi bersamaan pada baris yang sama dari utas yang terpisah. Namun, per dokumentasi MySQL menggunakan apa yang disebut kunci 'tombol selanjutnya' untuk DELETE
pernyataan, yang mengunci kunci yang cocok dan beberapa celah. Hal ini mengarah ke jalan buntu dan satu-satunya solusi yang kami temukan adalah menggunakan READ COMMITTED
tingkat isolasi.
Masalah
Masalah muncul ketika menjalankan DELETE
pernyataan kompleks dengan JOIN
s dari tabel besar. Dalam kasus tertentu, kami memiliki tabel dengan peringatan yang hanya memiliki dua baris, tetapi kueri harus menghapus semua peringatan milik beberapa entitas tertentu dari dua INNER JOIN
tabel ed yang terpisah . Kueri adalah sebagai berikut:
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
Ketika tabel day_position cukup besar (dalam kasus pengujian saya ada 1448 baris) maka transaksi apa pun bahkan dengan READ COMMITTED
mode isolasi memblokir seluruh proc_warnings
tabel.
Masalah ini selalu direproduksi pada data sampel ini - http://yadi.sk/d/QDuwBtpW1BxB9 baik di MySQL 5.1 (diperiksa pada 5.1.59) dan MySQL 5.5 (diperiksa pada MySQL 5.5.24).
EDIT: Data sampel tertaut juga berisi skema dan indeks untuk tabel kueri, direproduksi di sini untuk kenyamanan:
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`)
);
Kueri per transaksi adalah sebagai berikut:
Transaksi 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;
Transaksi 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;
Salah satunya selalu gagal dengan kesalahan 'Kunci waktu tunggu tunggu terlampaui ...'. The information_schema.innodb_trx
berisi baris berikut:
| 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' |
Seperti yang saya lihat, kedua query menginginkan X
kunci eksklusif pada baris dengan primary key = 53. Namun, tidak satu pun dari mereka harus menghapus baris dari proc_warnings
tabel. Saya hanya tidak mengerti mengapa indeks terkunci. Selain itu, indeks tidak dikunci ketika proc_warnings
tabel kosong atau day_position
tabel berisi jumlah baris yang lebih sedikit (yaitu seratus baris).
Investigasi lebih lanjut adalah untuk menyelidiki pertanyaan yang EXPLAIN
sama SELECT
. Ini menunjukkan bahwa pengoptimal kueri tidak menggunakan indeks ke proc_warnings
tabel kueri dan itulah satu-satunya alasan yang dapat saya bayangkan mengapa ia memblokir seluruh indeks kunci primer.
Kasing sederhana
Masalah juga dapat direproduksi dalam kasus yang lebih sederhana ketika hanya ada dua tabel dengan beberapa catatan, tetapi tabel anak tidak memiliki indeks pada kolom ref tabel induk.
Buat parent
tabel
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Buat child
tabel
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Isi tabel
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Tes dalam dua transaksi paralel:
Transaksi 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;
Transaksi 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;
Bagian umum dalam kedua kasus adalah MySQL tidak menggunakan indeks. Saya percaya itulah alasan kunci seluruh meja.
Solusi Kami
Satu-satunya solusi yang dapat kita lihat untuk saat ini adalah meningkatkan batas waktu tunggu kunci default dari 50 detik menjadi 500 detik untuk membiarkan utas selesai berbenah. Kemudian jagalah agar tetap bersilangan.
Setiap bantuan dihargai.
day_position
biasanya terdapat pada tabel, ketika mulai berjalan sangat lambat sehingga Anda harus menabrak batas waktu habis menjadi 500 detik? 2) Berapa lama untuk menjalankan ketika Anda hanya memiliki data sampel?Jawaban:
JAWABAN BARU (SQL dinamis gaya MySQL): Oke, yang ini menangani masalah seperti yang dijelaskan oleh salah satu poster lainnya - membalikkan urutan penguncian kunci eksklusif yang saling tidak kompatibel sehingga terlepas dari berapa banyak yang terjadi, hanya terjadi untuk jumlah waktu paling sedikit pada akhir pelaksanaan transaksi.
Ini dilakukan dengan memisahkan bagian baca dari pernyataan menjadi pernyataan pilih sendiri dan secara dinamis menghasilkan pernyataan penghapusan yang akan dipaksa untuk berjalan terakhir karena urutan tampilan pernyataan, dan yang hanya akan memengaruhi tabel proc_warnings.
Demo tersedia di sql fiddle:
Tautan ini menunjukkan skema dengan data sampel, dan kueri sederhana untuk baris yang cocok dengannya
ivehicle_id=2
. Hasil 2 baris, karena tidak ada satupun yang dihapus.Tautan ini menunjukkan skema yang sama, sampel data, tetapi meneruskan nilai 2 ke program DeleteEntries disimpan, memberitahu SP untuk menghapus
proc_warnings
entri untukivehicle_id=2
. Kueri sederhana untuk baris tidak mengembalikan hasil karena semuanya telah berhasil dihapus. Tautan demo hanya menunjukkan bahwa kode berfungsi sebagaimana dimaksudkan untuk dihapus. Pengguna dengan lingkungan pengujian yang tepat dapat mengomentari apakah ini menyelesaikan masalah utas yang diblokir.Berikut ini kodenya juga untuk kenyamanan:
Ini adalah sintaks untuk memanggil program dari dalam suatu transaksi:
JAWABAN ASLI (masih berpikir itu tidak terlalu buruk) Sepertinya 2 masalah: 1) permintaan lambat 2) perilaku penguncian tak terduga
Mengenai masalah # 1, permintaan lambat sering diselesaikan dengan dua teknik yang sama dalam penyederhanaan pernyataan kueri tandem dan penambahan atau modifikasi yang berguna untuk indeks. Anda sendiri telah membuat koneksi ke indeks - tanpa mereka optimizer tidak dapat mencari set baris terbatas untuk diproses, dan setiap baris dari setiap tabel mengalikan per baris tambahan memindai jumlah pekerjaan tambahan yang harus dilakukan.
DIREVISI SETELAH MELIHAT POS POS DAN SKEMA: Tapi saya kira Anda akan mendapatkan manfaat kinerja paling besar untuk permintaan Anda dengan memastikan Anda memiliki konfigurasi indeks yang baik. Untuk melakukannya, Anda bisa menggunakan kinerja penghapusan yang lebih baik, dan bahkan mungkin kinerja penghapusan yang lebih baik, dengan trade off dari indeks yang lebih besar dan mungkin terasa lebih lambat menyisipkan kinerja pada tabel yang sama dengan yang ditambahkan struktur indeks tambahan.
LEBIH BAIK LEBIH BAIK:
DIREVISI DI SINI TERLALU: Karena ini membutuhkan waktu selama berjalan, saya akan meninggalkan dirty_data dalam indeks, dan saya juga salah ketika memastikannya setelah ivehicle_day_id dalam urutan indeks - seharusnya itu yang pertama.
Tetapi jika saya memiliki tangan saya di atasnya, pada titik ini, karena harus ada jumlah data yang baik untuk membuatnya selama itu, saya hanya akan pergi untuk semua indeks yang meliputi hanya untuk memastikan saya mendapatkan pengindeksan terbaik yang waktu pemecahan masalah saya dapat dibeli, jika tidak ada hal lain untuk mengesampingkan bagian dari masalah tersebut.
INDEKS TERBAIK / PENUTUPAN:
Ada dua sasaran pengoptimalan kinerja yang dicari oleh dua saran perubahan terakhir:
1) Jika kunci pencarian untuk tabel yang diakses secara berturut-turut tidak sama dengan hasil kunci yang dikelompokkan yang dikembalikan untuk tabel yang saat ini diakses, kami menghilangkan apa yang seharusnya menjadi kebutuhan untuk membuat set kedua operasi pencarian-dengan-scan indeks
pada indeks berkerumun 2) Jika yang terakhir tidak terjadi, masih ada setidaknya kemungkinan bahwa pengoptimal dapat memilih algoritma bergabung yang lebih efisien karena indeks akan menjaga diperlukan kunci bergabung dalam urutan.
Kueri Anda tampak sesederhana mungkin (disalin di sini jika diedit nanti):
Kecuali tentu saja ada sesuatu tentang urutan bergabung tertulis yang memengaruhi cara pengoptimal kueri menghasilkan dalam hal ini Anda dapat mencoba beberapa saran penulisan ulang yang telah diberikan orang lain, termasuk mungkin ini dengan petunjuk indeks (opsional):
Mengenai # 2, perilaku penguncian yang tidak terduga.
Saya kira itu akan menjadi indeks yang terkunci karena baris data yang akan dikunci berada dalam indeks berkerumun, yaitu baris tunggal data itu sendiri berada di indeks.
Itu akan dikunci, karena:
1) menurut http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html
Anda juga menyebutkan di atas:
dan memberikan referensi berikut untuk itu:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed
Yang menyatakan sama dengan Anda, kecuali bahwa menurut referensi yang sama ada suatu kondisi di mana kunci harus dilepaskan:
Yang diulangi juga di halaman manual ini http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html
Jadi, kami diberitahu bahwa kondisi WHERE harus dievaluasi sebelum kunci dapat dilepaskan. Sayangnya kami tidak diberi tahu kapan kondisi WHERE dievaluasi dan mungkin akan ada sesuatu yang berubah dari satu paket ke paket lain yang dibuat oleh pengoptimal. Tapi itu memberitahu kita bahwa rilis kunci, entah bagaimana tergantung pada kinerja eksekusi permintaan, optimasi yang seperti yang kita bahas di atas tergantung pada penulisan pernyataan yang cermat, dan penggunaan indeks secara bijaksana. Ini juga dapat ditingkatkan dengan desain tabel yang lebih baik, tetapi itu mungkin akan menjadi pertanyaan terbaik.
Basis data tidak dapat mengunci catatan dalam indeks jika tidak ada.
Ini bisa berarti banyak hal seperti tetapi mungkin tidak terbatas pada: rencana eksekusi yang berbeda karena perubahan statistik, kunci yang terlalu singkat untuk diamati karena eksekusi yang jauh lebih cepat karena kumpulan data yang jauh lebih kecil / bergabung dengan operasi.
sumber
WHERE
kondisi dievaluasi ketika permintaan selesai. Bukan? Saya pikir kunci itu dilepaskan tepat setelah beberapa permintaan bersamaan dijalankan. Itu perilaku alami. Namun, ini tidak terjadi. Tak satu pun dari kueri yang disarankan di utas ini membantu menghindari penguncian indeks berkerumun diproc_warnings
tabel. Saya pikir saya akan mengajukan bug ke MySQL. Terima kasih atas bantuan Anda.Saya dapat melihat bagaimana READ_COMMITTED dapat menyebabkan situasi ini.
READ_COMMITTED memungkinkan untuk tiga hal:
Ini menciptakan paradigma internal untuk transaksi itu sendiri karena transaksi harus mempertahankan kontak dengan:
Jika dua transaksi READ_COMMITTED yang berbeda mengakses tabel / baris yang sama yang sedang diperbarui dengan cara yang sama, bersiaplah untuk berharap bukan kunci tabel, tetapi kunci eksklusif dalam gen_clust_index (alias Clustered Index) . Diberi pertanyaan dari case sederhana Anda:
Transaksi 1
Transaksi 2
Anda mengunci lokasi yang sama di gen_clust_index. Orang mungkin berkata, "tetapi setiap transaksi memiliki kunci utama yang berbeda.". Sayangnya, ini bukan kasus di mata InnoDB. Kebetulan bahwa id 1 dan id 2 berada di halaman yang sama.
Lihat kembali yang
information_schema.innodb_locks
Anda berikan dalam PertanyaanDengan pengecualian
lock_id
,,lock_trx_id
deskripsi kunci lainnya identik. Karena transaksi berada pada level bermain yang sama (isolasi transaksi yang sama), ini memang harus terjadi .Percayalah, saya telah membahas situasi semacam ini sebelumnya. Inilah posting saya yang lalu tentang ini:
Nov 05, 2012
: Bagaimana cara menganalisis status innodb pada kebuntuan dalam menyisipkan Query?Aug 08, 2011
: Apakah InnoDB Deadlock eksklusif untuk INSERT / UPDATE / DELETE?Jun 14, 2011
: Alasan terkadang permintaan lambat?Jun 08, 2011
: Apakah dua pertanyaan ini akan menghasilkan jalan buntu jika dieksekusi secara berurutan?Jun 06, 2011
: Masalah menguraikan kebuntuan dalam log status innodbsumber
Look back at information_schema.innodb_locks you supplied in the Question
)DELETE
pernyataan.Saya melihat pertanyaan dan menjelaskannya. Saya tidak yakin, tetapi punya firasat, bahwa masalahnya adalah sebagai berikut. Mari kita lihat kueri:
SELECT yang setara adalah:
Jika Anda melihat penjelasannya, Anda akan melihat bahwa rencana eksekusi dimulai dengan
proc_warnings
tabel. Itu berarti bahwa MySQL memindai kunci utama dalam tabel dan untuk setiap baris memeriksa apakah kondisinya benar, dan jika benar - baris dihapus. Artinya MySQL harus mengunci seluruh kunci primer.Yang Anda butuhkan adalah untuk membalikkan urutan GABUNG, yaitu menemukan semua id transaksi dengan
vd.ivehicle_id=16 AND dp.dirty_data=1
dan bergabung dengan mereka di atasproc_warnings
meja.Anda harus menambal salah satu indeks:
dan tulis ulang kueri penghapusan:
sumber
proc_warnings
masih terkunci. Bagaimanapun, terima kasih.Ketika Anda mengatur tingkat transaksi tanpa cara Anda melakukannya berlaku Baca Komit untuk transaksi berikutnya saja, dengan demikian (setel komit otomatis). Ini berarti setelah autocommit = 0, Anda tidak lagi di Komit Baca. Saya akan menulis seperti ini:
Anda dapat memeriksa tingkat isolasi apa yang Anda masuki dengan menanyakan
sumber
SET AUTOCOMMIT=0
harus mengatur ulang level isolasi untuk transaksi selanjutnya? Saya percaya ini memulai transaksi baru jika tidak ada yang dimulai sebelumnya (yang merupakan kasus saya). Jadi, lebih tepatnya pernyataan berikutnyaSTART TRANSACTION
atauBEGIN
tidak perlu. Tujuan saya menonaktifkan autocommit adalah membiarkan transaksi dibuka setelahDELETE
eksekusi pernyataan.