Neem om een verouderde documentopslag af te wijzen de revisie op die de editor oorspronkelijk heeft geladen in de voorwaarde van de update en verhoog deze vervolgens atomair. SERIALIZABLE alleen kan die vereiste niet uitdrukken wanneer het document in een eerdere transactie is geladen.

Deze handleiding is bedoeld voor een ontwikkelaar die in Terminal op een Mac werkt en wil dat een PostgreSQL-artikeleditor verouderde vervangende tekst afwijst. De handleiding gebruikt synthetische artikelrijen en een complete node-postgres-demonstratie. De taak is precies: sla alleen op als de opgeslagen revisie nog overeenkomt met de revisie waarop het ingediende concept is gebaseerd.

Waarom kan SERIALIZABLE een verouderde vervanging toestaan?

PostgreSQL definieert serialiseerbare uitvoering als een uitvoering met een effect dat gelijkwaardig is aan het één voor één uitvoeren van de vastgelegde transacties in een bepaalde volgorde. Die garantie heeft betrekking op databasetransacties en hun bewerkingen. PostgreSQL: Transaction Isolation.

Bekijk deze illustratieve volgorde:

  1. Een editor laadt artikel 1 op revisie 1 en de leestransactie eindigt.
  2. Een andere opslag vervangt de artikeltekst en legt revisie 2 vast.
  3. De oorspronkelijke editor dient vervangende tekst in die op revisie 1 is gebaseerd.
  4. Een nieuwe serialiseerbare transactie werkt artikel 1 bij met alleen de ID.

De laatste transactie kan slagen: de transacties zijn al na elkaar uitgevoerd. Niets in een update die alleen op id = 1 is gebaseerd, geeft aan dat de ingediende tekst revisie 1 vereist. Dit volgt uit de gedocumenteerde definitie van serialiseerbaarheid en vormt daarop geen uitzondering.

Gelijktijdige transacties zijn anders. PostgreSQL kan een serialiseerbare transactie afwijzen wanneer een gelijktijdige update conflicteert met de snapshot of wanneer afhankelijkheden seriële uitvoering onmogelijk zouden maken. De snapshot wordt vastgesteld door de eerste instructie die geen transactiebeheerinstructie is; alleen eerder BEGIN uitvoeren bewaart geen eerdere lezing door de editor. PostgreSQL: Transaction Isolation.

De diagnostische vraag is daarom: Waar vereist de opslag expliciet de revisie die de editor heeft geladen? Als het antwoord nergens is, voegt het wijzigen van het isolatieniveau die ontbrekende voorwaarde niet toe.

Wat moet de revisiecontrole garanderen?

Behandel elk geladen concept als een paar: de bewerkbare tekst en de basisrevisie. De opslag bevat de artikel-ID, de vervangende tekst en de ongewijzigde basisrevisie. De database-update moet zowel de ID als die revisie controleren, de tekst vervangen en de opgeslagen revisie in dezelfde instructie verhogen.

De voorwaarde WHERE id = $1 AND revision = $2 drukt bijvoorbeeld de vereiste voorafgaande controle uit, terwijl SET body = $3, revision = revision + 1 de wijziging uitvoert. PostgreSQL werkt rijen bij die aan de voorwaarde voldoen, en RETURNING levert waarden uit de rijen die daadwerkelijk zijn bijgewerkt. Een update van nul rijen is een geslaagde SQL-opdracht, dus de applicatie moet dit expliciet interpreteren. PostgreSQL: UPDATE.

Dit patroon wordt vaak optimistische concurrency control of compare-and-swap genoemd. In het ontwerp van dit artikel moet elke bewerking die bewerkbare artikelinhoud wijzigt de revisie verhogen. Houd revisies niet-null, reset ze niet en hergebruik geen artikelidentiteit met een oude revisietoken. Anders kan gelijkheid ten onrechte suggereren dat een oud concept nog steeds overeenkomt.

Een afzonderlijke lezing gevolgd door een update die alleen de ID gebruikt, laat de vereiste buiten de schrijfbewerking. Ook verandert het ophalen van de nieuwste revisie vlak voor het opslaan en het vervangen van de ingediende revisie door die waarde de betekenis van het verzoek: het staat toe dat een versie wordt vervangen die de editor niet als basis heeft gebruikt.

Voer een compleet synthetisch voorbeeld uit op je Mac

Zorg voor een actieve PostgreSQL-instantie, een bestaande scratchdatabase en een huidige of ondersteunde LTS-installatie van Node.js met npm. Open Terminal, maak een werkmap aan en installeer de driver met de gedocumenteerde opdracht npm install pg. node-postgres: Introduction and Install.

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

Sla het volgende complete programma in die map op als revision-demo.mjs. Het gebruikt de gedocumenteerde ESM-compatibele standaardimport. Zowel Client als Pool kan PostgreSQL-verbindingsinstellingen uit omgevingsvariabelen lezen; configureer die instellingen voor je bestaande scratchdatabase voordat je het programma uitvoert. node-postgres: ESM, node-postgres: Connecting.

Het programma vergelijkt twee synthetische artikelen. Artikel 1 krijgt twee onvoorwaardelijke serialiseerbare opslagbewerkingen. Artikel 2 krijgt twee revisiecontroles met dezelfde oorspronkelijke revisie. De tijdelijke tabel houdt de demonstratie gescheiden van permanente artikeltabellen. De JavaScript-syntaxis is gecontroleerd met Node.js; de datademonstratie is niet uitgevoerd in de onderzoeksomgeving.

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();
}

Als de bestaande scratchdatabase revision_demo heet, voer dan de onderstaande opdracht uit. Vervang dit door de werkelijke databasenaam en behoud eventuele verbindingsinstellingen voor de omgeving die je lokale instantie nodig heeft. node-postgres: Connecting.

PGDATABASE=revision_demo node revision-demo.mjs

De verwachte eindwaarden zijn:

Artikel Inhoud Revisie Betekenis
1 Stale replacement 3 Beide onvoorwaardelijke opslagbewerkingen zijn vastgelegd
2 First edit 2 De tweede opslag met revisiecontrole is afgewezen

De afgedrukte eindstatus hoort stale-or-missing te zijn.

De volgorde vermijdt bewust timingafhankelijke gelijktijdigheid. Ze laat de centrale grens direct zien: een verouderde vervanging kan slagen, zelfs wanneer elke opslagtransactie serialiseerbaar is, omdat de opslagbewerkingen in een geldige seriële volgorde worden uitgevoerd.

Hoe moet node-postgres elke uitkomst afhandelen?

Het programma geeft waarden gescheiden van de SQL-tekst door met behulp van de gedocumenteerde API van de driver voor geparametriseerde query's. Het controleert ook rowCount, dat aangeeft hoeveel rijen een update heeft verwerkt; een lege array rows is op zichzelf geen algemene vervanging voor het controleren van updateaantallen. node-postgres: Queries, node-postgres: pg.Result.

Gebruik de volgende beslissingstabel wanneer je de functie aanpast:

Uitkomst Actie van de applicatie
Eén bijgewerkte rij en geslaagde commit Accepteer de opslag en behoud de geretourneerde revisie
Nul bijgewerkte rijen Wijs deze opslag af als verouderd of ontbrekend; behoud het ingediende concept
SQLSTATE 40001 Voer een rollback uit en probeer de volledige transactie opnieuw, volgens een begrensd beleid
Andere fout of het aantal pogingen is bereikt Meld de fout; rapporteer de opslag niet als geslaagd

Een resultaat van nul rijen maakt niet duidelijk of het artikel is verwijderd of dat de revisie is gewijzigd. De demonstratie combineert die uitkomsten bewust. Als de interface meer details nodig heeft, laad dan daarna het huidige artikel en presenteer dat resultaat als een latere waarneming. Beschrijf het niet als bewijs van de precieze toestand op het moment van de mislukte update.

Serialisatiefouten gebruiken SQLSTATE 40001. PostgreSQL vereist dat de volledige transactie opnieuw wordt geprobeerd, inclusief de logica die bepaalt welke instructies en waarden worden gebruikt. Er kunnen meerdere pogingen nodig zijn; uiteindelijke voltooiing is niet gegarandeerd. Het voorbeeld gebruikt drie pogingen als illustratief applicatiebeleid. PostgreSQL: Serialization Failure Handling.

Houd expectedRevision ongewijzigd tijdens die pogingen. Deze waarde vertegenwoordigt de vereiste basis van het ingediende concept. Als een andere opslag de revisie heeft verhoogd, kan een nieuwe transactie de oude token vervolgens afwijzen met nul bijgewerkte rijen. De token automatisch vervangen door de nieuwste revisie zou de bescherming tegen verouderde schrijfbewerkingen opheffen.

De functie retourneert alleen succes nadat COMMIT is voltooid. Alle transactie-instructies gebruiken dezelfde Client, zoals node-postgres vereist. Als je dit aanpast voor een pool, haal dan een client op met pool.connect(), gebruik die client gedurende de hele transactie en geef hem daarna vrij. node-postgres: Transactions, node-postgres: pg.Pool.

Heb je SERIALIZABLE of SELECT FOR UPDATE nog nodig?

Voor deze beperkt afgebakende voorwaardelijke update van één rij kan Read Committed de revisievereiste ook afdwingen: nadat PostgreSQL heeft gewacht op een gelijktijdige updater die commit, evalueert het de voorwaarde van de update opnieuw aan de hand van de bijgewerkte rij. Daarmee wordt Read Committed niet uitgebreid tot een garantie voor complexe regels over meerdere rijen. PostgreSQL: Transaction Isolation.

Behoud SERIALIZABLE wanneer de omringende transactie afhankelijk is van uitgebreidere lees- en schrijfbewerkingen die de garanties ervan nodig hebben. De revisievoorwaarde drukt de vereiste van de editor uit; isolatie regelt de interacties tussen de databasebewerkingen van de transactie. Kies beide op basis van de daadwerkelijke taak.

SELECT FOR UPDATE kan worden gebruikt voor een transactie die een rij moet inspecteren en verschillende afhankelijke bewerkingen moet uitvoeren. De rijvergrendeling blokkeert conflicterende schrijvers totdat de transactie eindigt. Onder Repeatable Read of Serializable kan het vergrendelen van een rij die sinds de snapshot van de transactie is gewijzigd een fout veroorzaken. PostgreSQL: Explicit Locking.

Als je die vergrendeling pas verkrijgt wanneer een opslag binnenkomt, blijft de applicatie verantwoordelijk voor het vergelijken van de ingediende basisrevisie. Door de huidige rij te vergrendelen kan de database niet weten welke eerdere revisie de basis van het concept was. Voor de vervanging van één rij die hier wordt getoond, combineert de voorwaardelijke update de vergelijking en wijziging al.

Controleer voordat je het patroon integreert elk pad dat inhoud wegschrijft, behoud het concept van de editor bij afwijzing en werk de basisrevisie van de editor pas bij na een bevestigde opslag. Wanneer een verouderde opslag wordt afgewezen, bied dan naast het behouden concept het huidige artikel aan, zodat de lezer de tekst kan afstemmen en een nieuwe opslag kan indienen op basis van de daadwerkelijk beoordeelde revisie.

Bronnen