Bagaimana menghindari kebuntuan mysql 'ditemukan ketika mencoba untuk mendapatkan kunci; coba mulai ulang transaksi '

286

Saya memiliki tabel innoDB yang mencatat pengguna online. Itu diperbarui pada setiap halaman refresh oleh pengguna untuk melacak halaman mana mereka berada dan tanggal akses terakhir mereka ke situs. Saya kemudian memiliki cron yang berjalan setiap 15 menit untuk HAPUS catatan lama.

Saya mendapat 'Deadlock ditemukan ketika mencoba untuk mendapatkan kunci; coba mulai kembali transaksi 'selama sekitar 5 menit semalam dan tampaknya ketika menjalankan INSERT ke dalam tabel ini. Adakah yang bisa menyarankan cara menghindari kesalahan ini?

=== EDIT ===

Berikut ini pertanyaan yang sedang berjalan:

Kunjungan Pertama ke situs:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

Di setiap halaman segarkan:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron setiap 15 menit:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Ini kemudian menghitung jumlah untuk mencatat beberapa statistik (yaitu: anggota online, pengunjung online).

David
sumber
Bisakah Anda memberikan lebih banyak detail tentang struktur tabel? Apakah ada indeks berkerumun atau nonclustered?
Anders Abel
13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - Menjalankan "show engine innodb status" akan memberikan diagnostik yang bermanfaat.
Martin
Ini bukan praktik yang baik untuk menulis database sinkron ketika pengguna menginjak halaman. cara yang tepat untuk melakukannya adalah menyimpannya dalam memori seperti memcache atau antrian cepat dan menulis ke db dengan cron.
Nir

Jawaban:

292

Salah satu trik mudah yang dapat membantu sebagian besar deadlock adalah menyortir operasi dalam urutan tertentu.

Anda mendapatkan jalan buntu ketika dua transaksi mencoba untuk mengunci dua kunci pada pesanan yang berlawanan, yaitu:

  • koneksi 1: kunci kunci (1), kunci kunci (2);
  • koneksi 2: kunci kunci (2), kunci kunci (1);

Jika keduanya berjalan secara bersamaan, koneksi 1 akan mengunci kunci (1), koneksi 2 akan mengunci kunci (2) dan setiap koneksi akan menunggu yang lain untuk melepaskan kunci -> deadlock.

Sekarang, jika Anda mengubah kueri Anda sehingga koneksi akan mengunci kunci pada urutan yang sama, yaitu:

  • koneksi 1: kunci kunci (1), kunci kunci (2);
  • koneksi 2: kunci kunci ( 1 ), kunci kunci ( 2 );

tidak mungkin mendapatkan jalan buntu.

Jadi ini yang saya sarankan:

  1. Pastikan Anda tidak memiliki pertanyaan lain yang mengunci akses lebih dari satu kunci sekaligus kecuali untuk pernyataan hapus. jika Anda melakukannya (dan saya curiga Anda melakukannya), pesan WHERE mereka di (k1, k2, .. kn) dalam urutan menaik.

  2. Perbaiki pernyataan hapus Anda agar berfungsi dalam urutan menaik:

Perubahan

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Untuk

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Satu hal yang perlu diingat adalah bahwa dokumentasi mysql menyarankan bahwa jika terjadi kebuntuan, klien harus mencoba ulang secara otomatis. Anda dapat menambahkan logika ini ke kode klien Anda. (Katakan, 3 ulangi kesalahan khusus ini sebelum menyerah).

Omry Yadan
sumber
2
Jika saya memiliki Transaksi (autocommit = false), pengecualian kebuntuan dilemparkan. Apakah cukup dengan mencoba kembali statement.executeUpdate () yang sama atau seluruh transaksi sekarang dilimpahkan dan harus dikembalikan + jalankan kembali semua yang berjalan di dalamnya?
Siapa
5
jika transaksi Anda diaktifkan, semuanya atau tidak sama sekali. jika Anda memiliki pengecualian dalam bentuk apa pun, itu dijamin bahwa seluruh transaksi tidak berpengaruh. dalam hal ini Anda ingin memulai kembali semuanya.
Omry Yadan
4
Penghapusan
3
Terima kasih banyak, sobat. Kiat 'semacam pernyataan' memperbaiki masalah kunci mati saya.
Miere
4
@OmryYadan Dari yang saya tahu, di MySQL Anda tidak dapat memilih dalam subquery dari tabel yang sama di mana Anda membuat UPDATE. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe
72

Kebuntuan terjadi ketika dua transaksi saling menunggu untuk mendapatkan kunci. Contoh:

  • Tx 1: kunci A, lalu B
  • Tx 2: kunci B, lalu A

Ada banyak pertanyaan dan jawaban tentang kebuntuan. Setiap kali Anda memasukkan / memperbarui / atau menghapus baris, kunci diperoleh. Untuk menghindari kebuntuan, Anda harus memastikan bahwa transaksi bersamaan tidak memperbarui baris dalam urutan yang dapat menyebabkan kebuntuan. Secara umum, cobalah untuk mendapatkan kunci selalu dalam urutan yang sama bahkan dalam transaksi yang berbeda (misalnya selalu tabel A dulu, lalu tabel B).

Alasan lain untuk kebuntuan dalam basis data adalah indeks yang hilang . Ketika sebuah baris dimasukkan / perbarui / hapus, basis data perlu memeriksa kendala relasional, yaitu, pastikan hubungan konsisten. Untuk melakukannya, database perlu memeriksa kunci asing di tabel terkait. Ini mungkin menghasilkan kunci lain yang diperoleh dari baris yang dimodifikasi. Pastikan untuk selalu memiliki indeks pada kunci asing (dan tentu saja kunci utama), jika tidak maka akan menghasilkan kunci tabel, bukan kunci baris . Jika kunci meja terjadi, pertikaian kunci lebih tinggi dan kemungkinan kebuntuan meningkat.

ewernli
sumber
3
Jadi mungkin masalah saya adalah bahwa Pengguna telah me-refresh halaman dan dengan demikian memicu pembaruan data pada saat yang sama cron mencoba menjalankan DELETE pada catatan. Namun, saya mendapatkan kesalahan pada INSERTS, jadi cron tidak akan MENGHAPUS catatan yang baru saja dibuat. Jadi bagaimana kebuntuan dapat terjadi pada catatan yang belum dimasukkan?
David
Bisakah Anda memberikan sedikit lebih banyak informasi tentang tabel dan apa yang sebenarnya dilakukan transaksi?
ewernli
Saya tidak melihat bagaimana kebuntuan dapat terjadi jika hanya ada satu pernyataan per transaksi. Tidak ada operasi lain di tabel lain? Tidak ada kunci asing khusus atau batasan unik? Tidak ada kendala penghapusan kaskade?
ewernli
tidak, tidak ada yang istimewa ... Saya kira ini adalah sifat dari penggunaan tabel. baris sedang dimasukkan / perbarui setiap penyegaran halaman dari pengunjung. Sekitar 1000+ pengunjung aktif pada satu waktu.
David
12

Kemungkinan bahwa pernyataan penghapusan akan memengaruhi sebagian besar dari total baris dalam tabel. Akhirnya ini mungkin menyebabkan kunci meja diperoleh saat menghapus. Berpegang pada kunci (dalam hal ini kunci baris atau halaman) dan mendapatkan lebih banyak kunci selalu merupakan risiko kebuntuan. Namun saya tidak bisa menjelaskan mengapa pernyataan penyisipan mengarah ke eskalasi kunci - mungkin ada hubungannya dengan halaman splitting / menambahkan, tetapi seseorang yang mengetahui MySQL lebih baik harus mengisi di sana.

Sebagai permulaan, ada baiknya mencoba memperoleh kunci meja segera untuk pernyataan penghapusan. Lihat LOCK TABLES dan masalah penguncian tabel .

Anders Abel
sumber
6

Anda mungkin mencoba menjalankan deletepekerjaan itu dengan terlebih dahulu memasukkan kunci dari setiap baris untuk dihapus ke tabel temp seperti kodesemu ini

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

Memecahnya seperti ini kurang efisien tetapi menghindari kebutuhan untuk memegang kunci-kunci selama delete.

Juga, modifikasi selectkueri Anda untuk menambahkan whereklausa tidak termasuk baris yang lebih tua dari 900 detik. Ini menghindari ketergantungan pada pekerjaan cron dan memungkinkan Anda menjadwal ulang untuk menjalankan lebih jarang.

Teori tentang kebuntuan: Saya tidak memiliki banyak latar belakang di MySQL tapi begini ... Saya deleteakan memegang kunci-rentang kunci untuk datetime, untuk mencegah agar baris yang cocok dengan whereklausulnya tidak ditambahkan di tengah transaksi , dan ketika menemukan baris untuk menghapusnya akan berusaha mendapatkan kunci pada setiap halaman yang dimodifikasinya. The insertakan mendapatkan kunci pada halaman itu memasukkan ke dalam, dan kemudian berusaha untuk memperoleh kunci tombol. Biasanya insertakan menunggu dengan sabar untuk kunci kunci untuk membuka tetapi ini akan menemui jalan buntu jika deletemencoba untuk mengunci halaman yang sama yang insertdigunakan karena deletekebutuhan kunci halaman dan insertkebutuhan kunci kunci. Ini sepertinya tidak tepat untuk memasukkan, deletedaninsert menggunakan rentang datetime yang tidak tumpang tindih jadi mungkin sesuatu yang lain sedang terjadi.

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html

Brian Sandlin
sumber
4

Jika seseorang masih berjuang dengan masalah ini:

Saya menghadapi masalah serupa di mana 2 permintaan mengenai server pada saat bersamaan. Tidak ada situasi seperti di bawah ini:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Jadi, saya bingung mengapa kebuntuan terjadi.

Kemudian saya menemukan bahwa ada hubungan anak induk pengiriman antara 2 tabel karena kunci asing. Ketika saya memasukkan catatan di tabel anak, transaksi memperoleh kunci di baris tabel induk. Segera setelah itu saya mencoba untuk memperbarui baris induk yang memicu peningkatan kunci ke satu EKSKLUSIF. Karena transaksi bersamaan kedua sudah memegang kunci SHARED, itu menyebabkan kebuntuan.

Merujuk ke: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html

obrolan
sumber
Dalam kasus saya juga, sepertinya masalahnya adalah hubungan kunci asing. Thanks1
Chris Prince
3

Untuk programmer Java yang menggunakan Spring, saya menghindari masalah ini menggunakan aspek AOP yang secara otomatis mencoba kembali transaksi yang mengalami kebuntuan sementara.

Lihat @RetryTransaction Javadoc untuk info lebih lanjut.

Archie
sumber
0

Saya punya metode, bagian dalamnya dibungkus dengan MySqlTransaction.

Masalah kebuntuan muncul untuk saya ketika saya menjalankan metode yang sama secara paralel dengan dirinya sendiri.

Tidak ada masalah menjalankan instance dari metode ini.

Ketika saya menghapus MySqlTransaction, saya dapat menjalankan metode secara paralel dengan dirinya sendiri tanpa masalah.

Hanya berbagi pengalaman, saya tidak menganjurkan apa pun.

CINCHAPPS
sumber
0

cronberbahaya. Jika salah satu instance cron gagal selesai sebelum yang berikutnya jatuh tempo, mereka cenderung saling bertarung.

Akan lebih baik untuk memiliki pekerjaan yang terus berjalan yang akan menghapus beberapa baris, tidur beberapa, lalu ulangi.

Juga, INDEX(datetime)sangat penting untuk menghindari kebuntuan.

Tapi, jika tes datetime mencakup lebih dari, katakanlah, 20% dari tabel, DELETEakan melakukan pemindaian tabel. Potongan kecil yang dihapus lebih sering adalah solusi.

Alasan lain untuk menggunakan potongan yang lebih kecil adalah untuk mengunci lebih sedikit baris.

Intinya:

  • INDEX(datetime)
  • Tugas yang terus berjalan - hapus, tidur sebentar, ulangi.
  • Untuk memastikan bahwa tugas di atas belum mati, miliki tugas cron yang tujuan utamanya adalah memulai kembali setelah kegagalan.

Teknik penghapusan lainnya: http://mysql.rjweb.org/doc.php/deletebig

Rick James
sumber