Gunakan FOR UPDATE SKIP LOCKED dalam kueri yang juga mengubah baris terpilih dari ready menjadi processing. Ini memungkinkan worker konkuren mengklaim baris artikel sintetis yang berbeda, sementara status tersimpan mencegah klaim lain setelah kunci berakhir.

Panduan ini ditujukan bagi pengembang yang menggunakan Terminal di Mac dengan koneksi PostgreSQL yang berfungsi. Anda akan membuat tiga artikel sintetis, memverifikasi klaim konkuren dalam dua sesi basis data, dan menjalankan contoh node-postgres lengkap.

Mengapa dua worker dapat memilih artikel yang sama?

SELECT biasa tidak mencadangkan hasilnya. Dengan tingkat isolasi default PostgreSQL, Read Committed, dua worker dapat membaca baris ready yang sama dan telah di-commit sebelum salah satunya mengubah baris tersebut. Membungkus pembacaan biasa itu dan pembaruan berikutnya dalam transaksi tidak dengan sendirinya membuat pemilihan menjadi eksklusif. PostgreSQL menjelaskan perilaku snapshot ini dalam Transaction Isolation.

Klaim memerlukan dua perlindungan: kunci baris saat memilih artikel dan perubahan status sebelum kunci dilepas. PostgreSQL melepas kunci baris saat transaksi berakhir. Karena itu, pembacaan berkunci yang berdiri sendiri lalu diikuti pemrosesan akan membuat baris tersebut memenuhi syarat lagi setelah transaksi pembacaan berakhir. Lihat Explicit Locking.

Gunakan protokol klaim yang sama di setiap worker. Jalur aplikasi yang membaca artikel ready dan memulai pemrosesan tanpa berhasil mengklaimnya masih dapat menggandakan pekerjaan.

Buat antrean artikel sintetis kecil

Prasyaratnya adalah basis data PostgreSQL yang berjalan, perintah psql yang tersedia di Terminal, dan konfigurasi koneksi yang sudah ada. Untuk bagian JavaScript, siapkan juga rilis Node.js yang masih didukung dan npm. Artikel ini mengasumsikan koneksi basis data sudah berfungsi; cara instalasi bergantung pada konfigurasi Mac yang Anda gunakan.

Fitur SKIP LOCKED yang diperlukan tersedia mulai PostgreSQL 9.5. Itu adalah batas minimum sintaks, bukan rekomendasi untuk menjalankan rilis yang sudah tidak didukung. Gunakan versi PostgreSQL yang masih didukung; dokumentasi terkini yang diambil menyebut PostgreSQL 18 sebagai versi saat ini. Lihat catatan rilis 9.5 dan dokumentasi SELECT terkini.

Buka Terminal dan jalankan psql menggunakan pengaturan koneksi yang biasa Anda pakai. Perintah ini menyediakan prompt interaktif untuk menjalankan SQL dan melihat hasil, sebagaimana dijelaskan dalam dokumentasi psql. Jalankan penyiapan berikut sekali dalam basis data sementara tempat article_claim_demo belum ada. Penyiapan ini sengaja tidak mengganti tabel yang sudah ada.

SHOW server_version;
SHOW transaction_isolation;

CREATE TABLE article_claim_demo (
    id integer PRIMARY KEY,
    title text NOT NULL,
    state text NOT NULL DEFAULT 'ready'
        CHECK (state IN ('ready', 'processing', 'done')),
    rendered_title text
);

INSERT INTO article_claim_demo (id, title) VALUES
    (1, 'Paper shapes'),
    (2, 'Desk arrangements'),
    (3, 'Color combinations');

Contoh-contoh ini mengasumsikan isolasi Read Committed dan tabel sederhana ini tanpa pemicu tambahan. Jangan ubah tiga baris contoh sampai Anda menyelesaikan pengujian dua sesi.

Klaim satu artikel dengan satu pernyataan SQL

Jalankan pernyataan ini setiap kali worker memerlukan satu artikel. Kueri kandidat mengunci baris yang tersedia; pembaruan mencatat klaimnya dan mengembalikan kolom input. Dokumentasi UPDATE PostgreSQL menjelaskan pola CTE-dan-pembaruan ini serta memastikan bahwa RETURNING melaporkan baris yang benar-benar diperbarui.

WITH candidate AS (
    SELECT id
    FROM article_claim_demo
    WHERE state = 'ready'
    ORDER BY id
    LIMIT 1
    FOR UPDATE SKIP LOCKED
)
UPDATE article_claim_demo AS a
SET state = 'processing'
FROM candidate AS c
WHERE a.id = c.id
  AND a.state = 'ready'
RETURNING a.id, a.title;

Kunci berada di dalam CTE kandidat. SKIP LOCKED memungkinkan worker lain melewati kandidat yang tidak dapat segera dikuncinya. LIMIT 1 membatasi setiap klaim, dan penggabungan dengan kunci utama menentukan satu target pembaruan. PostgreSQL secara eksplisit mendokumentasikan penguncian di dalam kueri WITH dan penggunaan baris yang dilewati untuk antrean di SELECT: Klausul Penguncian.

Tanpa BEGIN yang eksplisit, PostgreSQL menjalankan pernyataan dalam transaksinya sendiri. Pernyataan yang berhasil akan meng-commit pembaruan terkait pemilihan dan perubahan status sebelum pemrosesan berikutnya dimulai. Setelah commit, kunci baris dilepas, tetapi state = 'ready' mengecualikan artikel tersebut. Lihat Transactions.

Proses hanya artikel yang dikembalikan oleh pernyataan klaim. Hasil kosong berarti percobaan ini tidak memperoleh artikel; itu tidak membuktikan bahwa semua pekerjaan telah selesai. Worker lain mungkin sedang memegang baris yang memenuhi syarat. Selain itu, SKIP LOCKED melewati waktu tunggu kunci baris, sedangkan kunci tingkat tabel masih dapat menyebabkan penantian. Batasan ini didokumentasikan dalam SELECT.

Verifikasi perilaku klaim dalam dua sesi Terminal

Gunakan dua jendela Terminal yang terhubung melalui psql ke basis data yang sama. Mulailah dengan tiga baris ready awal. Urutan terkontrol berikut membiarkan setiap transaksi tetap terbuka agar Anda dapat mengamati tumpang tindihnya secara langsung.

  1. Di sesi A, jalankan BEGIN ISOLATION LEVEL READ COMMITTED;, lalu tempelkan pernyataan klaim lengkap di atas. Biarkan transaksi tetap terbuka. Dengan data contoh yang belum diubah, ID yang dikembalikan seharusnya 1.
  2. Di sesi B, jalankan BEGIN ISOLATION LEVEL READ COMMITTED;, lalu tempelkan pernyataan klaim yang sama. ID yang dikembalikan seharusnya 2 saat A masih memegang ID 1.
  3. Jalankan ROLLBACK; di sesi B, lalu ROLLBACK; di sesi A.
  4. Di salah satu sesi, jalankan kueri pemeriksaan di bawah. Kedua klaim dibatalkan, jadi ketiga artikel seharusnya kembali berstatus ready.
SELECT id, title, state, rendered_title
FROM article_claim_demo
ORDER BY id;

Hasil ini adalah hasil yang diperkirakan berdasarkan perilaku penguncian dan rollback yang terdokumentasi, bukan laporan pengujian basis data yang telah dijalankan. Rollback membatalkan setiap pembaruan status dan melepas kuncinya. Jika Anda menggunakan COMMIT, artikel-artikel tersebut akan tetap berstatus processing dan tidak akan dipilih oleh klaim berikutnya. Lihat Explicit Locking dan Transactions.

Pengujian ini menunjukkan dua hal yang berguna: sesi B beralih melewati artikel yang sedang dikunci, dan perubahan status yang di-commit membuat artikel tidak masuk klaim berikutnya. Uji kedua sifat ini saat menyesuaikan worker yang sudah ada.

Jalankan tiga worker dengan node-postgres

Setelah melakukan rollback pada kedua transaksi pengujian, buat direktori proyek lokal baru di Terminal. Instal paket menggunakan perintah instalasi node-postgres yang terdokumentasi:

mkdir article-claim-demo
cd article-claim-demo
npm init -y
npm install pg

Simpan program lengkap berikut sebagai workers.mjs. Program ini menggunakan lingkungan koneksi basis data yang sama dengan psql; node-postgres mendokumentasikan variabel koneksi bersama ini dalam Connecting. Setiap worker mencoba satu klaim, mengubah judul sintetis menjadi huruf kapital, lalu menyimpan hasilnya.

import pg from 'pg';

const { Pool } = pg;
const pool = new Pool({ max: 3 });

const claimSQL = `
WITH candidate AS (
  SELECT id
  FROM article_claim_demo
  WHERE state = 'ready'
  ORDER BY id
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE article_claim_demo AS a
SET state = 'processing'
FROM candidate AS c
WHERE a.id = c.id AND a.state = 'ready'
RETURNING a.id, a.title;
`;

async function worker(label) {
  const claimed = await pool.query(claimSQL);

  if (claimed.rows.length === 0) {
    console.log(`${label}: no row available now`);
    return;
  }

  const article = claimed.rows[0];
  const renderedTitle = article.title.toUpperCase();

  const completed = await pool.query(
    `UPDATE article_claim_demo
     SET rendered_title = $2, state = 'done'
     WHERE id = $1 AND state = 'processing'
     RETURNING id`,
    [article.id, renderedTitle]
  );

  if (completed.rowCount !== 1) {
    throw new Error(`${label}: completion did not update one row`);
  }

  console.log(`${label}: completed article ${article.id}`);
}

try {
  const results = await Promise.allSettled([
    worker('A'),
    worker('B'),
    worker('C')
  ]);

  for (const result of results) {
    if (result.status === 'rejected') {
      console.error(result.reason);
      process.exitCode = 1;
    }
  }
} finally {
  await pool.end();
}

Jalankan node workers.mjs, lalu jalankan kembali kueri pemeriksaan di psql. Dengan tiga artikel ready yang belum diubah dan tanpa aktivitas lain, ketiga status diperkirakan menjadi done, dengan nilai huruf kapital di rendered_title. Label worker dan urutan keluaran konsol tidak harus sesuai dengan urutan artikel.

Penggunaan pool.query sesuai di sini karena setiap klaim adalah satu pernyataan lengkap dan setiap penyelesaian adalah pernyataan terpisah. Pool mengelola peminjaman dan pengembalian koneksi; pool.end() menutup pool setelah semua percobaan selesai. Lihat Pooling.

Kueri penyelesaian mengirimkan nilainya secara terpisah menggunakan $1 dan $2, mengikuti Queries. Program memeriksa array rows yang dikembalikan untuk mengetahui apakah klaim kosong, dan memeriksa rowCount untuk jumlah baris penyelesaian yang diperbarui, sesuai dengan pg.Result.

JavaScript telah melewati pemeriksaan sintaks selama persiapan. Program PostgreSQL tidak tersedia di lingkungan riset, sehingga SQL dan perilaku konkuren basis data diperiksa berdasarkan dokumentasi, tetapi tidak dijalankan di sini.

Tentukan batas transaksi yang tepat dan tangani pekerjaan yang belum selesai

Contoh ini meng-commit klaim singkat sebelum melakukan transformasi judul. Cara ini membuat masa berlaku kunci tetap singkat, tetapi menetapkan batas pemulihan: jika worker berhenti setelah mengklaim dan sebelum menyelesaikan pekerjaan, artikel tetap berstatus processing. Menjalankan ulang program tidak akan mengklaimnya lagi.

Untuk demonstrasi desktop ini, hentikan semua worker dan periksa artikel processing sebelum sengaja membuatnya memenuhi syarat lagi. Jangan mengatur ulang statusnya secara otomatis saat worker sebelumnya mungkin masih menyelesaikan pekerjaan: contoh ini tidak memiliki kolom generasi klaim untuk membedakan percobaan sebelumnya dari percobaan berikutnya. Pencegahan klaim ganda saja tidak menyediakan pemulihan otomatis atau pemrosesan tepat satu kali.

Gunakan panduan keputusan ini saat menyesuaikan contoh:

Tugas Anda Pendekatan transaksi Konsekuensi
Klaim artikel, lalu lakukan pekerjaan aplikasi yang lebih lama Commit klaim dalam satu pernyataan terlebih dahulu Pertahankan status processing yang terlihat dan rancang pemulihan secara terpisah
Lakukan transformasi kecil sepenuhnya di PostgreSQL Klaim dan selesaikan dalam satu transaksi Perubahan basis data dapat di-commit atau di-rollback bersama-sama
Jalankan beberapa pernyataan SQL sebagai satu transaksi di Node.js Pinjam satu klien dan gunakan sepanjang transaksi Gunakan koneksi yang sama untuk BEGIN, pernyataan, dan COMMIT atau ROLLBACK

Untuk pendekatan terakhir, node-postgres mengharuskan setiap pernyataan dalam transaksi menggunakan klien yang sama dan telah dipinjam. Pemanggilan pool.query yang terpisah mungkin menggunakan koneksi berbeda, sehingga tidak dapat membentuk transaksi tersebut. Lepaskan klien setelah selesai atau melakukan rollback. Lihat Transactions dan Pooling.

Nyatakan asumsi isolasi yang ditunjukkan secara eksplisit. Repeatable Read dan Serializable dapat menghasilkan galat ketika baris yang akan dikunci telah berubah sejak transaksi dimulai; aplikasi yang menggunakan tingkat tersebut memerlukan penanganan percobaan ulang transaksi. PostgreSQL menjelaskan batasan ini dalam Explicit Locking.

Untuk tugas artikel sintetis ini, urutan pentingnya jelas: pilih dan kunci baris yang memenuhi syarat, ubah statusnya dalam pernyataan yang sama, lalu proses hanya artikel yang dikembalikan setelah klaim berhasil. Verifikasi tumpang tindih dalam dua sesi, lalu tentukan cara aplikasi Anda menangani artikel yang worker-nya tidak pernah mencatat penyelesaian.

Sumber