Gunakan FOR UPDATE SKIP LOCKED di dalam kueri yang juga mengubah baris yang dipilih dari ready ke processing. Ini membolehkan pekerja serentak mengklaim baris artikel sintetik yang berbeza, manakala keadaan yang disimpan mencegah klaim lain selepas kunci tamat.

Panduan ini adalah untuk pembangun yang menggunakan Terminal di Mac dengan sambungan PostgreSQL yang berfungsi. Anda akan mencipta tiga artikel sintetik, mengesahkan klaim serentak dalam dua sesi pangkalan data, dan menjalankan contoh node-postgres yang lengkap.

Mengapa dua pekerja boleh memilih artikel yang sama?

Sebuah SELECT biasa tidak menempah keputusannya. Di bawah pengasingan Read Committed lalai PostgreSQL, dua pekerja boleh membaca baris ready yang sama yang telah dikommit sebelum mana-mana mengubahnya. Membungkus bacaan biasa itu dan kemas kini kemudian dalam sebuah transaksi tidak menjadikan pemilihan itu eksklusif. PostgreSQL mendokumentasikan tingkah laku snapshot dalam Transaction Isolation.

Klaim memerlukan dua perlindungan: kunci baris semasa memilih artikel, dan perubahan keadaan sebelum melepaskan kunci itu. PostgreSQL melepaskan kunci baris apabila transaksi tamat. Oleh itu, bacaan kunci berterusan diikuti oleh pemprosesan akan meninggalkan baris layak sekali lagi setelah transaksi bacaan itu tamat. Lihat Explicit Locking.

Gunakan protokol klaim yang sama dalam setiap pekerja. Laluan aplikasi yang membaca artikel ready dan memulakan pemprosesan tanpa berjaya mengklaimnya masih boleh menduplikasi kerja.

Ciptakan antrian artikel sintetik kecil

Prasyaratnya adalah pangkalan data PostgreSQL yang berjalan, arahan psql yang tersedia di Terminal, dan konfigurasi sambungan sedia ada. Untuk bahagian JavaScript, juga memiliki pelancaran Node.js yang disokong kini dan npm dipasang. Artikel ini bermula dari sambungan pangkalan data yang berfungsi; pemasangan berbeza mengikut tetapan Mac sedia ada anda.

Ciri SKIP LOCKED yang diperlukan tiba di PostgreSQL 9.5. Itu adalah minimum sintaks, bukan syaran untuk menjalankan pelancaran yang tidak disokong. Gunakan versi PostgreSQL yang disokong; dokumentasi semasa yang diperoleh mengenal pasti PostgreSQL 18 sebagai semasa. Lihat nota pelepasan 9.5 dan dokumentasi SELECT semasa.

Buka Terminal dan jalankan psql menggunakan tetapan sambungan biasa anda. Ia menyediakan prom interaktif untuk mengeksekusi SQL dan melihat keputusan, sebagaimana yang dinyatakan dalam dokumentasi psql. Jalankan susun atur berikut sekali dalam pangkalan data cetak di mana article_claim_demo belum wujud. Ia sengaja tidak menggantikan jadual sedia 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 mengandaikan pengasingan Read Committed dan jadual mudah ini tanpa pemicu tambahan. Kekalkan tiga baris sampel tidak berubah sehingga anda menyiapkan ujian dua sesi.

Klaim satu artikel dengan satu pernyataan SQL

Jalankan pernyataan ini setiap kali seorang pekerja memerlukan satu artikel. Kueri calon mengunci baris yang tersedia; kemas kini mencatat klaimnya dan mengembalikan medan inputnya. Dokumentasi UPDATE PostgreSQL memperihalkan corak CTE-dan-kemas-ini ini dan mengesahkan bahawa RETURNING melaporkan baris yang sebenarnya dikemas kini.

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 itu berada di dalam CTE calon. SKIP LOCKED membolehkan pekerja lain melewati calon yang tidak dapat dikunci dengan serta-merta. LIMIT 1 menghadkan setiap klaim, dan penyertaan kunci utama mengenal pasti satu sasaran kemas kini. PostgreSQL secara jelas mendokumentasikan penguncian di dalam kueri WITH dan penggunaan baris yang dilangakkan seperti antrian dalam SELECT: The Locking Clause.

Tanpa BEGIN yang jelas, PostgreSQL mengeksekusi pernyataan dalam transaksinya sendiri. Pernyataan yang berjaya mengkommit kedua-dua kemas kini yang berkaitan dengan pemilihan dan perubahan keadaannya sebelum pemprosesan seterusnya bermula. Selepas komit, kunci baris hilang, tetapi state = 'ready' mengecualikan artikel itu. Lihat Transactions.

Hanya proses artikel yang dikembalikan oleh pernyataan klaim. Keputusan kosong bermakna cubaan ini tidak mendapatkan artikel; ia tidak menentukan bahawa semua kerja selesai. Pekerja lain mungkin sedang memegang baris yang layak. Juga, SKIP LOCKED mengelakkan tunggu kunci-baris, manakala kunci peringkat jadual masih boleh menyebabkan menunggu. Sempadan ini didokumentasikan dalam SELECT.

Sahkan tingkah laku klaim dalam dua sesi Terminal

Gunakan dua tetingkap Terminal yang disambungkan melalui psql ke pangkalan data yang sama. Mulakan dengan tiga baris ready asal. Urutan terkawal berikut mengekalkan setiap transaksi terbuka supaya anda boleh memperhatikan tumpang tindih secara langsung.

  1. Dalam sesi A, eksekusi BEGIN ISOLATION LEVEL READ COMMITTED;, kemudian tampal pernyataan klaim lengkap di atas. Biarkan transaksi terbuka. Dengan data sampel yang tidak disentuh, ID yang dikembalikan sepatutnya 1.
  2. Dalam sesi B, eksekusi BEGIN ISOLATION LEVEL READ COMMITTED;, kemudian tampal pernyataan klaim yang sama. Ia sepatutnya mengembalikan ID 2 manakala A masih memegang ID 1.
  3. Eksekusi ROLLBACK; dalam sesi B, kemudian ROLLBACK; dalam sesi A.
  4. Dalam mana-mana sesi, jalankan kueri pemeriksaan di bawah. Kedua-dua klaim digulung balik, jadi ketiga-tiga artikel sepatutnya kembali ready.
SELECT id, title, state, rendered_title
FROM article_claim_demo
ORDER BY id;

Ini adalah keputusan yang dijangka yang terbit dari tingkah laku kunci dan gulung balik yang didokumentasikan, bukan dari jalankan ujian pangkalan data yang dilaporkan. Menggulung balik membatalkan setiap kemas kini keadaan dan melepaskan kuncinya. Jika anda menggunakan COMMIT sebagai gantinya, artikel-artikel itu kekal processing dan tidak akan dipilih oleh klaim-klaim kemudian. Lihat Explicit Locking dan Transactions.

Ujian ini membezakan dua pemerhatian yang berguna: sesi B maju melewati artikel yang dikunci kini, dan perubahan keadaan yang dikommit mengekalkan artikel keluar dari klaim masa hadapan. Uji kedua-dua sifat ini semasa menyesuaikan pekerja sedia ada.

Jalankan tiga pekerja dengan node-postgres

Selepas menggulung balik kedua-dua transaksi ujian, ciptakan direktori projek tempatan baru di Terminal. Pasang pakej dengan arahan pemasangan node-postgres yang didokumentasikan:

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

Simpan program lengkap berikut sebagai workers.mjs. Ia menggunakan persekitaran sambungan pangkalan data yang sama seperti psql; node-postgres mendokumentasikan pemboleh ubah sambungan berkongsinya dalam Connecting. Setiap pekerja mencuba satu klaim, menukar tajuk sintetik kepada huruf besar, dan menyimpan keputusannya.

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, kemudian jalankan kueri pemeriksaan lagi di psql. Dengan tiga artikel ready yang tidak disentuh dan tiada aktiviti lain, jangka ketiga-tiga state menjadi done, dengan nilai huruf besar dalam rendered_title. Label pekerja dan urutan konsol tidak perlu sepadan dengan urutan artikel.

Penggunaan pool.query adalah sesuai di sini kerana setiap klaim adalah satu pernyataan lengkap dan setiap penyelesaian adalah pernyataan tersendiri. Kolam menguruskan keluar masuk sambungan; pool.end() menutup kolam selepas cubaan-cubaan selesai. Lihat Pooling.

Kueri penyelesaian melewatkan nilai-nilainya secara berasingan menggunakan $1 dan $2, mengikut Queries. Program memeriksa larik rows yang dikembalikan untuk klaim kosong dan memeriksa rowCount untuk jumlah baris penyelesaian yang dikemas kini, selaras dengan pg.Result.

JavaScript telah lulus pemeriksaan sintaks semasa persediaan. Pelaksana PostgreSQL tidak tersedia dalam persekitaran penyelidikan, jadi SQL dan tingkah laku pangkalan data serentak diperiksa terhadap dokumentasi tetapi tidak dijalankan di sini.

Pilih sempadan transaksi yang betul dan urus kerja yang tidak selesai

Contoh ini mengkommit klaim pendek sebelum melakukan transformasi tajuk. Itu mengekalkan hayat kunci pendek, tetapi mencipta sempadan pemulihan yang sengaja: jika pekerja berhenti selepas mengklaim dan sebelum penyelesaian, artikel kekal processing. Menjalankan semula program tidak akan menebusnya kembali.

Untuk demonstrasi desktop ini, hentikan semua pekerja dan periksa mana-mana artikel processing sebelum sengaja menjadikannya layak lagi. Jangan set semula mereka secara automatik manakala pekerja sebelumnya mungkin masih selesai: contoh ini tiada medan penjanaan klaim untuk membezakan cubaan sebelumnya dari yang kemudian. Pencegahan klaim ganda sahaja tidak menyediakan pemulihan automatik atau pemprosesan tepat-sekali.

Gunakan bantuan keputusan ini semasa menyesuaikan contoh:

Tugas anda Pendekatan transaksi Kesan
Klaim artikel, lalu lakukan kerja aplikasi yang lebih lama Komit klaim pernyataan tunggal terlebih dahulu Kekalkan state processing yang nampak dan rekabentuk pemulihan secara berasingan
Lakukan transformasi kecil sepenuhnya di PostgreSQL Klaim dan lengkapkan dalam satu transaksi Perubahan pangkalan data boleh dikomit atau digulung balik bersama-sama
Jalankan beberapa pernyataan SQL sebagai satu transaksi dalam Node.js Semak keluar satu klien dan gunakannya sepanjang Kekalkan sambungan yang sama melalui BEGIN, pernyataan, dan COMMIT atau ROLLBACK

Untuk pendekatan terakhir, node-postgres menghendaki setiap pernyataan dalam transaksi menggunakan klien yang disemak keluar yang sama. Panggilan pool.query yang terpisah mungkin menggunakan sambungan berbeza, jadi mereka tidak dapat membentuk transaksi itu. Lepaskan klien selepas selesai atau menggulung balik. Lihat Transactions dan Pooling.

Jadikan andaian pengasingan yang ditunjukkan secara jelas. Repeatable Read dan Serializable boleh menghasilkan ralat apabila baris yang hendak dikunci telah berubah sejak transaksi bermula; aplikasi yang menggunakan tahap-tahap itu memerlukan penanganan cuba semula transaksi. PostgreSQL memperihalkan sempadan itu dalam Explicit Locking.

Untuk tugas artikel sintetik ini, urutan yang penting adalah konkret: pilih dan kunci baris yang layak, tukar keadaannya dalam pernyataan yang sama, dan proses hanya artikel yang dikembalikan selepas klaim berjaya. Sahkan tumpang tindih dalam dua sesi, kemudian tentukan bagaimana aplikasi anda akan mengendalikan artikel yang pekerjaannya tidak pernah mencatat penyelesaian.

Sumber