Mengunci masalah dengan DELETE / INSERT bersamaan di PostgreSQL

35

Ini cukup sederhana, tapi saya bingung dengan apa yang PG lakukan (v9.0). Kami mulai dengan tabel sederhana:

CREATE TABLE test (id INT PRIMARY KEY);

dan beberapa baris:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

Menggunakan alat kueri JDBC favorit saya (ExecuteQuery), saya menghubungkan dua jendela sesi ke db tempat tabel ini tinggal. Keduanya bersifat transaksional (yaitu, auto-commit = false). Sebut saja S1 dan S2.

Bit kode yang sama untuk masing-masing:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

Sekarang, jalankan ini dalam gerakan lambat, jalankan satu per satu di jendela.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

Sekarang, ini berfungsi dengan baik di SQLServer. Ketika S2 melakukan penghapusan, ia melaporkan 1 baris dihapus. Dan kemudian sisipan S2 berfungsi dengan baik.

Saya menduga PostgreSQL mengunci indeks pada tabel di mana baris itu ada, sedangkan SQLServer mengunci nilai kunci aktual.

Apakah saya benar? Apakah ini bisa berfungsi?

DaveyBob
sumber

Jawaban:

39

Mat dan Erwin keduanya benar, dan saya hanya menambahkan jawaban lain untuk memperluas apa yang mereka katakan dengan cara yang tidak sesuai dengan komentar. Karena jawaban mereka tampaknya tidak memuaskan semua orang, dan ada saran bahwa pengembang PostgreSQL harus dikonsultasikan, dan saya adalah satu, saya akan menguraikan.

Poin penting di sini adalah bahwa di bawah standar SQL, dalam transaksi yang berjalan di READ COMMITTEDtingkat isolasi transaksi, batasannya adalah bahwa pekerjaan transaksi yang tidak dikomit tidak boleh terlihat. Ketika pekerjaan transaksi yang dilakukan menjadi terlihat tergantung pada implementasi. Apa yang Anda tunjukkan adalah perbedaan dalam bagaimana dua produk telah memilih untuk mengimplementasikannya. Tidak ada implementasi yang melanggar persyaratan standar.

Inilah yang terjadi di dalam PostgreSQL, secara terperinci:

S1-1 berjalan (1 baris dihapus)

Baris lama dibiarkan di tempat, karena S1 mungkin masih memutar kembali, tetapi S1 sekarang memegang kunci pada baris sehingga sesi lain yang mencoba mengubah baris akan menunggu untuk melihat apakah S1 berkomitmen atau mundur. Setiap bacaan tabel masih dapat melihat baris lama, kecuali jika mereka berusaha menguncinya dengan SELECT FOR UPDATEatau SELECT FOR SHARE.

Berjalan S2-1 (tetapi diblokir karena S1 memiliki kunci tulis)

S2 sekarang harus menunggu untuk melihat hasil S1. Jika S1 mundur daripada komit, S2 akan menghapus baris. Perhatikan bahwa jika S1 memasukkan versi baru sebelum memutar kembali, versi baru tidak akan pernah ada dari perspektif transaksi lain, juga versi lama tidak akan dihapus dari perspektif transaksi lainnya.

S1-2 berjalan (1 baris disisipkan)

Baris ini tidak tergantung pada yang lama. Jika ada pembaruan baris dengan id = 1, versi lama dan baru akan terkait, dan S2 dapat menghapus versi terbaru dari baris ketika menjadi tidak terblokir. Bahwa baris baru kebetulan memiliki nilai yang sama dengan beberapa baris yang ada di masa lalu tidak menjadikannya sama dengan versi terbaru dari baris itu.

S1-3 berjalan, melepaskan kunci tulis

Jadi perubahan S1 tetap ada. Satu baris hilang. Satu baris telah ditambahkan.

Berjalan S2-1, sekarang bisa mendapatkan kunci. Tetapi laporan 0 baris dihapus. HAH???

Apa yang terjadi secara internal, adalah bahwa ada penunjuk dari satu versi baris ke versi berikutnya dari baris yang sama jika diperbarui. Jika baris dihapus, tidak ada versi berikutnya. Ketika READ COMMITTEDtransaksi terbangun dari blok pada konflik tulis, itu mengikuti rantai pembaruan itu sampai akhir; jika baris belum dihapus dan jika masih memenuhi kriteria pemilihan kueri itu akan diproses. Baris ini telah dihapus, sehingga kueri S2 melanjutkan.

S2 mungkin atau mungkin tidak sampai ke baris baru selama pemindaian tabel. Jika ya, ia akan melihat bahwa baris baru dibuat setelah DELETEpernyataan S2 dimulai, dan juga bukan bagian dari rangkaian baris yang terlihat.

Jika PostgreSQL me-restart seluruh pernyataan DELETE S2 dari awal dengan snapshot baru, itu akan berperilaku sama dengan SQL Server. Komunitas PostgreSQL tidak memilih untuk melakukan itu karena alasan kinerja. Dalam kasus sederhana ini Anda tidak akan pernah melihat perbedaan dalam kinerja, tetapi jika Anda sepuluh juta baris menjadi DELETEketika Anda diblokir, Anda pasti akan melihatnya. Ada trade-off di sini di mana PostgreSQL telah memilih kinerja, karena versi yang lebih cepat masih memenuhi persyaratan standar.

Berjalan S2-2, melaporkan pelanggaran batasan kunci yang unik

Tentu saja, barisnya sudah ada. Ini adalah bagian paling tidak mengejutkan dari gambar ini.

Meskipun ada beberapa perilaku mengejutkan di sini, semuanya sesuai dengan standar SQL dan dalam batas-batas apa yang "khusus implementasi" sesuai dengan standar. Tentu dapat mengejutkan jika Anda mengasumsikan bahwa beberapa perilaku implementasi lain akan hadir di semua implementasi, tetapi PostgreSQL berusaha sangat keras untuk menghindari kegagalan serialisasi di READ COMMITTEDtingkat isolasi, dan memungkinkan beberapa perilaku yang berbeda dari produk lain untuk mencapai itu.

Sekarang, secara pribadi saya bukan penggemar READ COMMITTEDtingkat isolasi transaksi dalam implementasi produk apa pun . Mereka semua memungkinkan kondisi ras untuk menciptakan perilaku yang mengejutkan dari sudut pandang transaksional. Begitu seseorang menjadi terbiasa dengan perilaku aneh yang diizinkan oleh satu produk, mereka cenderung menganggap itu "normal" dan pengorbanan yang dipilih oleh produk lain aneh. Tetapi setiap produk harus membuat semacam trade-off untuk mode apa pun yang tidak benar-benar diterapkan SERIALIZABLE. Di mana pengembang PostgreSQL telah memilih untuk menarik garis READ COMMITTEDadalah untuk meminimalkan pemblokiran (membaca jangan memblokir menulis dan menulis jangan memblokir membaca) dan untuk meminimalkan kemungkinan kegagalan serialisasi.

Standar ini mensyaratkan bahwa SERIALIZABLEtransaksi menjadi default, tetapi sebagian besar produk tidak melakukan itu karena hal itu menyebabkan kinerja lebih tinggi dari tingkat isolasi transaksi yang lebih longgar. Beberapa produk bahkan tidak memberikan transaksi yang benar-benar serial ketika SERIALIZABLEdipilih - terutama Oracle dan versi PostgreSQL sebelum 9.1. Tetapi menggunakan SERIALIZABLEtransaksi yang sebenarnya adalah satu-satunya cara untuk menghindari efek mengejutkan dari kondisi balapan, dan SERIALIZABLEtransaksi selalu harus diblokir untuk menghindari kondisi balapan atau memutar kembali beberapa transaksi untuk menghindari kondisi balapan yang berkembang. Implementasi SERIALIZABLEtransaksi yang paling umum adalah Strict Two-Phase Locking (S2PL) yang memiliki kegagalan pemblokiran dan serialisasi (dalam bentuk deadlock).

Pengungkapan penuh: Saya bekerja dengan Dan Ports dari MIT untuk menambahkan transaksi yang benar-benar serial ke PostgreSQL versi 9.1 menggunakan teknik baru yang disebut Serializable Snapshot Isolasi.

kgrittn
sumber
Saya ingin tahu apakah cara yang benar-benar murah (murahan?) Untuk membuat pekerjaan ini adalah dengan mengeluarkan dua DELET yang diikuti oleh INSERT. Dalam pengujian terbatas saya (2 utas), itu bekerja OK, tetapi perlu menguji lebih banyak untuk melihat apakah itu akan berlaku untuk banyak utas.
DaveyBob
Selama Anda menggunakan READ COMMITTEDtransaksi, Anda memiliki kondisi balapan: apa yang akan terjadi jika transaksi lain menyisipkan baris baru setelah yang pertama DELETEdimulai dan sebelum yang kedua DELETEdimulai? Dengan transaksi yang tidak seketat SERIALIZABLEdua cara utama untuk menutup kondisi balapan adalah melalui promosi konflik (tapi itu tidak membantu ketika baris dihapus) dan terwujudnya konflik. Anda bisa mematerialisasi konflik dengan memiliki tabel "id" yang diperbarui untuk setiap baris yang dihapus, atau dengan mengunci tabel secara eksplisit. Atau gunakan coba lagi pada kesalahan.
kgrittn
Coba lagi itu. Terima kasih banyak atas wawasan yang berharga!
DaveyBob
21

Saya percaya ini adalah desain, sesuai dengan deskripsi tingkat isolasi read-commit untuk PostgreSQL 9.2:

MEMPERBARUI, MENGHAPUS, MEMILIH UNTUK MEMPERBARUI, dan MEMILIH UNTUK BERBAGI perintah berperilaku sama dengan SELECT dalam hal mencari baris target: mereka hanya akan menemukan baris target yang dilakukan pada waktu mulai perintah 1 . Namun, baris target seperti itu mungkin telah diperbarui (atau dihapus atau dikunci) oleh transaksi bersamaan lainnya saat ditemukan. Dalam hal ini, calon pemberi pembaruan akan menunggu transaksi pembaruan pertama untuk melakukan atau memutar kembali (jika masih dalam proses). Jika pembaru pertama mundur, maka efeknya dinegasikan dan pembaru kedua dapat melanjutkan dengan memperbarui baris yang awalnya ditemukan. Jika pembaru pertama melakukan, pembaru kedua akan mengabaikan baris jika pembaru pertama menghapusnya 2, jika tidak, ia akan mencoba menerapkan operasinya ke versi baris yang diperbarui.

Baris Anda memasukkan dalam S1tidak ada namun ketika S2's DELETEmulai. Jadi itu tidak akan terlihat oleh delete S2seperti pada ( 1 ) di atas. Salah satu yang S1dihapus diabaikan oleh S2's DELETEsesuai dengan ( 2 ).

Jadi S2, hapus tidak melakukan apa-apa. Ketika sisipan muncul, orang tersebut melihat S1sisipan:

Karena mode Baca Komitmen memulai setiap perintah dengan snapshot baru yang mencakup semua transaksi yang dilakukan hingga saat itu, perintah berikutnya dalam transaksi yang sama akan melihat efek dari transaksi bersamaan yang dilakukan dalam kasus apa pun . Poin yang dipermasalahkan di atas adalah apakah perintah tunggal melihat tampilan yang benar-benar konsisten dari database.

Jadi upaya memasukkan S2gagal dengan kendala kendala.

Melanjutkan membaca dokumen itu, menggunakan pembacaan berulang atau bahkan serializable tidak akan menyelesaikan masalah Anda sepenuhnya - sesi kedua akan gagal dengan kesalahan serialisasi pada penghapusan.

Ini akan memungkinkan Anda untuk mencoba kembali transaksi.

Tikar
sumber
Terima kasih Mat. Sementara itu tampaknya apa yang terjadi, tampaknya ada kesalahan dalam logika itu. Tampak bagi saya bahwa, dalam tingkat iso READ_COMMITTED, maka dua pernyataan ini harus berhasil dalam tx: HAPUS DARI tes WHERE ID = 1 MASUKKAN DALAM NILAI tes (1) Maksud saya, jika saya menghapus baris dan kemudian memasukkan baris, maka sisipan itu harus berhasil. SQLServer melakukan ini dengan benar. Karena, saya mengalami kesulitan menangani situasi ini dalam produk yang harus bekerja dengan kedua database.
DaveyBob
11

Saya sepenuhnya setuju dengan jawaban yang sangat baik dari @ Mat . Saya hanya menulis jawaban lain, karena tidak akan cocok dengan komentar.

Sebagai balasan untuk komentar Anda: DELETES2 dalam sudah terhubung pada versi baris tertentu. Karena ini dibunuh oleh S1 sementara itu, S2 menganggap dirinya berhasil. Meskipun tidak jelas dari pandangan sekilas, rangkaian acara sebenarnya adalah seperti ini:

   S1 DELETE berhasil  
S2 DELETE (berhasil dengan proxy - DELETE dari S1)  
   S1 re-INSERT menghapus nilai secara virtual  
S2 INSERT gagal dengan pelanggaran batasan kunci unik

Semuanya dengan desain. Anda benar-benar perlu menggunakan SERIALIZABLEtransaksi untuk kebutuhan Anda dan pastikan Anda mencoba lagi pada kegagalan serialisasi.

Erwin Brandstetter
sumber
1

Gunakan kunci utama DEFERRABLE dan coba lagi.

Frank Heikens
sumber
terima kasih atas tipnya, tetapi menggunakan DEFERRABLE tidak membuat perbedaan sama sekali. Doc membaca seperti seharusnya, tetapi tidak.
DaveyBob
-2

Kami juga menghadapi masalah ini. Solusi kami menambahkan select ... for updatesebelumnya delete from ... where. Level isolasi harus Baca Komitmen.

Mian Huang
sumber