Locks in PostgreSQL: 3. Mengunci objek lain

Kita telah berbicara tentang beberapa kunci pada level objek (khususnya, tentang kunci pada relasi), serta tentang kunci pada level baris , hubungannya dengan kunci objek dan antrian tunggu, yang tidak selalu jujur.

Hari ini kita memiliki gado-gado. Mari kita mulai dengan kebuntuan (sebenarnya, saya akan berbicara tentang mereka terakhir kali, tetapi artikel itu ternyata panjang tidak senonoh), maka kita akan membahas kunci objek yang tersisa dan berbicara tentang kunci predikat sebagai kesimpulan.

Jalan buntu


Saat menggunakan kunci, situasi deadlock (atau deadlock ) mungkin terjadi. Itu terjadi ketika satu transaksi mencoba untuk menangkap sumber daya yang sudah ditangkap oleh transaksi lain, sedangkan transaksi lain mencoba untuk menangkap sumber daya yang ditangkap oleh yang pertama. Ini diilustrasikan dalam gambar kiri di bawah ini: panah padat menunjukkan sumber daya yang ditangkap, panah putus-putus menunjukkan upaya untuk menangkap sumber daya yang sudah diduduki.

Adalah mudah untuk memvisualisasikan kebuntuan dengan membuat grafik harapan. Untuk melakukan ini, kami menghapus sumber daya tertentu dan hanya meninggalkan transaksi, mencatat transaksi mana yang menunggu. Jika grafik memiliki kontur (dari atas Anda dapat melihatnya dengan panah) - ini adalah jalan buntu.



Tentu saja, jalan buntu tidak hanya dimungkinkan untuk dua transaksi, tetapi juga untuk jumlah yang lebih besar.

Jika kebuntuan terjadi, transaksi yang terlibat di dalamnya tidak dapat berbuat apa-apa - mereka akan menunggu tanpa batas waktu. Karenanya, semua DBMS, dan PostgreSQL, juga, secara otomatis melacak kebuntuan.

Namun, pengecekan memerlukan upaya tertentu, yang tidak ingin saya lakukan setiap kali kunci baru diminta (setelah semua, deadlock sangat jarang terjadi). Oleh karena itu, ketika proses mencoba untuk menangkap kunci dan tidak bisa, itu memasuki antrian dan tertidur, tetapi mulai timer ke nilai yang ditentukan dalam parameter deadlock_timeout (secara default, 1 detik). Jika sumber dibebaskan sebelumnya, maka bagus, kami menghemat verifikasi. Tetapi jika setelah deadlock_timeout tunggu terus, maka proses menunggu akan dibangunkan dan memulai pemeriksaan.

Jika cek (yang terdiri dalam membangun grafik harapan dan mencari kontur di dalamnya) tidak mengungkapkan kebuntuan, maka proses terus tidur - sekarang sudah sampai pada akhir yang pahit.

Sebelumnya di komentar, saya benar dicela karena tidak mengatakan apa-apa tentang parameter lock_timeout , yang bertindak pada operator apa pun dan menghindari menunggu lama tanpa batas: jika kunci tidak dapat diperoleh dalam waktu yang ditentukan, pernyataan diakhiri dengan kesalahan lock_not_available. Seharusnya tidak bingung dengan parameter statement_timeout , yang membatasi waktu eksekusi total pernyataan, tidak peduli apakah itu mengharapkan kunci atau hanya melakukan pekerjaan.

Jika kebuntuan terdeteksi, maka salah satu transaksi (dalam kebanyakan kasus, transaksi yang memulai cek) dihentikan secara paksa. Dalam hal ini, kunci yang ditangkap olehnya dilepaskan dan sisa transaksi dapat terus berfungsi.

Kebuntuan biasanya berarti bahwa aplikasi tidak dirancang dengan benar. Ada dua cara untuk mendeteksi situasi seperti itu: pertama, pesan akan muncul di log server, dan kedua, nilai pg_stat_database.deadlocks akan meningkat.

Contoh deadlock


Penyebab umum kebuntuan adalah urutan berbeda di mana baris dalam tabel dikunci.
Contoh sederhana. Transaksi pertama bermaksud untuk mentransfer 100 rubel dari akun pertama ke akun kedua. Untuk melakukan ini, pertama-tama dia mengurangi hitungan pertama:

=> BEGIN; => UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1; 
 UPDATE 1 

Pada saat yang sama, transaksi kedua bermaksud untuk mentransfer 10 rubel dari akun kedua ke yang pertama. Dia mulai dengan mengurangi hitungan kedua:

 | => BEGIN; | => UPDATE accounts SET amount = amount - 10.00 WHERE acc_no = 2; 
 | UPDATE 1 

Sekarang transaksi pertama sedang mencoba untuk menambah akun kedua, tetapi menemukan bahwa baris terkunci.

 => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2; 

Kemudian transaksi kedua mencoba meningkatkan akun pertama, tetapi juga diblokir.

 | => UPDATE accounts SET amount = amount + 10.00 WHERE acc_no = 1; 

Ada harapan siklus yang tidak akan pernah berakhir dengan sendirinya. Setelah satu detik, transaksi pertama, tidak memiliki akses ke sumber daya, memulai pemeriksaan kebuntuan dan memutuskan server.

 ERROR: deadlock detected DETAIL: Process 16477 waits for ShareLock on transaction 530695; blocked by process 16513. Process 16513 waits for ShareLock on transaction 530694; blocked by process 16477. HINT: See server log for query details. CONTEXT: while updating tuple (0,2) in relation "accounts" 

Sekarang transaksi kedua dapat dilanjutkan.

 | UPDATE 1 
 | => ROLLBACK; 

 => ROLLBACK; 

Cara yang benar untuk melakukan operasi seperti itu adalah memblokir sumber daya dalam urutan yang sama. Misalnya, dalam hal ini, Anda dapat memblokir akun dalam urutan angka-angka mereka.

Jalan buntu untuk dua perintah PEMBARUAN


Kadang-kadang Anda bisa mendapatkan jalan buntu di mana, tampaknya, seharusnya tidak. Misalnya, mudah dan akrab untuk menganggap perintah SQL sebagai atom, tetapi ambil UPDATE - perintah ini memblokir baris saat mereka diperbarui. Ini tidak terjadi sekaligus. Oleh karena itu, jika satu perintah memperbarui baris dalam satu urutan dan yang lainnya dalam urutan lainnya, mereka mungkin menemui jalan buntu.

Sepertinya tidak mungkin mendapatkan situasi seperti itu, tetapi bagaimanapun juga itu bisa bertemu. Untuk pemutaran, kami akan membuat indeks pada kolom jumlah, dibuat dalam urutan jumlah menurun:

 => CREATE INDEX ON accounts(amount DESC); 

Untuk memiliki waktu untuk melihat apa yang terjadi, kami akan menulis fungsi yang meningkatkan nilai yang ditransmisikan, tetapi secara perlahan, perlahan, sebentar:

 => CREATE FUNCTION inc_slow(n numeric) RETURNS numeric AS $$ SELECT pg_sleep(1); SELECT n + 100.00; $$ LANGUAGE SQL; 

Kita juga membutuhkan ekstensi pgrowlocks.

 => CREATE EXTENSION pgrowlocks; 

Perintah UPDATE pertama akan memperbarui seluruh tabel. Rencana eksekusi sudah jelas - pemindaian berurutan:

 | => EXPLAIN (costs off) | UPDATE accounts SET amount = inc_slow(amount); 
 | QUERY PLAN | ---------------------------- | Update on accounts | -> Seq Scan on accounts | (2 rows) 

Karena versi baris pada halaman tabel kami berada dalam urutan jumlah yang meningkat (persis seperti yang kami tambahkan), mereka akan diperbarui dalam urutan yang sama. Kami memulai pembaruan untuk bekerja.

 | => UPDATE accounts SET amount = inc_slow(amount); 

Sementara itu, di sesi lain, kami akan melarang penggunaan pemindaian berurutan:

 || => SET enable_seqscan = off; 

Dalam hal ini, penjadwal memutuskan untuk menggunakan pemindaian indeks untuk pernyataan UPDATE berikut:

 || => EXPLAIN (costs off) || UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00; 
 || QUERY PLAN || -------------------------------------------------------- || Update on accounts || -> Index Scan using accounts_amount_idx on accounts || Index Cond: (amount > 100.00) || (3 rows) 

Baris kedua dan ketiga jatuh dalam kondisi tersebut, dan, karena indeks dibangun dalam urutan menurun, baris akan diperbarui dalam urutan terbalik.

Kami meluncurkan pembaruan berikutnya.

 || => UPDATE accounts SET amount = inc_slow(amount) WHERE amount > 100.00; 

Pandangan sekilas pada halaman tabular menunjukkan bahwa operator pertama telah berhasil memperbarui baris pertama (0,1), dan yang kedua - terakhir (0,3):

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------- locked_row | (0,1) locker | 530699 <-  multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 2 ]----------------- locked_row | (0,3) locker | 530700 <-  multi | f xids | {530700} modes | {"No Key Update"} pids | {16549} 

Satu detik berlalu. Operator pertama memperbarui baris kedua, dan yang kedua ingin melakukan ini, tetapi tidak bisa.

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------- locked_row | (0,1) locker | 530699 <-  multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 2 ]----------------- locked_row | (0,2) locker | 530699 <-    multi | f xids | {530699} modes | {"No Key Update"} pids | {16513} -[ RECORD 3 ]----------------- locked_row | (0,3) locker | 530700 <-  multi | f xids | {530700} modes | {"No Key Update"} pids | {16549} 

Sekarang pernyataan pertama ingin memperbarui baris terakhir dari tabel, tetapi sudah dikunci oleh yang kedua. Ini jalan buntu.

Salah satu transaksi dibatalkan:

 || ERROR: deadlock detected || DETAIL: Process 16549 waits for ShareLock on transaction 530699; blocked by process 16513. || Process 16513 waits for ShareLock on transaction 530700; blocked by process 16549. || HINT: See server log for query details. || CONTEXT: while updating tuple (0,2) in relation "accounts" 

Dan yang lainnya menyelesaikan eksekusi:

 | UPDATE 3 

Detail menarik tentang mendeteksi dan mencegah kebuntuan dapat ditemukan di manajer kunci README .

Itu semua tentang kebuntuan, dan kami melanjutkan ke kunci objek yang tersisa.



Kunci Non-Hubungan


Saat Anda ingin mengunci sumber daya yang bukan hubungan dalam pemahaman PostgreSQL, kunci objek digunakan. Sumber daya semacam itu bisa berupa apa saja: ruang tabel, langganan, skema, peran, tipe data yang disebutkan ... Secara kasar, semua yang dapat ditemukan dalam katalog sistem.

Mari kita lihat contoh sederhana. Kami memulai transaksi dan membuat tabel di dalamnya:

 => BEGIN; => CREATE TABLE example(n integer); 

Sekarang mari kita lihat jenis objek kunci apa yang muncul di pg_locks:

 => SELECT database, (SELECT datname FROM pg_database WHERE oid = l.database) AS dbname, classid, (SELECT relname FROM pg_class WHERE oid = l.classid) AS classname, objid, mode, granted FROM pg_locks l WHERE l.locktype = 'object' AND l.pid = pg_backend_pid(); 
  database | dbname | classid | classname | objid | mode | granted ----------+--------+---------+--------------+-------+-----------------+--------- 0 | | 1260 | pg_authid | 16384 | AccessShareLock | t 16386 | test | 2615 | pg_namespace | 2200 | AccessShareLock | t (2 rows) 

Untuk memahami apa yang sebenarnya diblokir di sini, Anda perlu melihat tiga bidang: database, classid dan objid. Mari kita mulai dengan baris pertama.

Basis data adalah OID dari basis data tempat sumber daya terkunci. Dalam kasus kami, ada nol di kolom ini. Ini berarti bahwa kita berhadapan dengan objek global yang bukan milik basis tertentu.

Classid berisi OID pg_class, yang sesuai dengan nama tabel katalog sistem, yang menentukan jenis sumber daya. Dalam kasus kami, pg_authid, artinya, peran adalah sumber daya (pengguna).

Objid berisi OID dari tabel katalog sistem yang ditunjukkan oleh classid kepada kami.

 => SELECT rolname FROM pg_authid WHERE oid = 16384; 
  rolname --------- student (1 row) 

Dengan demikian, peran siswa diblokir, dari mana kami bekerja.

Sekarang mari kita berurusan dengan baris kedua. Basis data ditunjukkan, dan ini adalah basis data uji yang terhubung dengan kami.

Classid menunjuk ke tabel pg_namespace yang berisi skema.

 => SELECT nspname FROM pg_namespace WHERE oid = 2200; 
  nspname --------- public (1 row) 

Dengan demikian, skema publik diblokir.

Jadi, kami melihat bahwa saat membuat objek, peran pemilik dan skema tempat objek dibuat diblokir (dalam mode berbagi). Yang logis: kalau tidak, seseorang bisa menghapus peran atau skema sementara transaksi belum selesai.

 => ROLLBACK; 

Kunci Ekstensi Hubungan


Ketika jumlah baris dalam suatu relasi (yaitu, dalam tabel, indeks, tampilan terwujud) meningkat, PostgreSQL dapat menggunakan ruang kosong di halaman yang ada untuk memasukkan, tetapi, jelas, pada titik tertentu Anda harus menambahkan halaman baru. Secara fisik, mereka ditambahkan ke akhir file yang sesuai. Ini dipahami sebagai memperluas hubungan .

Untuk mencegah dua proses dari terburu-buru untuk menambah halaman pada saat yang sama, proses ini dilindungi oleh kunci jenis khusus yang diperluas. Kunci yang sama digunakan saat membersihkan indeks sehingga proses lain tidak dapat menambahkan halaman selama pemindaian.

Tentu saja, kunci ini dilepaskan tanpa menunggu akhir transaksi.

Sebelumnya, tabel hanya diperluas satu halaman pada satu waktu. Ini menyebabkan masalah ketika beberapa proses secara bersamaan memasukkan baris, oleh karena itu, dalam PostgreSQL 9.6, beberapa halaman ditambahkan ke tabel sekaligus (sebanding dengan jumlah proses yang menunggu untuk dikunci, tetapi tidak lebih dari 512).

Kunci Halaman


Kunci tingkat halaman diterapkan dalam satu-satunya kasus (kecuali untuk kunci predikat, yang akan dibahas kemudian).

Indeks GIN memungkinkan Anda untuk mempercepat pencarian dalam nilai majemuk, misalnya, kata-kata dalam dokumen teks (atau elemen dalam array). Untuk perkiraan pertama, indeks tersebut dapat direpresentasikan sebagai pohon-B biasa, di mana bukan dokumen itu sendiri disimpan, tetapi kata-kata individual dari dokumen-dokumen ini. Oleh karena itu, ketika menambahkan dokumen baru, indeks harus dibangun kembali dengan cukup kuat, memasukkan setiap kata yang dimasukkan ke dalam dokumen.

Untuk meningkatkan kinerja, indeks GIN memiliki fitur penyisipan tertunda yang diaktifkan oleh opsi penyimpanan pembaruan cepat. Kata-kata baru pertama-tama dengan cepat ditambahkan ke daftar pending yang tidak berurutan, dan setelah beberapa waktu, semua yang telah terakumulasi dipindahkan ke struktur indeks utama. Tabungan disebabkan oleh fakta bahwa dokumen yang berbeda cenderung mengandung kata-kata rangkap.

Untuk mengecualikan beberapa proses dari pindah dari daftar tunggu ke indeks utama pada saat yang sama, halaman meta indeks diblokir dalam mode eksklusif selama durasi transfer. Ini tidak mengganggu penggunaan indeks dalam mode normal.

Kunci penasehat


Tidak seperti kunci lainnya (seperti kunci hubungan), kunci penasihat tidak pernah diatur secara otomatis, mereka dikelola oleh pengembang aplikasi. Mereka nyaman digunakan, misalnya, jika suatu aplikasi membutuhkan pemblokiran logika untuk beberapa tujuan yang tidak sesuai dengan logika standar kunci biasa.

Misalkan kita memiliki sumber daya bersyarat yang tidak sesuai dengan objek basis data apa pun (yang dapat kita blokir dengan perintah seperti SELECT FOR atau LOCK TABLE). Anda harus membuat pengidentifikasi numerik untuk itu. Jika sumber daya memiliki nama unik, maka opsi sederhana adalah mengambil kode hash dari itu:

 => SELECT hashtext('1'); 
  hashtext ----------- 243773337 (1 row) 

Inilah cara kami menangkap kuncinya:

 => BEGIN; => SELECT pg_advisory_lock(hashtext('1')); 

Seperti biasa, informasi kunci tersedia di pg_locks:

 => SELECT locktype, objid, mode, granted FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid(); 
  locktype | objid | mode | granted ----------+-----------+---------------+--------- advisory | 243773337 | ExclusiveLock | t (1 row) 

Agar kunci benar-benar berfungsi, proses lain juga harus mendapatkan kunci sebelum mengakses sumber. Kepatuhan terhadap aturan ini jelas harus dipastikan oleh aplikasi.

Dalam contoh di atas, kunci valid hingga akhir sesi, dan bukan transaksi, seperti biasa.

 => COMMIT; => SELECT locktype, objid, mode, granted FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid(); 
  locktype | objid | mode | granted ----------+-----------+---------------+--------- advisory | 243773337 | ExclusiveLock | t (1 row) 

Itu harus secara eksplisit dirilis:

 => SELECT pg_advisory_unlock(hashtext('1')); 

Ada sejumlah besar fungsi untuk bekerja dengan kunci penasihat untuk semua kesempatan:

  • pg_advisory_lock_shared memperlakukan kunci bersama,
  • pg_advisory_xact_lock (dan pg_advisory_xact_lock_share) mendapat kunci sampai akhir transaksi,
  • pg_try_advisory_lock (serta pg_try_advisory_xact_lock dan pg_try_advisory_xact_lock_shared) tidak menunggu kunci, tetapi mengembalikan nilai palsu jika kunci tidak dapat diperoleh dengan segera.

Serangkaian fungsi coba memberikan cara lain untuk tidak menunggu kunci, selain yang tercantum dalam artikel sebelumnya .

Kunci predikat


Istilah penguncian predikat muncul sejak lama, pada upaya pertama untuk menerapkan isolasi penuh berdasarkan kunci di DBMSs awal (tingkat Serializable, meskipun standar SQL tidak ada pada waktu itu). Masalah yang kemudian ditemui adalah bahwa bahkan memblokir semua baris yang dibaca dan diubah tidak memberikan isolasi lengkap: baris baru mungkin muncul dalam tabel yang termasuk dalam kondisi pemilihan yang sama, yang mengarah pada hantu (lihat artikel tentang isolasi ) .

Ide kunci predikat adalah untuk memblokir predikat, bukan baris. Jika, ketika menjalankan kueri dengan kondisi a > 10, predikat a > 10 diblokir, ini tidak akan menambah baris baru ke tabel yang termasuk dalam kondisi dan akan menghindari hantu. Masalahnya adalah bahwa dalam kasus umum ini adalah tugas yang sulit secara komputasi; dalam praktiknya, itu bisa diselesaikan hanya untuk predikat yang memiliki bentuk yang sangat sederhana.

Dalam PostgreSQL, lapisan Serializable diimplementasikan secara berbeda, di atas isolasi berbasis snapshot yang ada. Istilah predicate lock tetap ada, tetapi artinya telah berubah secara radikal. Bahkan, "kunci" seperti itu tidak memblokir apa pun, tetapi digunakan untuk melacak ketergantungan data antara transaksi.

Terbukti bahwa isolasi berdasarkan gambar memungkinkan anomali rekaman yang tidak konsisten dan anomali hanya transaksi membaca , tetapi tidak ada anomali lain yang mungkin. Untuk memahami bahwa kita berurusan dengan salah satu dari dua anomali yang terdaftar, kita dapat menganalisis dependensi antara transaksi dan menemukan pola tertentu di dalamnya.

Kami tertarik pada dua jenis dependensi:

  • satu transaksi membaca satu baris, yang kemudian diubah oleh transaksi lain (ketergantungan RW),
  • satu transaksi memodifikasi baris yang dibaca oleh transaksi lain (ketergantungan WR).

WR-dependensi dapat dilacak menggunakan kunci konvensional yang ada, tetapi RW-dependency hanya perlu melacak tambahan.

Saya ulangi sekali lagi: terlepas dari namanya, kunci predikat tidak memblokir apa pun. Alih-alih, ketika transaksi dilakukan, pemeriksaan dilakukan dan, jika urutan dependensi “buruk” terdeteksi yang mungkin mengindikasikan anomali, transaksi terputus.

Mari kita lihat bagaimana pemasangan kunci predikat terjadi. Untuk melakukan ini, buat tabel dengan jumlah baris yang cukup besar dan indeks di atasnya.

 => CREATE TABLE pred(n integer); => INSERT INTO pred(n) SELECT gn FROM generate_series(1,10000) g(n); => CREATE INDEX ON pred(n) WITH (fillfactor = 10); => ANALYZE pred; 

Jika kueri dijalankan oleh pemindaian berurutan dari seluruh tabel, maka kunci predikat diatur di seluruh tabel (bahkan jika tidak semua baris berada dalam kondisi penyaringan).

 | => SELECT pg_backend_pid(); 
 | pg_backend_pid | ---------------- | 12763 | (1 row) 

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n > 100; 
 | QUERY PLAN | ---------------------------------------------------------------- | Seq Scan on pred (actual time=0.047..12.709 rows=9900 loops=1) | Filter: (n > 100) | Rows Removed by Filter: 100 | Planning Time: 0.190 ms | Execution Time: 15.244 ms | (5 rows) 

Setiap kunci predikat selalu ditangkap dalam satu mode SIReadLock (Serializable Isolation Read) khusus:

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+----------+------+------- relation | pred | | (1 row) 

 | => ROLLBACK; 

Tetapi jika kueri dieksekusi menggunakan pemindaian indeks, situasinya berubah menjadi lebih baik. Jika kita berbicara tentang B-tree, maka cukup untuk mengatur kunci pada baris tabel baca dan pada halaman rindang indeks - dengan demikian kita memblokir tidak hanya nilai-nilai spesifik, tetapi juga seluruh rentang dibaca.

 | => BEGIN ISOLATION LEVEL SERIALIZABLE; | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n BETWEEN 1000 AND 1001; 
 | QUERY PLAN | ------------------------------------------------------------------------------------ | Index Only Scan using pred_n_idx on pred (actual time=0.122..0.131 rows=2 loops=1) | Index Cond: ((n >= 1000) AND (n <= 1001)) | Heap Fetches: 2 | Planning Time: 0.096 ms | Execution Time: 0.153 ms | (5 rows) 

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- tuple | pred | 3 | 236 tuple | pred | 3 | 235 page | pred_n_idx | 22 | (3 rows) 

Anda mungkin memperhatikan beberapa kesulitan.

Pertama, kunci terpisah dibuat untuk setiap versi baris yang dibaca, tetapi berpotensi ada banyak versi seperti itu. Jumlah total kunci predikat dalam sistem dibatasi oleh produk dari nilai parameter max_pred_locks_per_transaction × max_connections (nilai default masing-masing adalah 64 dan 100). Memori untuk kunci tersebut dialokasikan pada startup server; berusaha melebihi angka ini akan menghasilkan kesalahan.

Oleh karena itu, untuk kunci predikat (dan hanya untuk mereka!), Kenaikan level digunakan. Sebelum PostgreSQL 10, ada batasan yang tertanam dalam kode, dan mulai dengan itu Anda dapat mengontrol parameter dengan menaikkan level. Jika jumlah kunci versi baris per baris lebih besar dari max_pred_locks_per_page , kunci tersebut diganti dengan satu kunci tingkat halaman. Berikut ini sebuah contoh:

 => SHOW max_pred_locks_per_page; 
  max_pred_locks_per_page ------------------------- 2 (1 row) 

 | => EXPLAIN (analyze, costs off) | SELECT * FROM pred WHERE n BETWEEN 1000 AND 1002; 
 | QUERY PLAN | ------------------------------------------------------------------------------------ | Index Only Scan using pred_n_idx on pred (actual time=0.019..0.039 rows=3 loops=1) | Index Cond: ((n >= 1000) AND (n <= 1002)) | Heap Fetches: 3 | Planning Time: 0.069 ms | Execution Time: 0.057 ms | (5 rows) 

Alih-alih tiga kunci tuple, kita melihat satu jenis halaman:

 => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- page | pred | 3 | page | pred_n_idx | 22 | (2 rows) 

Demikian pula, jika jumlah kunci halaman yang terkait dengan satu hubungan melebihi max_pred_locks_per_relation , kunci tersebut diganti dengan satu kunci tingkat hubungan.

Tidak ada level lain: kunci predikat hanya ditangkap untuk hubungan, halaman, atau versi baris, dan selalu dengan mode SIReadLock.

Tentu saja, peningkatan tingkat kunci tak terelakkan mengarah pada fakta bahwa jumlah transaksi yang lebih besar akan secara salah menghasilkan kesalahan serialisasi dan, sebagai hasilnya, throughput sistem akan menurun. Di sini Anda perlu mencari keseimbangan antara konsumsi memori dan kinerja.

Kesulitan kedua adalah bahwa dalam berbagai operasi dengan indeks (misalnya, karena pemisahan halaman indeks ketika memasukkan baris baru), jumlah halaman lembar yang mencakup rentang baca dapat berubah. Tetapi implementasi ini memperhitungkan:

 => INSERT INTO pred SELECT 1001 FROM generate_series(1,1000); => SELECT locktype, relation::regclass, page, tuple FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 12763; 
  locktype | relation | page | tuple ----------+------------+------+------- page | pred | 3 | page | pred_n_idx | 211 | page | pred_n_idx | 212 | page | pred_n_idx | 22 | (4 rows) 

 | => ROLLBACK; 

Omong-omong, kunci predikat tidak selalu dihapus segera setelah transaksi selesai, karena mereka diperlukan untuk melacak ketergantungan antara beberapa transaksi. Tetapi bagaimanapun juga, mereka dikelola secara otomatis.

Tidak semua tipe indeks di PostgreSQL mendukung kunci predikat. Sebelumnya, hanya pohon-B yang dapat membanggakan ini, tetapi dalam PostgreSQL 11 situasinya membaik: indeks hash, GiST dan GIN ditambahkan ke daftar. Jika akses indeks digunakan, dan indeks tidak berfungsi dengan kunci predikat, maka seluruh indeks dikunci ke kunci. Tentu saja, ini juga meningkatkan jumlah jeda transaksi palsu.

Sebagai kesimpulan, saya perhatikan bahwa dengan penggunaan kunci predikatlah ada batasan bahwa, untuk menjamin isolasi lengkap, semua transaksi harus bekerja pada tingkat Serializable. Jika suatu transaksi menggunakan tingkat yang berbeda, itu tidak akan menetapkan (dan memeriksa) kunci predikat.

Secara tradisional, saya akan meninggalkan tautan ke README pada kunci predikat , dari mana Anda dapat mulai mempelajari kode sumber.

Untuk dilanjutkan .

Source: https://habr.com/ru/post/id465263/


All Articles