PEMBARUAN Postgres ... BATAS 1

77

Saya memiliki database Postgres yang berisi perincian tentang kelompok server, seperti status server ('aktif', 'siaga' dll). Server aktif kapan saja mungkin perlu gagal ke siaga, dan saya tidak peduli siaga mana yang digunakan secara khusus.

Saya ingin permintaan basis data untuk mengubah status siaga - JUST ONE - dan mengembalikan IP server yang akan digunakan. Pilihannya dapat arbitrer: karena status server berubah dengan kueri, tidak masalah siaga mana yang dipilih.

Apakah mungkin membatasi permintaan saya hanya untuk satu pembaruan?

Inilah yang saya miliki sejauh ini:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Postgres tidak menyukai ini. Apa yang bisa saya lakukan secara berbeda?

jauh sekaliupormanorman
sumber
Pilih saja server dalam kode dan tambahkan sebagai tempat yang dibatasi. Ini juga memungkinkan Anda untuk memiliki kondisi tambahan (tertua, terbaru, paling hidup, paling tidak dimuat, dc yang sama, rak yang berbeda, kesalahan paling sedikit) diperiksa terlebih dahulu. Kebanyakan protokol failover memerlukan beberapa bentuk determinisme.
eckes
@ cek Itu ide yang menarik. Dalam kasus saya "memilih server dalam kode" berarti pertama membaca daftar server yang tersedia dari db, dan kemudian memperbarui catatan. Karena banyak contoh aplikasi dapat melakukan tindakan ini, ada kondisi balapan dan operasi atom diperlukan (atau 5 tahun yang lalu). Pilihannya tidak perlu deterministik.
vastlysuperiorman

Jawaban:

125

Tanpa akses tulis bersamaan

Wujudkan seleksi dalam CTE dan gabungkan ke dalam FROMklausa UPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING server_ip;

Saya awalnya memiliki subquery sederhana di sini, tetapi itu dapat menghindari LIMITuntuk rencana permintaan tertentu seperti yang ditunjukkan Feike :

Perencana dapat memilih untuk membuat rencana yang mengeksekusi loop bersarang di atas LIMITingsubquery, menyebabkan lebih UPDATEsdari LIMIT, misalnya:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

Reproduksi kasus uji

Cara untuk memperbaiki hal di atas adalah dengan membungkus LIMITsubquery dalam CTE sendiri, karena CTE terwujud tidak akan mengembalikan hasil yang berbeda pada iterasi yang berbeda dari loop bersarang.

Atau gunakan subquery berkorelasi rendahuntuk kasus sederhana denganLIMIT 1. Lebih sederhana, lebih cepat:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;

Dengan akses tulis bersamaan

Dengan asumsi tingkat isolasi standarREAD COMMITTED untuk semua ini. Level isolasi yang lebih ketat ( REPEATABLE READdan SERIALIZABLE) masih dapat mengakibatkan kesalahan serialisasi. Lihat:

Di bawah beban tulis bersamaan, tambahkan FOR UPDATE SKIP LOCKEDuntuk mengunci baris untuk menghindari kondisi balapan. SKIP LOCKEDditambahkan di Postgres 9.5 , untuk versi yang lebih lama lihat di bawah. Manual:

Dengan SKIP LOCKED, setiap baris yang dipilih yang tidak dapat segera dikunci dilewati. Melewati baris yang dikunci memberikan tampilan data yang tidak konsisten, jadi ini tidak cocok untuk pekerjaan tujuan umum, tetapi dapat digunakan untuk menghindari pertikaian kunci dengan banyak konsumen mengakses tabel seperti antrian.

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;

Jika tidak ada kualifikasi, baris terbuka yang tersisa, tidak ada yang terjadi dalam permintaan ini (tidak ada baris diperbarui) dan Anda mendapatkan hasil kosong. Untuk operasi tidak kritis itu berarti Anda selesai.

Namun, transaksi bersamaan mungkin telah mengunci baris, tetapi kemudian tidak menyelesaikan pembaruan ( ROLLBACKatau alasan lainnya). Yang pasti jalankan pemeriksaan terakhir:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTjuga melihat baris yang terkunci. Sementara itu tidak kembali true, satu atau lebih baris sedang diproses dan transaksi masih bisa dibatalkan. (Atau baris baru telah ditambahkan sementara itu.) Tunggu sebentar, kemudian lingkarkan kedua langkah: ( UPDATEsampai Anda tidak mendapatkan baris kembali; SELECT...) sampai Anda mendapatkannya true.

Terkait:

Tanpa SKIP LOCKEDdi PostgreSQL 9.4 atau lebih lama

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Transaksi bersamaan yang mencoba mengunci baris yang sama diblokir sampai yang pertama melepaskan kuncinya.

Jika yang pertama dibatalkan, transaksi berikutnya mengambil kunci dan hasil secara normal; yang lain dalam antrian terus menunggu.

Jika komitmen pertama, WHEREkondisi dievaluasi kembali dan jika tidak TRUElagi ( statustelah berubah), CTE (agak mengejutkan) tidak mengembalikan baris. Tidak ada yang terjadi. Itu perilaku yang diinginkan ketika semua transaksi ingin memperbarui yang sama berturut-turut .
Tapi tidak ketika setiap transaksi ingin memperbarui yang berikutnya berturut-turut . Dan karena kami hanya ingin memperbarui baris yang arbitrer (atau acak ) , tidak ada gunanya menunggu sama sekali.

Kami dapat membuka blokir situasi dengan bantuan kunci penasihat :

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Dengan cara ini, baris berikutnya yang belum dikunci akan diperbarui. Setiap transaksi mendapat baris baru untuk dikerjakan. Saya mendapat bantuan dari Czech Postgres Wiki untuk trik ini.

idmenjadi bigintkolom unik apa pun (atau jenis apa pun dengan gips seperti int4atau int2)

Jika kunci penasihat digunakan untuk beberapa tabel dalam database Anda secara bersamaan, jelaskan dengan pg_try_advisory_xact_lock(tableoid::int, id)- idmenjadi unik di integersini.
Sejak tableoidadalah bigintkuantitas, dapat secara teoritis melimpah integer. Jika Anda cukup paranoid, gunakan (tableoid::bigint % 2147483648)::int- meninggalkan "tabrakan hash" teoretis untuk ...

Juga, Postgres bebas untuk menguji WHEREkondisi dalam urutan apa pun. Itu bisa menguji pg_try_advisory_xact_lock()dan mendapatkan kunci sebelumnya status = 'standby' , yang dapat menghasilkan kunci penasihat tambahan pada baris yang tidak terkait, di mana status = 'standby'tidak benar. Pertanyaan terkait pada SO:

Biasanya, Anda bisa mengabaikan ini. Untuk menjamin bahwa hanya baris yang memenuhi syarat yang dikunci, Anda dapat membuat sarang predikat dalam CTE seperti di atas atau subquery dengan OFFSET 0peretasan (mencegah inlining) . Contoh:

Atau (lebih murah untuk pemindaian berurutan) memeriksa kondisi dalam CASEpernyataan seperti:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Namun para CASEtrik juga akan tetap Postgres menggunakan indeks pada status. Jika indeks seperti itu tersedia, Anda tidak perlu bersarang ekstra untuk memulai: hanya baris yang memenuhi syarat yang akan dikunci dalam pemindaian indeks.

Karena Anda tidak dapat memastikan bahwa indeks digunakan dalam setiap panggilan, Anda bisa saja:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Secara CASElogis berlebihan, tetapi server tujuan yang dibahas.

Jika perintah adalah bagian dari transaksi panjang, pertimbangkan kunci tingkat sesi yang dapat (dan harus) dilepaskan secara manual. Jadi, Anda dapat membuka kunci segera setelah selesai dengan baris yang terkunci: pg_try_advisory_lock()danpg_advisory_unlock() . Manual:

Setelah diperoleh di tingkat sesi, kunci penasehat diadakan hingga secara eksplisit dilepaskan atau sesi berakhir.

Terkait:

Erwin Brandstetter
sumber