Untuk menolak penyimpanan dokumen lapuk, sertakan semakan asal yang dimuatkan oleh editor dalam syarat kemas kini, kemudian tingkatkannya secara atomik. SERIALIZABLE sahaja tidak dapat menyatakan keperluan itu apabila dokumen dimuatkan dalam transaksi terdahulu.

Tutorial ini ditujukan kepada pembangun yang bekerja dalam Terminal pada Mac dan memerlukan editor artikel PostgreSQL menolak teks pengganti yang sudah lapuk. Tutorial ini menggunakan baris artikel sintetik dan demonstrasi node-postgres yang lengkap. Tugasnya khusus: simpan hanya jika semakan tersimpan masih sepadan dengan semakan yang menjadi asas draf yang dihantar.

Mengapa SERIALIZABLE boleh membenarkan teks pengganti lapuk?

PostgreSQL mentakrifkan pelaksanaan bersiri sebagai menghasilkan kesan yang setara dengan menjalankan transaksi yang dikomit satu demi satu dalam suatu tertib. Jaminan itu berkaitan dengan transaksi pangkalan data dan operasinya. PostgreSQL: Transaction Isolation.

Pertimbangkan urutan ilustrasi ini:

  1. Editor memuatkan artikel 1 pada semakan 1, lalu transaksi baca tamat.
  2. Simpanan lain menggantikan teks artikel dan mengomit semakan 2.
  3. Editor asal menghantar teks pengganti berdasarkan semakan 1.
  4. Transaksi bersiri baharu mengemas kini artikel 1 dengan menggunakan ID sahaja.

Transaksi terakhir boleh berjaya: transaksi-transaksi itu memang sudah berjalan secara berurutan. Tiada apa-apa dalam kemas kini yang hanya disyaratkan oleh id = 1 menyatakan bahawa teks yang dihantar memerlukan semakan 1. Ini ialah penerapan takrif kebersirian yang didokumenkan, bukannya pengecualian daripadanya.

Transaksi yang bertindih berbeza keadaannya. PostgreSQL boleh menolak transaksi bersiri apabila kemas kini serentak bercanggah dengan petikan datanya atau apabila kebergantungan menghalang pelaksanaan bersiri. Petikan data ditetapkan oleh pernyataan pertama yang bukan kawalan transaksi; sekadar mengeluarkan BEGIN lebih awal tidak mengekalkan bacaan editor terdahulu. PostgreSQL: Transaction Isolation.

Oleh itu, soalan diagnostiknya ialah: Di manakah simpanan secara jelas mensyaratkan semakan yang dimuatkan oleh editor? Jika tiada, menukar aras pengasingan tidak akan menambah syarat yang tiada itu.

Apakah yang perlu dijamin oleh semakan semakan?

Anggap setiap draf yang dimuatkan sebagai pasangan: teks yang boleh disunting dan semakan asasnya. Simpanan membawa ID artikel, teks pengganti dan semakan asas yang tidak berubah. Kemas kini pangkalan data mesti memadankan kedua-dua ID dan semakan itu, menggantikan teks, serta meningkatkan semakan tersimpan dalam pernyataan yang sama.

Sebagai contoh, syarat WHERE id = $1 AND revision = $2 menyatakan prasyarat, manakala SET body = $3, revision = revision + 1 melaksanakan peralihan. PostgreSQL mengemas kini baris yang memenuhi syarat itu, dan RETURNING memberikan nilai daripada baris yang benar-benar dikemas kini. Kemas kini sifar baris ialah arahan SQL yang berjaya, jadi aplikasi mesti mentafsirkannya secara jelas. PostgreSQL: UPDATE.

Corak ini sering dipanggil kawalan konkurensi optimistik atau compare-and-swap. Dalam reka bentuk artikel ini, setiap operasi yang mengubah kandungan artikel yang boleh disunting mesti meningkatkan semakan. Pastikan semakan tidak bernilai null, jangan tetapkan semula semakan, dan jangan gunakan semula identiti artikel dengan token semakan lama. Jika tidak, kesamaan boleh tersalah menunjukkan bahawa draf lama masih sepadan.

Bacaan berasingan yang diikuti kemas kini menggunakan ID sahaja tidak memasukkan keperluan itu ke dalam operasi tulis. Begitu juga, mendapatkan semakan terkini sejurus sebelum menyimpan lalu menggantikannya dengan semakan yang dihantar akan mengubah maksud permintaan: tindakan itu membenarkan penggantian versi yang tidak digunakan oleh editor sebagai asasnya.

Jalankan contoh sintetik lengkap pada Mac anda

Sediakan tika PostgreSQL yang sedang berjalan, pangkalan data kerja sedia ada, serta pemasangan Node.js LTS semasa atau yang disokong dengan npm. Buka Terminal, cipta folder kerja, dan pasang pemacu dengan arahan npm install pg yang didokumenkan. node-postgres: Introduction and Install.

mkdir revision-write-demo
cd revision-write-demo
npm install pg

Simpan program lengkap berikut sebagai revision-demo.mjs dalam folder itu. Program ini menggunakan import lalai yang serasi dengan ESM dan didokumenkan. Kedua-dua Client dan Pool boleh membaca tetapan sambungan PostgreSQL daripada pemboleh ubah persekitaran; konfigurasikan tetapan itu untuk pangkalan data kerja sedia ada anda sebelum menjalankannya. node-postgres: ESM, node-postgres: Connecting.

Program ini membandingkan dua artikel sintetik. Artikel 1 menerima dua simpanan bersiri tanpa syarat. Artikel 2 menerima dua simpanan yang disemak berdasarkan semakan asal yang sama. Jadual sementara mengasingkan demonstrasi ini daripada jadual artikel kekal. Sintaks JavaScript telah diperiksa dengan Node.js; demonstrasi pangkalan data tidak dijalankan dalam persekitaran penyelidikan.

import pg from 'pg';
import assert from 'node:assert/strict';
const { Client } = pg;
const client = new Client();

async function saveArticle(id, expectedRevision, body) {
  for (let attempt = 1; attempt <= 3; attempt += 1) {
    let active = false;
    try {
      await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
      active = true;
      const result = await client.query(
        `UPDATE revision_demo_articles
         SET body = $3, revision = revision + 1
         WHERE id = $1 AND revision = $2
         RETURNING id, body, revision`,
        [id, expectedRevision, body]
      );
      if (result.rowCount === 0) {
        await client.query('ROLLBACK');
        active = false;
        return { status: 'stale-or-missing' };
      }
      await client.query('COMMIT');
      active = false;
      return { status: 'saved', article: result.rows[0] };
    } catch (error) {
      if (active) await client.query('ROLLBACK');
      if (error.code !== '40001' || attempt === 3) throw error;
    }
  }
}

await client.connect();
try {
  await client.query(`
    CREATE TEMP TABLE revision_demo_articles (
      id integer PRIMARY KEY,
      body text NOT NULL,
      revision integer NOT NULL CHECK (revision > 0)
    )
  `);
  await client.query(
    `INSERT INTO revision_demo_articles (id, body, revision)
     VALUES (1, 'Opening draft', 1), (2, 'Opening draft', 1)`
  );
  const loaded = await client.query(
    'SELECT id, body, revision FROM revision_demo_articles ORDER BY id'
  );
  const expectedRevision = loaded.rows[1].revision;

  // Illustrative blind saves: each is a separate transaction.
  for (const body of ['First edit', 'Stale replacement']) {
    await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
    try {
      await client.query(
        `UPDATE revision_demo_articles
         SET body = $1, revision = revision + 1 WHERE id = 1`,
        [body]
      );
      await client.query('COMMIT');
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    }
  }

  const first = await saveArticle(2, expectedRevision, 'First edit');
  const stale = await saveArticle(2, expectedRevision, 'Stale replacement');
  assert.equal(first.status, 'saved');
  assert.equal(first.article.revision, 2);
  assert.equal(stale.status, 'stale-or-missing');

  const final = await client.query(
    'SELECT id, body, revision FROM revision_demo_articles ORDER BY id'
  );
  assert.deepEqual(final.rows, [
    { id: 1, body: 'Stale replacement', revision: 3 },
    { id: 2, body: 'First edit', revision: 2 }
  ]);
  console.table(final.rows);
  console.log(stale.status);
} finally {
  await client.end();
}

Jika nama pangkalan data kerja sedia ada ialah revision_demo, jalankan arahan di bawah. Gantikan dengan nama pangkalan data sebenar anda dan kekalkan sebarang tetapan persekitaran sambungan yang diperlukan oleh tika setempat anda. node-postgres: Connecting.

PGDATABASE=revision_demo node revision-demo.mjs

Nilai akhir yang dijangka ialah:

Artikel Kandungan Semakan Maksud
1 Stale replacement 3 Kedua-dua simpanan tanpa syarat dikomit
2 First edit 2 Simpanan kedua yang disemak berdasarkan semakan ditolak

Status akhir yang dicetak sepatutnya ialah stale-or-missing.

Urutan ini sengaja mengelakkan konkurensi yang bergantung pada masa. Urutan ini menunjukkan batas utamanya secara langsung: teks pengganti lapuk boleh berjaya walaupun setiap transaksi simpanan bersiri, kerana simpanan dijalankan dalam tertib bersiri yang sah.

Bagaimanakah node-postgres patut mengendalikan setiap hasil?

Program ini menghantar nilai secara berasingan daripada teks SQL menggunakan API pertanyaan berparameter yang didokumenkan oleh pemacu. Program ini juga menyemak rowCount, yang melaporkan bilangan baris yang diproses oleh kemas kini; tatasusunan rows yang kosong sahaja bukan pengganti umum untuk menyemak kiraan kemas kini. node-postgres: Queries, node-postgres: pg.Result.

Gunakan jadual keputusan berikut apabila menyesuaikan fungsi itu:

Hasil Tindakan aplikasi
Satu baris dikemas kini dan komit berjaya Terima simpanan dan kekalkan semakan yang dipulangkan
Tiada baris dikemas kini Tolak simpanan ini sebagai lapuk atau tiada; kekalkan draf yang dihantar
SQLSTATE 40001 Buat rollback dan cuba semula keseluruhan transaksi, tertakluk pada dasar percubaan semula yang terhad
Ralat lain, atau had percubaan semula dicapai Paparkan kegagalan; jangan laporkan simpanan berjaya

Hasil sifar baris tidak menentukan sama ada artikel telah dipadamkan atau semakannya berubah. Demonstrasi ini sengaja menggabungkan kedua-dua hasil tersebut. Jika antara muka memerlukan butiran lanjut, muatkan artikel semasa selepas itu dan paparkan hasil tersebut sebagai pemerhatian kemudian. Jangan gambarkannya sebagai bukti keadaan tepat ketika kemas kini gagal.

Kegagalan pensirian menggunakan SQLSTATE 40001. PostgreSQL menghendaki keseluruhan transaksi dicuba semula, termasuk logik yang menentukan pernyataan dan nilainya. Percubaan semula mungkin memerlukan beberapa kali cubaan dan tidak menjamin transaksi akhirnya selesai. Contoh ini menggunakan tiga cubaan sebagai dasar aplikasi ilustrasi. PostgreSQL: Serialization Failure Handling.

Kekalkan expectedRevision tanpa perubahan sepanjang cubaan tersebut. Nilai itu mewakili prasyarat draf yang dihantar. Jika simpanan lain meningkatkan semakan, transaksi baharu kemudiannya boleh menolak token lama dengan hasil sifar baris dikemas kini. Menggantikan token secara automatik dengan semakan terkini akan menghapuskan perlindungan penulisan lapuk.

Fungsi ini hanya memulangkan kejayaan selepas COMMIT selesai. Semua pernyataan transaksi menggunakan Client yang sama, seperti yang diwajibkan oleh node-postgres. Apabila menyesuaikan kod ini untuk kumpulan sambungan, dapatkan klien dengan pool.connect(), gunakan klien itu sepanjang transaksi, kemudian lepaskannya. node-postgres: Transactions, node-postgres: pg.Pool.

Adakah anda masih memerlukan SERIALIZABLE atau SELECT FOR UPDATE?

Bagi kemas kini bersyarat satu baris yang terhad ini, Read Committed juga boleh menguatkuasakan prasyarat semakan: selepas menunggu pengemas kini serentak yang mengomit, PostgreSQL menilai semula syarat kemas kini terhadap baris yang telah dikemas kini. Jaminan ini tidak meluaskan Read Committed menjadi jaminan untuk peraturan berbilang baris yang kompleks. PostgreSQL: Transaction Isolation.

Kekalkan SERIALIZABLE apabila transaksi sekeliling bergantung pada bacaan dan penulisan yang lebih luas serta memerlukan jaminannya. Syarat semakan menyatakan prasyarat editor; pengasingan mengawal interaksi antara operasi pangkalan data dalam transaksi. Pilih kedua-duanya berdasarkan tugas sebenar.

SELECT FOR UPDATE boleh menyokong transaksi yang perlu memeriksa baris dan melaksanakan beberapa operasi yang saling bergantung. Kunci barisnya menghalang penulis yang bercanggah sehingga transaksi tamat. Di bawah Repeatable Read atau Serializable, mengunci baris yang telah berubah sejak petikan transaksi boleh mengakibatkan ralat. PostgreSQL: Explicit Locking.

Mendapatkan kunci itu hanya apabila simpanan tiba tetap menjadikan aplikasi bertanggungjawab membandingkan semakan asas yang dihantar. Mengunci baris semasa tidak dapat memberitahu pangkalan data semakan terdahulu yang menghasilkan draf itu. Untuk penggantian satu baris yang ditunjukkan di sini, kemas kini bersyarat sudah menggabungkan perbandingan dan perubahan.

Sebelum menyepadukan corak ini, semak setiap laluan yang menulis kandungan, kekalkan draf editor apabila simpanan ditolak, dan kemas kini semakan asas editor hanya selepas simpanan disahkan. Apabila simpanan lapuk ditolak, tawarkan artikel semasa bersama draf yang dikekalkan supaya pembaca dapat menyelaraskan teks dan menghantar simpanan baharu berdasarkan semakan yang benar-benar telah disemak.

Sumber