MySQL InnoDB mengunci kunci primer pada penghapusan bahkan dalam READ COMMITTED

11

Kata pengantar

Aplikasi kami menjalankan beberapa utas yang menjalankan DELETEkueri secara paralel. Kueri mempengaruhi data yang terisolasi, yaitu seharusnya tidak ada kemungkinan yang DELETEterjadi bersamaan pada baris yang sama dari utas yang terpisah. Namun, per dokumentasi MySQL menggunakan apa yang disebut kunci 'tombol selanjutnya' untuk DELETEpernyataan, yang mengunci kunci yang cocok dan beberapa celah. Hal ini mengarah ke jalan buntu dan satu-satunya solusi yang kami temukan adalah menggunakan READ COMMITTEDtingkat isolasi.

Masalah

Masalah muncul ketika menjalankan DELETEpernyataan kompleks dengan JOINs 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 JOINtabel 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 COMMITTEDmode 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_trxberisi 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 Xkunci eksklusif pada baris dengan primary key = 53. Namun, tidak satu pun dari mereka harus menghapus baris dari proc_warningstabel. Saya hanya tidak mengerti mengapa indeks terkunci. Selain itu, indeks tidak dikunci ketika proc_warningstabel kosong atau day_positiontabel berisi jumlah baris yang lebih sedikit (yaitu seratus baris).

Investigasi lebih lanjut adalah untuk menyelidiki pertanyaan yang EXPLAINsama SELECT. Ini menunjukkan bahwa pengoptimal kueri tidak menggunakan indeks ke proc_warningstabel 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 parenttabel

CREATE TABLE `parent` (
  `id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

Buat childtabel

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.

vitalidze
sumber
Saya punya pertanyaan: Apakah Anda menjalankan COMMIT di salah satu transaksi?
RolandoMySQLDBA
Tentu saja. Masalahnya adalah semua transaksi lain harus menunggu sampai salah satu dari mereka melakukan perubahan. Kasing uji sederhana tidak berisi pernyataan komit untuk menunjukkan cara mereproduksi masalah. Jika Anda menjalankan komit atau kembalikan dalam transaksi non-menunggu itu melepaskan kunci secara bersamaan dan menunggu transaksi selesai berfungsi.
vitalidze
Ketika Anda mengatakan MySQL tidak menggunakan indeks dalam kedua kasus, apakah itu karena tidak ada dalam skenario nyata? Jika ada indeks, bisakah Anda memberikan kode untuk mereka? Apakah mungkin untuk mencoba saran indeks yang diposting di bawah ini? Jika tidak ada indeks, dan tidak mungkin untuk mencoba menambahkan, maka MySQL tidak dapat membatasi kumpulan data yang diproses oleh setiap utas. Jika itu yang terjadi maka N thread hanya akan mengalikan beban kerja server dengan N kali, dan akan lebih efisien untuk membiarkan satu thread dijalankan dengan daftar parameter seperti {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
JM Hicks
Oke, ternyata indeks tersimpan di file data sampel yang ditautkan.
JM Hicks
beberapa pertanyaan lagi: 1) berapa banyak baris yang day_positionbiasanya 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?
JM Hicks

Jawaban:

3

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_warningsentri untuk ivehicle_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:

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;

Ini adalah sintaks untuk memanggil program dari dalam suatu transaksi:

CALL DeleteEntries(2);

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:

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`)
);

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:

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`)
);

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

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;

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

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 = ?
    )
);

Mengenai # 2, perilaku penguncian yang tidak terduga.

Seperti yang saya lihat, kedua query menginginkan kunci X eksklusif pada satu baris dengan primary key = 53. Namun, keduanya tidak harus menghapus baris dari tabel proc_warnings. Saya hanya tidak mengerti mengapa indeks terkunci.

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

... DELETE umumnya menetapkan kunci rekaman pada setiap catatan indeks yang dipindai dalam pemrosesan pernyataan SQL. Tidak masalah apakah ada kondisi WHERE dalam pernyataan yang akan mengecualikan baris. InnoDB tidak ingat persis kondisi WHERE, tetapi hanya tahu rentang indeks mana yang dipindai.

Anda juga menyebutkan di atas:

... bagi saya fitur utama READ COMMITTED adalah bagaimana ia berurusan dengan kunci. Itu harus melepaskan kunci indeks dari baris yang tidak cocok, tetapi tidak.

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:

Juga, kunci rekaman untuk baris yang tidak cocok dilepaskan setelah MySQL telah mengevaluasi kondisi WHERE.

Yang diulangi juga di halaman manual ini http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html

Ada juga efek lain dari menggunakan tingkat isolasi READ COMMITTED atau mengaktifkan innodb_locks_unsafe_for_binlog: Kunci rekaman untuk baris yang tidak cocok dilepaskan setelah MySQL telah mengevaluasi kondisi WHERE.

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.

Selain itu, indeks juga tidak dikunci saat tabel proc_warnings kosong

Basis data tidak dapat mengunci catatan dalam indeks jika tidak ada.

Selain itu, indeks tidak dikunci ketika ... tabel day_position berisi lebih sedikit jumlah baris (yaitu seratus baris).

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.

JM Hicks
sumber
The WHEREkondisi 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 di proc_warningstabel. Saya pikir saya akan mengajukan bug ke MySQL. Terima kasih atas bantuan Anda.
vitalidze
Saya tidak akan mengharapkan mereka untuk menghindari perilaku mengunci juga. Saya berharap untuk mengunci karena saya pikir dokumentasi mengatakan itulah yang diharapkan, apakah itu cara kita ingin memproses kueri. Saya hanya berharap bahwa menyingkirkan masalah kinerja akan menjaga permintaan bersamaan dari pemblokiran untuk waktu yang jelas (500+ detik) lama.
JM Hicks
Meskipun {WHERE} Anda sepertinya dapat digunakan selama proses penggabungan untuk membatasi baris mana yang termasuk dalam perhitungan penggabungan, saya tidak melihat bagaimana klausa {WHERE} Anda dapat dievaluasi per baris yang dikunci hingga seluruh rangkaian gabungan bergabung dihitung juga. Yang mengatakan, untuk analisis kami, saya menduga Anda benar bahwa kami harus mencurigai "Kondisi WHERE dievaluasi ketika kueri selesai". Namun itu membawa saya pada kesimpulan keseluruhan yang sama, bahwa kinerja perlu diselesaikan, dan kemudian tingkat konkurensi yang terlihat akan meningkat secara proporsional.
JM Hicks
Ingat bahwa indeks yang tepat berpotensi menghilangkan pemindaian tabel penuh yang terjadi pada tabel proc_warnings. Agar hal itu terjadi, kami membutuhkan pengoptimal kueri agar berfungsi dengan baik untuk kami, dan kami membutuhkan indeks, kueri, dan data kami untuk bekerja sama dengan baik. Nilai-nilai parameter harus mengevaluasi pada akhirnya ke baris dalam tabel target yang tidak tumpang tindih antara dua kueri. Indeks perlu menyediakan pengoptimal permintaan dengan cara untuk mencari baris-baris tersebut secara efisien. Kami membutuhkan pengoptimal untuk menyadari bahwa efisiensi pencarian potensial dan memilih rencana seperti itu.
JM Hicks
Jika semua berjalan dengan baik antara nilai parameter, indeks, hasil yang tidak tumpang tindih dalam tabel proc_warnings, dan pemilihan paket pengoptimal, meskipun kunci dapat dihasilkan selama durasi waktu yang diperlukan untuk menjalankan kueri untuk setiap utas, kunci tersebut, jika tidak tumpang tindih, tidak akan bertentangan dengan permintaan kunci dari utas lainnya.
JM Hicks
3

Saya dapat melihat bagaimana READ_COMMITTED dapat menyebabkan situasi ini.

READ_COMMITTED memungkinkan untuk tiga hal:

  • Visibilitas perubahan yang dilakukan oleh transaksi lain menggunakan READ_COMMITTED tingkat isolasi.
  • Non-Repeatable Reads: Transaksi melakukan pengambilan yang sama dengan kemungkinan mendapatkan hasil yang berbeda setiap kali.
  • Hantu: Transaksi mungkin memiliki baris muncul di tempat itu tidak terlihat sebelumnya.

Ini menciptakan paradigma internal untuk transaksi itu sendiri karena transaksi harus mempertahankan kontak dengan:

  • InnoDB Buffer Pool (sementara commit masih belum rata)
  • Kunci Utama Tabel
  • Mungkin
    • Penyangga Tulis Ganda
    • Batalkan Tablespace
  • Representasi Bergambar

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

    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;

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_locksAnda berikan dalam Pertanyaan

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

Dengan pengecualian lock_id,, lock_trx_iddeskripsi 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:

RolandoMySQLDBA
sumber
Saya telah membaca tentang hal-hal yang Anda gambarkan dalam dokumen MySQL. Tetapi bagi saya fitur utama READ COMMITTED adalah bagaimana ia berurusan dengan kunci . Itu harus melepaskan kunci indeks dari baris yang tidak cocok, tetapi tidak.
vitalidze
Jika hanya satu pernyataan SQL yang dibatalkan karena kesalahan, beberapa kunci yang ditetapkan oleh pernyataan tersebut dapat dipertahankan. Ini terjadi karena InnoDB menyimpan kunci baris dalam format yang tidak dapat diketahui sesudahnya kunci mana yang ditetapkan oleh pernyataan mana: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA
Harap dicatat bahwa saya menyebutkan kemungkinan dua baris yang ada di halaman yang sama untuk penguncian (Lihat Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA
Tentang memutar kembali pernyataan tunggal - Saya mengerti ini seolah-olah pernyataan tunggal gagal dalam satu transaksi itu mungkin masih memegang kunci. Tidak apa-apa. Pertanyaan besar saya adalah mengapa ia tidak merilis kunci baris yang tidak cocok setelah berhasil memproses DELETEpernyataan.
vitalidze
Dengan dua kunci yang lengkap, satu harus digulung kembali. Ada kemungkinan kunci terkunci. TEORI KERJA: transaksi yang memutar kembali dapat mencoba kembali dan mungkin menemukan kunci lama dari transaksi sebelumnya yang menahannya.
RolandoMySQLDBA
2

Saya melihat pertanyaan dan menjelaskannya. Saya tidak yakin, tetapi punya firasat, bahwa masalahnya adalah sebagai berikut. Mari kita lihat kueri:

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;

SELECT yang setara adalah:

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;

Jika Anda melihat penjelasannya, Anda akan melihat bahwa rencana eksekusi dimulai dengan proc_warningstabel. 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=1dan bergabung dengan mereka di atas proc_warningsmeja.

Anda harus menambal salah satu indeks:

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

dan tulis ulang kueri penghapusan:

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;
baru ditemukan
sumber
Sayangnya ini tidak membantu, yaitu baris proc_warningsmasih terkunci. Bagaimanapun, terima kasih.
vitalidze
2

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:

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;

Anda dapat memeriksa tingkat isolasi apa yang Anda masuki dengan menanyakan

SELECT @@tx_isolation;
Franck
sumber
Itu tidak benar. Mengapa SET AUTOCOMMIT=0harus 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 berikutnya START TRANSACTIONatau BEGINtidak perlu. Tujuan saya menonaktifkan autocommit adalah membiarkan transaksi dibuka setelah DELETEeksekusi pernyataan.
vitalidze
1
@SqlKiwi ini adalah cara untuk mengedit posting ini, dan inilah yang mengomentari ;-)
jcolebrand