Tanpa akses tulis bersamaan
Wujudkan seleksi dalam CTE dan gabungkan ke dalam FROM
klausa 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 LIMIT
untuk rencana permintaan tertentu seperti yang ditunjukkan Feike :
Perencana dapat memilih untuk membuat rencana yang mengeksekusi loop bersarang di atas LIMITing
subquery, menyebabkan lebih UPDATEs
dari 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 LIMIT
subquery 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 READ
dan SERIALIZABLE
) masih dapat mengakibatkan kesalahan serialisasi. Lihat:
Di bawah beban tulis bersamaan, tambahkan FOR UPDATE SKIP LOCKED
untuk mengunci baris untuk menghindari kondisi balapan. SKIP LOCKED
ditambahkan 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 ( ROLLBACK
atau alasan lainnya). Yang pasti jalankan pemeriksaan terakhir:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
juga 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: ( UPDATE
sampai Anda tidak mendapatkan baris kembali; SELECT
...) sampai Anda mendapatkannya true
.
Terkait:
Tanpa SKIP LOCKED
di 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, WHERE
kondisi dievaluasi kembali dan jika tidak TRUE
lagi ( status
telah 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.
id
menjadi bigint
kolom unik apa pun (atau jenis apa pun dengan gips seperti int4
atau int2
)
Jika kunci penasihat digunakan untuk beberapa tabel dalam database Anda secara bersamaan, jelaskan dengan pg_try_advisory_xact_lock(tableoid::int, id)
- id
menjadi unik di integer
sini.
Sejak tableoid
adalah bigint
kuantitas, dapat secara teoritis melimpah integer
. Jika Anda cukup paranoid, gunakan (tableoid::bigint % 2147483648)::int
- meninggalkan "tabrakan hash" teoretis untuk ...
Juga, Postgres bebas untuk menguji WHERE
kondisi 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 0
peretasan (mencegah inlining) . Contoh:
Atau (lebih murah untuk pemindaian berurutan) memeriksa kondisi dalam CASE
pernyataan seperti:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Namun para CASE
trik 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 CASE
logis 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: