Mengunci Postgres untuk kombinasi UPDATE / INSERT

11

Saya punya dua meja. Salah satunya adalah tabel log; lainnya berisi, pada dasarnya, kode kupon yang hanya dapat digunakan satu kali.

Pengguna harus dapat menebus kupon, yang akan menyisipkan baris ke tabel log dan menandai kupon yang digunakan (dengan memperbarui usedkolom ke true).

Secara alami, ada masalah kondisi ras / keamanan yang jelas di sini.

Saya telah melakukan hal serupa di masa lalu di dunia mySQL. Di dunia itu, saya akan mengunci kedua tabel secara global, melakukan logika dengan aman karena mengetahui bahwa ini hanya dapat terjadi sekali dalam satu waktu, dan kemudian membuka kunci tabel setelah saya selesai.

Apakah ada cara yang lebih baik di Postgres untuk melakukan ini? Secara khusus, saya khawatir kunci itu bersifat global, tetapi tidak harus - saya benar-benar hanya perlu memastikan tidak ada orang lain yang mencoba memasukkan kode tertentu, jadi mungkin penguncian level-baris akan berfungsi?

Rob Miller
sumber

Jawaban:

15

Saya pernah mendengar masalah konkurensi seperti itu di MySQL sebelumnya. Tidak demikian halnya dengan Postgres.

Kunci tingkat baris bawaan pada READ COMMITTEDtingkat isolasi transaksi default sudah cukup.

Saya menyarankan satu pernyataan dengan CTE pemodifikasi data (sesuatu yang MySQL juga tidak miliki) karena lebih mudah untuk menyampaikan nilai dari satu tabel ke tabel lainnya secara langsung (jika Anda memerlukannya). Jika Anda tidak memerlukan apa pun dari coupontabel, Anda dapat menggunakan transaksi dengan laporan terpisah UPDATEdan INSERTjuga.

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

Seharusnya menjadi hal yang langka bahwa lebih dari satu transaksi mencoba menebus kupon yang sama. Mereka memiliki nomor unik, bukan? Lebih dari satu transaksi yang mencoba pada saat yang sama harus jauh lebih jarang. (Mungkin bug aplikasi atau seseorang mencoba permainan sistem?)

Bagaimanapun , UPDATEsatu - satunya berhasil untuk tepat satu transaksi, apa pun yang terjadi. Seorang UPDATEmemperoleh kunci level baris pada setiap baris target sebelum memperbarui. Jika transaksi bersamaan mencoba ke UPDATEbaris yang sama, itu akan melihat kunci di baris dan menunggu sampai transaksi pemblokiran selesai ( ROLLBACKatau COMMIT), kemudian menjadi yang pertama dalam antrian kunci:

  • Jika berkomitmen, periksa kembali kondisinya. Jika masih NOT used, kunci baris dan lanjutkan. Kalau tidak, UPDATEsekarang tidak menemukan baris kualifikasi dan tidak melakukan apa pun , tidak mengembalikan baris, sehingga INSERTjuga tidak melakukan apa pun.

  • Jika dibatalkan, kunci baris dan lanjutkan.

Tidak ada potensi untuk kondisi balapan .

Tidak ada potensi kebuntuan kecuali Anda lebih banyak menulis ke dalam transaksi yang sama atau mengunci lebih banyak baris daripada hanya satu.

Ini INSERTbebas perawatan. Jika, karena suatu kesalahan coupon_idsudah ada dalam logtabel (dan Anda memiliki batasan UNIK atau PK log.coupon_id), seluruh transaksi akan dibatalkan setelah pelanggaran unik. Menunjukkan keadaan ilegal di DB Anda. Jika pernyataan di atas adalah satu-satunya cara untuk menulis ke logtabel, itu seharusnya tidak pernah terjadi.

Erwin Brandstetter
sumber
Memang seharusnya menjadi hal yang langka bahwa lebih dari satu transaksi mencoba untuk menebus kode yang sama, tetapi kecurigaan Anda benar bahwa ini akan terjadi secara eksklusif ketika seseorang mencoba permainan sistem. Terima kasih banyak untuk ini - CTE adalah daya tarik besar bagi saya untuk pindah ke Postgres tetapi saya tidak menyadari bahwa penguncian implisit akan cukup baik untuk ini.
Rob Miller