Menangani konkurensi PostgreSQL menggunakan Rails find_or_create

Untuk beberapa alasan kode ini dapat membuat game duplikat jika pengguna berbeda menjalankannya pada saat yang sama:

game = Game.find_or_create_by(
    status: Game::STATUS[:waiting],
    category_id: params[:category_id],
    private: 0
) do |g|
  is_new = true
  g.user = current_user
end

Saya tidak tahu dengan jelas apa masalahnya, tapi mungkin ini tentang proses Unicorn berbeda yang menggunakan koneksi database berbeda sehingga transaksi dapat berjalan secara paralel.

Kalau iya, saya butuh cara yang tepat untuk menghindarinya, mungkin sebaiknya saya menggunakan transaksi Rails atau kunci Postgres, tapi saya sangat membutuhkan contoh penggunaannya.

Terima kasih.


person Alexander Zinchuk    schedule 22.05.2014    source sumber
comment
Saya yakin masalah Anda ada di pengguna_saat ini, tetapi pertama-tama ubah find_or_create_by menjadi create_by karena is_new benar.   -  person Hitham S. AlQadheeb    schedule 22.05.2014
comment
Tambahkan dan indeks ke database untuk menghindari entri duplikat?   -  person manu29.d    schedule 22.05.2014
comment
manu29.d, sebenarnya saya tidak bisa menggunakan indeks UNIK pada [status, kategori_id, pribadi]: karena saya hanya memerlukan catatan unik ketika statusnya 0 (MENUNGGU). Untuk status lain saya dapat memiliki catatan, menduplikasi kolom tersebut.   -  person Alexander Zinchuk    schedule 22.05.2014
comment
Hitham S. AlQadheeb, saya tidak mengerti maksud Anda. Apa masalah dengan pengguna_saat ini dan apa itu create_by? is_new digunakan dalam kode berikut, hanya untuk mengetahui bahwa catatan baru saja dibuat.   -  person Alexander Zinchuk    schedule 22.05.2014


Jawaban (1)


Hal ini dapat terjadi pada tingkat konkurensi yang tinggi.

Menurut dokumen Rails, kueri berikut akan dijalankan:

SELECT * FROM games WHERE status = 'waiting' AND ... LIMIT 1;
INSERT INTO games (status, ...) VALUES ('waiting', ...);

Yang kedua hanya berjalan, ketika yang pertama belum mengembalikan satu baris pun.

Mungkin saja, jika dua (atau lebih) koneksi memulai kueri pertama dalam beberapa mikrodetik, beberapa proses akan membuat banyak entri. Untuk mencegahnya, Anda dapat menggunakan ACCESS EXCLUSIVE lock pada tabel tersebut , atau gunakan kunci penasehat khusus.

Anda juga dapat menggunakan beberapa indeks unik, untuk mencegah banyak entri dimasukkan ke dalam database Anda, namun jika digunakan sendiri, hal itu akan menyebabkan pengecualian SQL dalam situasi ini.

EDIT:

ACCESS EXCLUSIVE kunci dapat diperoleh melalui LOCK perintah.

Kunci penasehat dapat diperoleh melalui penggunaan pg_advisory_lock(id) fungsi.

Keduanya mengharuskan Anda menjalankan perintah SQL sewenang-wenang.


Cara lain adalah dengan menggunakan kueri khusus dengan:

  1. masukkan hanya jika tidak ada (& kembalikan dengan semua bidang)
  2. pilih hanya jika tidak dimasukkan

Sesuatu seperti:

INSERT INTO games (status, category_id, private)
     SELECT 'waiting', 2, 0
      WHERE NOT EXISTS (
             SELECT 1
               FROM games
              WHERE status = 'waiting'
                AND category_id = 2
                AND private = 0
       )
  RETURNING *;

-- only select, when this not inserted anything

SELECT *
  FROM games
 WHERE status = 'waiting'
   AND category_id = 2
   AND private = 0
person pozs    schedule 22.05.2014
comment
Terima kasih. Bisakah Anda memberikan sedikit contoh tentang cara menerapkan jenis kunci tersebut di kode Rails saya? Bukankah transaksi Rails berguna dalam kasus saya? - person Alexander Zinchuk; 22.05.2014