Använd FOR UPDATE SKIP LOCKED i en fråga som också ändrar den valda raden från ready till processing. Då kan samtidiga arbetare göra anspråk på olika syntetiska artikelrader, medan det sparade tillståndet hindrar ett nytt anspråk när låset upphör.
Den här genomgången är avsedd för en utvecklare som använder Terminal på en Mac med en fungerande PostgreSQL-anslutning. Du skapar tre syntetiska artiklar, verifierar samtidiga anspråk i två databassessioner och kör ett komplett exempel med node-postgres.
Varför kan två arbetare välja samma artikel?
En vanlig SELECT reserverar inte resultatet. Med PostgreSQLs standardisoleringsnivå Read Committed kan två arbetare läsa samma committade ready-rad innan någon av dem ändrar den. Att lägga den vanliga läsningen och en senare uppdatering i en transaktion gör inte i sig valet exklusivt. PostgreSQL beskriver hur ögonblicksbilder fungerar i Transaction Isolation.
Anspråket behöver två skydd: ett radlås när artikeln väljs och en tillståndsändring innan låset släpps. PostgreSQL släpper radlås när transaktionen avslutas. Därför blir raden åter valbar när transaktionen för en fristående låsande läsning avslutas, om man bara låser och sedan börjar bearbeta den. Se Explicit Locking.
Använd samma protokoll för anspråk i alla arbetare. En kodväg i programmet som läser en artikel med tillståndet ready och börjar bearbeta den utan att först lyckas göra anspråk på den kan fortfarande orsaka dubbelarbete.
Skapa en liten syntetisk artikelkö
Förutsättningar: en PostgreSQL-databas som körs, kommandot psql tillgängligt i Terminal och en befintlig anslutningskonfiguration. För JavaScript-avsnittet behöver du också en version av Node.js som fortfarande stöds och npm installerat. Den här artikeln utgår från att du redan har en fungerande databasanslutning; installationen varierar beroende på din befintliga Mac-konfiguration.
Funktionen SKIP LOCKED lades till i PostgreSQL 9.5. Det är den lägsta syntaxversionen, inte en rekommendation att köra en version som inte längre stöds. Använd en version av PostgreSQL som stöds; den aktuella dokumentationen som hämtades anger PostgreSQL 18 som den senaste versionen. Se 9.5 release notes och den aktuella SELECT-dokumentationen.
Öppna Terminal och kör psql med dina vanliga anslutningsinställningar. Kommandot ger en interaktiv prompt där du kan köra SQL och visa resultat, enligt beskrivningen i psql-dokumentationen. Kör följande konfiguration en gång i en tillfällig databas där article_claim_demo inte redan finns. Den ersätter avsiktligt inte en befintlig tabell.
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');
Exemplen utgår från isoleringsnivån Read Committed och den här enkla tabellen utan ytterligare triggers. Låt de tre exempelraderna vara oförändrade tills du är klar med testet i två sessioner.
Gör anspråk på en artikel med en enda SQL-sats
Kör den här satsen när en arbetare behöver en artikel. Kandidatfrågan låser en tillgänglig rad; uppdateringen registrerar anspråket och returnerar artikelns indatafält. PostgreSQLs UPDATE-dokumentation beskriver det här mönstret med CTE och uppdatering och bekräftar att RETURNING rapporterar rader som faktiskt har uppdaterats.
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;
Låset ska finnas i kandidat-CTE:n. Med SKIP LOCKED kan en annan arbetare gå vidare förbi en kandidat som den inte kan låsa omedelbart. LIMIT 1 begränsar varje anspråk, och kopplingen via primärnyckeln identifierar en enda målradsuppdatering. PostgreSQL dokumenterar uttryckligen låsning i en WITH-fråga och användningen av överhoppade rader i köliknande sammanhang i SELECT: The Locking Clause.
Utan en uttrycklig BEGIN kör PostgreSQL satsen i en egen transaktion. En sats som lyckas committar både den associerade uppdateringen av valet och dess tillståndsändring innan den efterföljande bearbetningen börjar. Efter commit är radlåset borta, men state = 'ready' gör att artikeln inte väljs igen. Se Transactions.
Bearbeta endast en artikel som returneras av anspråkssatsen. Ett tomt resultat betyder att försöket inte fick någon artikel; det visar inte att allt arbete är klart. Andra arbetare kan för tillfället hålla lås på valbara rader. Dessutom kringgår SKIP LOCKED väntan på radlås, men lås på tabellnivå kan fortfarande orsaka väntan. De här begränsningarna dokumenteras i SELECT.
Verifiera anspråksbeteendet i två Terminal-sessioner
Använd två Terminal-fönster anslutna via psql till samma databas. Börja med de ursprungliga tre raderna med tillståndet ready. Följande kontrollerade sekvens håller varje transaktion öppen så att du kan se överlappningen direkt.
- Kör
BEGIN ISOLATION LEVEL READ COMMITTED;i session A och klistra sedan in hela anspråkssatsen ovan. Låt transaktionen vara öppen. Med de oförändrade exempeldata ska det returnerade ID:t vara1. - Kör
BEGIN ISOLATION LEVEL READ COMMITTED;i session B och klistra sedan in samma anspråkssats. Den bör returnera ID:t2medan A fortfarande håller ID:t1. - Kör
ROLLBACK;i session B och sedanROLLBACK;i session A. - Kör granskningsfrågan nedan i valfri session. Båda anspråken rullades tillbaka, så alla tre artiklar ska åter ha tillståndet
ready.
SELECT id, title, state, rendered_title
FROM article_claim_demo
ORDER BY id;
Detta är förväntade resultat som följer av det dokumenterade lås- och återställningsbeteendet, inte resultat från en rapporterad databaskörning. Återställning avbryter varje tillståndsuppdatering och släpper låsen. Om du i stället använder COMMIT förblir artiklarna i tillståndet processing och väljs inte av senare anspråk. Se Explicit Locking och Transactions.
Testet visar två användbara saker: session B går vidare förbi en artikel som för tillfället är låst, och en committad tillståndsändring håller en artikel borta från framtida anspråk. Testa båda egenskaperna när du anpassar en befintlig arbetare.
Kör tre arbetare med node-postgres
När du har rullat tillbaka båda testtransaktionerna skapar du en ny lokal projektkatalog i Terminal. Installera paketet med det dokumenterade installationskommandot för node-postgres:
mkdir article-claim-demo
cd article-claim-demo
npm init -y
npm install pg
Spara följande kompletta program som workers.mjs. Det använder samma miljövariabler för databasanslutning som psql; node-postgres dokumenterar de gemensamma anslutningsvariablerna i Connecting. Varje arbetare försöker göra anspråk på en artikel, omvandlar den syntetiska titeln till versaler och sparar resultatet.
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();
}
Kör node workers.mjs och kör sedan granskningsfrågan igen i psql. Med tre orörda artiklar i tillståndet ready och ingen annan aktivitet förväntas alla tre tillstånd ändras till done, och versalvärden visas i rendered_title. Arbetarnas etiketter och utskriftsordning behöver inte följa artikelordningen.
pool.query passar bra här eftersom varje anspråk är en fullständig sats och varje slutförande är en separat sats. Poolen hanterar utlåning och återlämning av anslutningar; pool.end() stänger poolen när försöken har slutförts. Se Pooling.
Slutförandefrågan skickar värden separat med $1 och $2, enligt Queries. Programmet kontrollerar om den returnerade arrayen rows saknar artiklar och kontrollerar rowCount för antalet uppdaterade slutföranderader, i enlighet med pg.Result.
JavaScript-koden klarade en syntaxkontroll under förberedelserna. PostgreSQL-programmen fanns inte tillgängliga i forskningsmiljön, så SQL-koden och det samtidiga databeteendet kontrollerades mot dokumentationen men kördes inte här.
Välj rätt transaktionsgräns och hantera ofärdigt arbete
Exemplet committar ett kort anspråk innan titeln omvandlas. Det håller låset kortvarigt, men skapar en avsiktlig återställningsgräns: om en arbetare stannar efter att ha gjort anspråk men innan den har slutfört arbetet förblir artikeln i tillståndet processing. Om programmet körs igen kommer den inte att tas över på nytt.
För den här skrivbordsdemonstrationen ska du stoppa alla arbetare och granska artiklar som har tillståndet processing innan du avsiktligt gör dem valbara igen. Återställ dem inte automatiskt medan en tidigare arbetare fortfarande kan bli klar: exemplet har inget anspråksgenereringsfält som skiljer ett tidigare försök från ett senare. Att enbart förhindra dubbla anspråk ger inte automatisk återställning eller exakt en gång-bearbetning.
Använd den här beslutshjälpen när du anpassar exemplet:
| Din uppgift | Transaktionsmetod | Konsekvens |
|---|---|---|
| Gör anspråk på en artikel och utför sedan längre arbete i programmet | Committa anspråket med en enda sats först | Behåll ett synligt processing-tillstånd och utforma återställning separat |
| Utför en liten omvandling helt i PostgreSQL | Gör anspråk på artikeln och slutför arbetet i samma transaktion | Databasändringarna kan committas eller rullas tillbaka tillsammans |
| Kör flera SQL-satser som en transaktion i Node.js | Låna ut en klient och använd den genom hela transaktionen | Behåll samma anslutning genom BEGIN, satserna och COMMIT eller ROLLBACK |
För den sista metoden kräver node-postgres att varje sats i transaktionen använder samma utlånade klient. Separata anrop till pool.query kan använda olika anslutningar och kan därför inte ingå i samma transaktion. Frigör klienten när arbetet är klart eller har rullats tillbaka. Se Transactions och Pooling.
Var tydlig med de isoleringsantaganden som visas här. Med Repeatable Read och Serializable kan det uppstå fel om en rad som ska låsas har ändrats sedan transaktionen började; program som använder dessa nivåer behöver hantera omförsök av transaktioner. PostgreSQL beskriver den begränsningen i Explicit Locking.
För den här syntetiska artikeluppgiften är den viktiga sekvensen konkret: välj och lås en valbar rad, ändra dess tillstånd i samma sats och bearbeta endast artikeln som returneras när anspråket har lyckats. Verifiera överlappningen i två sessioner och bestäm sedan hur ditt program ska hantera en artikel vars arbetare aldrig registrerar att arbetet är klart.
Källor
- SELECT — PostgreSQL Global Development Group.
- UPDATE — PostgreSQL Global Development Group.
- 13.3. Explicit Locking — PostgreSQL Global Development Group.
- 13.2. Transaction Isolation — PostgreSQL Global Development Group.
- 3.4. Transactions — PostgreSQL Global Development Group.
- Release 9.5 — PostgreSQL Global Development Group.
- psql — PostgreSQL Global Development Group.
- node-postgres: Introduction and Install — node-postgres.
- Connecting — node-postgres.
- Pooling — node-postgres.
- Transactions — node-postgres.
- Queries — node-postgres.
- pg.Result — node-postgres.