MVCC dalam PostgreSQL-4. Jepretan

Setelah membahas masalah isolasi dan melakukan penyimpangan mengenai struktur data tingkat rendah , terakhir kali kami menjelajahi versi baris dan mengamati bagaimana berbagai operasi mengubah bidang tuple header.

Sekarang kita akan melihat bagaimana snapshot data yang konsisten diperoleh dari tuple.

Apa itu snapshot data?


Halaman data dapat secara fisik berisi beberapa versi dari baris yang sama. Tetapi setiap transaksi harus melihat hanya satu (atau tidak ada) versi dari setiap baris, sehingga semuanya membuat gambaran yang konsisten dari data (dalam arti ACID) pada titik waktu tertentu.

Isolasi dalam PosgreSQL didasarkan pada snapshots: setiap transaksi bekerja dengan snapshot datanya sendiri, yang "berisi" data yang dilakukan sebelum saat snapshot dibuat dan tidak "berisi" data yang belum dilakukan saat itu. Kita telah melihat bahwa walaupun isolasi yang dihasilkan tampak lebih ketat daripada yang disyaratkan oleh standar, ia masih memiliki anomali.

Pada tingkat isolasi Baca Komitmen, sebuah snapshot dibuat di awal setiap pernyataan transaksi. Cuplikan ini aktif saat pernyataan sedang dijalankan. Dalam gambar, saat snapshot dibuat (yang, seperti kita ingat, ditentukan oleh ID transaksi) ditunjukkan dengan warna biru.



Pada tingkat Repeatable Read dan Serializable, snapshot dibuat sekali, pada awal pernyataan transaksi pertama. Cuplikan seperti itu tetap aktif hingga akhir transaksi.



Visibilitas tuple dalam snapshot


Aturan visibilitas


Cuplikan tentu bukan salinan fisik dari semua tupel yang diperlukan. Snapshot sebenarnya ditentukan oleh beberapa angka, dan visibilitas tuple dalam snapshot ditentukan oleh aturan.

Apakah tuple akan terlihat atau tidak dalam snapshot tergantung pada dua bidang dalam header, yaitu, xmin dan xmax , yaitu ID transaksi yang membuat dan menghapus tuple. Interval seperti ini tidak tumpang tindih, dan oleh karena itu, tidak lebih dari satu versi mewakili satu baris di setiap foto.

Aturan visibilitas yang tepat cukup rumit dan memperhitungkan banyak kasus dan ekstrem yang berbeda.
Anda dapat dengan mudah memastikannya dengan melihat ke src / backend / utils / time / tqual.c (dalam versi 12, centang dipindahkan ke src / backend / akses / heap / heapam_visibility.c).

Untuk menyederhanakan, kita dapat mengatakan bahwa tuple terlihat ketika dalam snapshot, perubahan yang dibuat oleh transaksi xmin terlihat, sedangkan yang dibuat oleh transaksi xmax tidak (dengan kata lain, sudah jelas bahwa tuple dibuat, tetapi belum jelas apakah itu dihapus).

Mengenai transaksi, perubahannya terlihat dalam snapshot baik jika itu adalah transaksi yang menciptakan snapshot (itu memang melihat sendiri belum melakukan perubahan) atau transaksi dilakukan sebelum snapshot dibuat.

Kami dapat secara grafis mewakili transaksi berdasarkan segmen (dari waktu mulai hingga waktu komit):



Di sini:

  • Perubahan transaksi 2 akan terlihat sejak selesai sebelum snapshot dibuat.
  • Perubahan transaksi 1 tidak akan terlihat karena aktif saat snapshot dibuat.
  • Perubahan transaksi 3 tidak akan terlihat sejak dimulai setelah snapshot dibuat (terlepas dari apakah itu selesai atau tidak).

Sayangnya, sistem tidak mengetahui waktu transaksi yang dilakukan. Hanya waktu mulainya yang diketahui (yang ditentukan oleh ID transaksi dan ditandai dengan garis putus-putus pada gambar di atas), tetapi acara penyelesaian tidak ditulis di mana pun.

Yang bisa kita lakukan adalah mencari tahu status transaksi saat ini di pembuatan snapshot. Informasi ini tersedia dalam memori bersama server, dalam struktur ProcArray, yang berisi daftar semua sesi aktif dan transaksi mereka.

Tetapi kami tidak dapat menemukan post factum apakah transaksi tertentu aktif pada saat snapshot dibuat. Oleh karena itu, snapshot harus menyimpan daftar semua transaksi aktif saat ini.

Dari penjelasan di atas, maka dalam PostgreSQL, tidak mungkin untuk membuat snapshot yang menunjukkan data konsisten pada waktu tertentu mundur, bahkan jika semua tuple yang diperlukan tersedia di halaman tabel. Sebuah pertanyaan sering muncul mengapa PostgreSQL tidak memiliki pertanyaan retrospektif (atau temporal; atau flashback, seperti Oracle menyebutnya) - dan ini adalah salah satu alasannya.
Agak lucu adalah bahwa fungsi ini pertama kali tersedia, tetapi kemudian dihapus dari DBMS. Anda dapat membaca tentang ini di artikel oleh Joseph M. Hellerstein .

Jadi, snapshot ditentukan oleh beberapa parameter:

  • Saat snapshot dibuat, lebih tepatnya, ID transaksi berikutnya, namun tidak tersedia di sistem ( snapshot.xmax ).
  • Daftar transaksi aktif (sedang berlangsung) pada saat snapshot dibuat ( snapshot.xip ).

Untuk kenyamanan dan optimalisasi, ID dari transaksi aktif paling awal juga disimpan ( snapshot.xmin ). Nilai ini masuk akal, yang akan dibahas di bawah ini.

Cuplikan juga menyimpan beberapa parameter lagi, yang tidak penting bagi kami.



Contoh


Untuk memahami bagaimana snapshot menentukan visibilitas, mari kita buat contoh di atas dengan tiga transaksi. Tabel akan memiliki tiga baris, di mana:

  • Yang pertama ditambahkan oleh transaksi yang dimulai sebelum pembuatan snapshot tetapi diselesaikan setelah itu.
  • Yang kedua ditambahkan oleh transaksi yang dimulai dan diselesaikan sebelum pembuatan snapshot.
  • Yang ketiga ditambahkan setelah pembuatan snapshot.

 => TRUNCATE TABLE accounts; 

Transaksi pertama (belum selesai):

 => BEGIN; => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00); => SELECT txid_current(); 
 => SELECT txid_current(); txid_current -------------- 3695 (1 row) 

Transaksi kedua (selesai sebelum snapshot dibuat):

 | => BEGIN; | => INSERT INTO accounts VALUES (2, '2001', 'bob', 100.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3696 | (1 row) 
 | => COMMIT; 

Membuat snapshot dalam transaksi di sesi lain.

 || => BEGIN ISOLATION LEVEL REPEATABLE READ; || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

Melakukan transaksi pertama setelah foto dibuat:

 => COMMIT; 

Dan transaksi ketiga (muncul setelah foto dibuat):

 | => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3697 | (1 row) 
 | => COMMIT; 

Terbukti, hanya satu baris yang masih terlihat di snapshot kami:

 || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

Pertanyaannya adalah bagaimana Postgres memahami ini.

Semua ditentukan oleh snapshot. Mari kita melihatnya:

 || => SELECT txid_current_snapshot(); 
 || txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row) 

Di sini snapshot.xmin , snapshot.xmin , dan snapshot.xip dicantumkan, dibatasi oleh titik dua ( snapshot.xip adalah satu nomor dalam kasus ini, tetapi secara umum ini adalah daftar).

Menurut aturan di atas, dalam snapshot, perubahan itu harus terlihat yang dilakukan oleh transaksi dengan ID xid sedemikian sehingga snapshot.xmin <= xid < snapshot.xmax kecuali yang ada dalam daftar snapshot.xip . Mari kita lihat semua baris tabel (dalam snapshot baru):

 => SELECT xmin, xmax, * FROM accounts ORDER BY id; 
  xmin | xmax | id | number | client | amount ------+------+----+--------+--------+--------- 3695 | 0 | 1 | 1001 | alice | 1000.00 3696 | 0 | 2 | 2001 | bob | 100.00 3697 | 0 | 3 | 2002 | bob | 900.00 (3 rows) 

Baris pertama tidak terlihat: itu dibuat oleh transaksi yang ada di daftar transaksi aktif ( xip ).
Baris kedua terlihat: dibuat oleh transaksi yang berada dalam kisaran snapshot.
Baris ketiga tidak terlihat: itu dibuat oleh transaksi yang berada di luar jangkauan snapshot.

 || => COMMIT; 

Perubahan transaksi sendiri


Menentukan visibilitas perubahan transaksi itu sendiri agak menyulitkan situasi. Dalam hal ini, mungkin diperlukan untuk melihat hanya sebagian dari perubahan tersebut. Sebagai contoh: pada tingkat isolasi apa pun, kursor yang dibuka pada titik waktu tertentu tidak boleh melihat perubahan dilakukan nanti.

Untuk tujuan ini, tuple header memiliki bidang khusus (diwakili dalam kolom semu cmax dan cmax ), yang menunjukkan nomor pesanan di dalam transaksi. cmin adalah angka untuk penyisipan, dan cmax - untuk penghapusan, tetapi untuk menghemat ruang dalam tuple header, ini sebenarnya satu bidang daripada dua bidang yang berbeda. Diasumsikan bahwa suatu transaksi jarang memasukkan dan menghapus baris yang sama.

Tetapi jika ini terjadi, id perintah kombo khusus ( combocid ) dimasukkan dalam bidang yang sama, dan proses backend mengingat cmin dan cmin sebenarnya untuk combocid ini. Tapi ini sepenuhnya eksotis.

Ini adalah contoh sederhana. Mari memulai transaksi dan menambahkan baris ke tabel:

 => BEGIN; => SELECT txid_current(); 
  txid_current -------------- 3698 (1 row) 
 INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00); 

Mari kita cmin isi tabel, bersama dengan bidang cmin (tetapi hanya untuk baris yang ditambahkan oleh transaksi - untuk yang lain tidak ada artinya):

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 (4 rows) 

Sekarang kita membuka kursor untuk kueri yang mengembalikan jumlah baris dalam tabel.

 => DECLARE c CURSOR FOR SELECT count(*) FROM accounts; 

Dan setelah itu kita tambahkan baris lain:

 => INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00); 

Kueri mengembalikan 4 - baris yang ditambahkan setelah membuka kursor tidak masuk ke snapshot data:

 => FETCH c; 
  count ------- 4 (1 row) 

Mengapa Karena snapshot hanya memperhitungkan tupel dengan cmin < 1 .

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 3698 | 1 | 5 | 3002 | charlie | 200.00 (5 rows) 
 => ROLLBACK; 

Cakrawala acara


ID transaksi aktif paling awal ( snapshot.xmin ) masuk akal: ia menentukan "horizon peristiwa" dari transaksi tersebut. Artinya, di luar cakrawala transaksi selalu hanya melihat versi baris terbaru.

Sungguh, versi baris yang sudah usang (mati) harus terlihat hanya ketika yang baru dibuat oleh transaksi yang belum selesai dan, oleh karena itu, belum terlihat. Tetapi semua transaksi "di luar cakrawala" selesai pasti.



Anda dapat melihat cakrawala transaksi di katalog sistem:

 => BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

Kami juga dapat mendefinisikan cakrawala di tingkat basis data. Untuk melakukan ini, kita perlu mengambil semua snapshot aktif dan menemukan xmin tertua di antara mereka. Dan itu akan menentukan cakrawala, di luar itu tupel mati dalam database tidak akan pernah terlihat oleh transaksi apa pun. Tupel semacam itu dapat dihilangkan dengan debu - dan inilah mengapa konsep horizon begitu penting dari sudut pandang praktis.

Jika suatu transaksi menahan snapshot untuk waktu yang lama, dengan itu ia juga akan memegang cakrawala basis data. Selain itu, hanya keberadaan transaksi yang belum selesai akan memegang cakrawala bahkan jika transaksi itu sendiri tidak memiliki snapshot.

Dan ini berarti bahwa tupel mati di DB tidak dapat disedot. Selain itu, ada kemungkinan bahwa transaksi "long-play" tidak bersinggungan dengan data dengan transaksi lain sama sekali, tetapi ini tidak terlalu penting karena semuanya berbagi satu cakrawala basis data.

Jika sekarang kami membuat segmen yang mewakili snapshots (dari snapshot.xmin ke snapshot.xmax ) daripada transaksi, kami dapat memvisualisasikan situasi sebagai berikut:



Dalam gambar ini, snapshot terendah berkaitan dengan transaksi yang tidak selesai, dan pada snapshot lainnya, snapshot.xmin tidak boleh lebih besar dari ID transaksi.

Dalam contoh kami, transaksi dimulai dengan tingkat isolasi Baca Komit. Meskipun tidak memiliki snapshot data aktif, ia tetap memiliki horizon:

 | => BEGIN; | => UPDATE accounts SET amount = amount + 1.00; | => COMMIT; 
 => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

Dan hanya setelah penyelesaian transaksi, cakrawala bergerak maju, yang memungkinkan menyedot tupel mati:

 => COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3700 (1 row) 

Dalam kasus situasi yang dijelaskan benar-benar menyebabkan masalah dan tidak ada cara untuk mengatasinya di tingkat aplikasi, dua parameter tersedia mulai dengan versi 9.6:

  • old_snapshot_threshold menentukan masa pakai maksimum foto. Saat kali ini berlalu, server akan memenuhi syarat untuk menyedot tupel mati, dan jika transaksi "lama-main" masih membutuhkannya, itu akan mendapatkan kesalahan "snapshot terlalu lama".
  • idle_in_transaction_session_timeout menentukan masa maksimal transaksi idle. Ketika waktu ini berlalu, transaksi dibatalkan.

Ekspor foto


Kadang-kadang situasi muncul di mana beberapa transaksi bersamaan harus dijamin untuk melihat data yang sama. Contohnya adalah utilitas pg_dump , yang dapat bekerja dalam mode paralel: semua proses pekerja harus melihat database dalam keadaan yang sama agar salinan cadangan konsisten.

Tentu saja, kita tidak dapat mengandalkan kepercayaan bahwa transaksi akan melihat data yang sama hanya karena mereka dimulai "secara bersamaan". Untuk tujuan ini, ekspor dan impor snapshot tersedia.

Fungsi pg_export_snapshot mengembalikan ID snapshot, yang dapat diteruskan ke transaksi lain (menggunakan alat di luar DBMS).

 => BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts; -- any query 
  count ------- 3 (1 row) 
 => SELECT pg_export_snapshot(); 
  pg_export_snapshot --------------------- 00000004-00000E7B-1 (1 row) 

Transaksi lainnya dapat mengimpor snapshot menggunakan perintah SET TRANSACTION SNAPSHOT sebelum melakukan kueri pertama. Baca berulang atau tingkat isolasi Serializable juga harus ditentukan sebelumnya karena pada tingkat Komitmen Baca, pernyataan akan menggunakan snapshot mereka sendiri.

 | => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1'; 

Transaksi kedua sekarang akan bekerja dengan snapshot dari yang pertama dan, karenanya, lihat tiga baris (bukan nol):

 | => SELECT count(*) FROM accounts; 
 | count | ------- | 3 | (1 row) 

Umur snapshot yang diekspor sama dengan umur transaksi ekspor.

 | => COMMIT; => COMMIT; 

Baca terus .

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


All Articles