जब समवर्ती PostgreSQL अपडेट एक-दूसरे से ओवरलैप करें, तो डेडलॉक रोकने के लिए अपडेट करने से पहले प्रत्येक ट्रांज़ैक्शन के पूरे लक्ष्य-समूह पर एक ही अपरिवर्तनीय-कुंजी क्रम में लॉक लगाएँ। ट्रांज़ैक्शन को एक ही node-postgres क्लाइंट पर चलाएँ और पात्र विफलताओं पर शुरुआत से दोबारा प्रयास करें।
यह मार्गदर्शिका macOS Terminal में काम कर रहे ऐसे डेवलपर के लिए है जिसे दो समवर्ती बैचों से कृत्रिम लेख-पंक्तियों को पूरी तरह अपडेट करवाना है। उदाहरण में PostgreSQL के मौजूदा दस्तावेज़ का उपयोग किया गया है, जो PostgreSQL 18 को दर्शाता है, और node-postgres के मौजूदा दस्तावेज़ का भी। इसका दायरा मौजूदा लेख ID की ज्ञात सूची तक सीमित है; समवर्ती कार्य के दौरान ID अपरिवर्तित रहती हैं। PostgreSQL: Explicit Locking
ओवरलैप करने वाले अपडेट डेडलॉक क्यों कर सकते हैं
दो ट्रांज़ैक्शन पर विचार करें: A पहले लेख 1, फिर लेख 2 अपडेट करता है; B पहले लेख 2, फिर लेख 1 अपडेट करता है। यदि हर एक दूसरे लॉक का अनुरोध करने से पहले अपनी पहली पंक्ति का लॉक ले लेता है, तो दोनों दूसरे के पास मौजूद लॉक की प्रतीक्षा करते हैं। PostgreSQL इस चक्र का पता लगाता है और एक ट्रांज़ैक्शन रद्द कर देता है; एप्लिकेशन यह नहीं चुन सकता कि कौन-सा रद्द होगा। स्पष्ट लॉकिंग स्टेटमेंट के बिना भी सामान्य अपडेट से यह स्थिति बन सकती है। PostgreSQL: Explicit Locking
उपयोगी अंतर ओवरलैप और चक्र के बीच है। ओवरलैप के कारण प्रतीक्षा करनी पड़ सकती है। चक्र किसी भी ट्रांज़ैक्शन को आगे बढ़ने से रोकता है। PostgreSQL सभी एप्लिकेशन में लॉक लेने का क्रम एकसमान रखने की सलाह देता है और सबसे मज़बूत आवश्यक लॉक मोड पहले लेने को कहता है। PostgreSQL: Explicit Locking
इस लेख के कार्यभार के लिए एक नियम अपनाएँ: किसी भी लेख में बदलाव करने से पहले अनुरोधित लेख की हर पंक्ति पर आरोही id क्रम में लॉक लें। इस नियम को पूरे ट्रांज़ैक्शन पर लागू करें, जिनमें लेखों को छूने वाले पहले के स्टेटमेंट भी शामिल हैं।
अपडेट से पहले पूरे लक्ष्य-समूह पर लॉक लगाएँ
JavaScript ऐरे को सॉर्ट करने से बल्क SQL अपडेट को यह निर्देश नहीं मिलता कि वह पंक्तियों पर उसी क्रम में काम करे। PostgreSQL के UPDATE सिंटैक्स में सीधे ORDER BY क्लॉज़ नहीं है। इसके दस्तावेज़ चुनने वाली क्वेरी के ज़रिए क्रमबद्ध लॉकिंग दिखाते हैं और समझाते हैं कि साझा क्रम अपडेट ऑपरेशनों के बीच डेडलॉक रोक सकता है। PostgreSQL: UPDATE
ट्रांज़ैक्शन के भीतर एक अलग, पूरी की जाने वाली लॉकिंग स्टेटमेंट का उपयोग करें। यह उदाहरणात्मक SQL ट्रांज़ैक्शन लेख 1, 2 और 3 को लक्ष्य बनाता है; सेटअप तालिका अगले अनुभाग में दी गई है।
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT id
FROM deadlock_demo_articles
WHERE id = ANY(ARRAY[3, 1, 2]::integer[])
ORDER BY id
FOR UPDATE;
UPDATE deadlock_demo_articles
SET revision = revision + 1
WHERE id = ANY(ARRAY[3, 1, 2]::integer[]);
COMMIT;
पहला स्टेटमेंट लॉक लेने का क्रम तय करता है। इसके बाद अपडेट उन पंक्तियों पर काम करता है जिन पर उसी ट्रांज़ैक्शन ने पहले ही लॉक लगा रखा है। पंक्ति लॉक सामान्यतः ट्रांज़ैक्शन समाप्त होने तक बने रहते हैं। इस अवधि को छोटा रखें: शुरू करने से पहले इनपुट तैयार करें और लॉक लगे रहने के दौरान डेस्कटॉप इनपुट की प्रतीक्षा न करें। PostgreSQL: Explicit Locking
निम्नलिखित शर्तें इस उदाहरण के क्रम-संबंधी अनुबंध को परिभाषित करती हैं:
- हर प्रतिस्पर्धी बैच लेख
idके आरोही क्रम का उपयोग करता है। - समवर्ती कार्य के दौरान ID अपरिवर्तित रहती हैं।
- लॉक लगाने से पहले हर बैच अपने पूरे लक्ष्य-समूह की पहचान करता है।
- किसी पहले के स्टेटमेंट ने लेख-पंक्तियों पर पहले ही किसी दूसरे क्रम में लॉक नहीं लगाया है।
- उदाहरण तालिका में ऐसे ट्रिगर या संबंधित-तालिका ऑपरेशन नहीं हैं जो अतिरिक्त लॉक निर्भरताएँ पैदा करें।
इस कार्यभार पर यह तर्क लागू करने के लिए ये पूर्वशर्तें हैं। कोई ट्रांज़ैक्शन जो पहले लेख 9 पर लॉक लगाता है और फिर लेख 1 से 3 माँगता है, वह अनुबंध से हट चुका है, भले ही उसकी अंतिम क्वेरी में ORDER BY id हो।
अपने Mac पर node-postgres का पूरा उदाहरण चलाएँ
इसके लिए चल रहा स्थानीय PostgreSQL सर्वर, मौजूदा अस्थायी डेटाबेस, npm वाला Node.js और Terminal में उपलब्ध psql चाहिए। अपने मौजूदा डेटाबेस कनेक्शन को PostgreSQL पर्यावरण वेरिएबल के ज़रिए कॉन्फ़िगर करें। node-postgres पूल और psql समान कनेक्शन वेरिएबल का उपयोग करते हैं; यदि ज़रूरत हो तो PGHOST को स्पष्ट रूप से सेट करें, ताकि दोनों कमांड एक ही सर्वर तक पहुँचें। node-postgres: Connecting
एक नया कार्यशील फ़ोल्डर बनाएँ और निर्भरता इंस्टॉल करें:
npm init -y
npm install pg
नीचे दिए गए पूरे सेटअप को setup.sql के रूप में सेव करें। यह एक नई प्रदर्शन तालिका और तीन कृत्रिम लेख बनाता है। इसे अस्थायी डेटाबेस में एक बार चलाएँ। यदि वह तालिका पहले से मौजूद है, तो मौजूदा सामग्री बदलने के बजाय यह जानबूझकर विफल होता है।
BEGIN;
CREATE TABLE deadlock_demo_articles (
id integer PRIMARY KEY,
title text NOT NULL,
revision integer NOT NULL DEFAULT 0
);
INSERT INTO deadlock_demo_articles (id, title)
VALUES
(1, 'Paper Shapes'),
(2, 'Color Notes'),
(3, 'Desk Sketches');
COMMIT;
psql -v ON_ERROR_STOP=1 -f setup.sql
अगले पूरे प्रोग्राम को update-articles.mjs के रूप में सेव करें। किसी प्रयास के सभी स्टेटमेंट pool.connect() से मिले क्लाइंट का उपयोग करते हैं। node-postgres स्पष्ट रूप से कहता है कि पूरे ट्रांज़ैक्शन में एक ही क्लाइंट का उपयोग करना आवश्यक है; ट्रांज़ैक्शन स्टेटमेंट को pool.query() के ज़रिए भेजने से यह आवश्यकता पूरी नहीं होती। node-postgres: Transactions
import pg from 'pg';
import { setTimeout as sleep } from 'node:timers/promises';
const { Pool } = pg;
const pool = new Pool({ max: 2, connectionTimeoutMillis: 5000 });
pool.on('error', error => {
console.error('Idle database client error:', error.message);
process.exitCode = 1;
});
function normalizeIds(input) {
if (!Array.isArray(input) || input.length === 0 ||
input.some(id => !Number.isInteger(id) ||
id < 1 || id > 2147483647)) {
throw new Error('Use a nonempty array of positive PostgreSQL integer IDs');
}
return [...new Set(input)].sort((a, b) => a - b);
}
async function incrementRevisions(input) {
const ids = normalizeIds(input);
const maxAttempts = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const client = await pool.connect();
let discard = false;
try {
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
const locked = await client.query(
`SELECT id
FROM deadlock_demo_articles
WHERE id = ANY($1::integer[])
ORDER BY id
FOR UPDATE`,
[ids]
);
if (locked.rows.length !== ids.length) {
throw new Error('A requested article is missing; batch cancelled');
}
const updated = await client.query(
`UPDATE deadlock_demo_articles
SET revision = revision + 1
WHERE id = ANY($1::integer[])
RETURNING id, revision`,
[ids]
);
if (updated.rowCount !== ids.length) {
throw new Error('Unexpected update count; batch cancelled');
}
await client.query('COMMIT');
return updated.rows.sort((a, b) => a.id - b.id);
} catch (error) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
discard = true;
throw new Error('Rollback failed; connection discarded', {
cause: rollbackError
});
}
const retryable = error.code === '40P01' || error.code === '40001';
if (!retryable || attempt === maxAttempts) throw error;
} finally {
client.release(discard);
}
// Illustrative bounded backoff, after releasing the client.
await sleep(50 * 2 ** (attempt - 1) + Math.floor(Math.random() * 50));
}
}
try {
const outcomes = await Promise.allSettled([
incrementRevisions([3, 1, 2]),
incrementRevisions([3, 2])
]);
for (const outcome of outcomes) {
if (outcome.status === 'rejected') throw outcome.reason;
}
const result = await pool.query(
'SELECT id, title, revision FROM deadlock_demo_articles ORDER BY id'
);
console.table(result.rows);
} catch (error) {
console.error(error);
process.exitCode = 1;
} finally {
await pool.end();
}
node update-articles.mjs
ऐरे को एक पैरामीटर के रूप में पास किया जाता है और स्पष्ट रूप से integer[] में कास्ट किया जाता है; node-postgres दस्तावेज़ बताते हैं कि JavaScript ऐरे PostgreSQL ऐरे मानों में बदले जाते हैं। डुप्लिकेट हटाने से अनुरोधित संख्या अलग-अलग लेखों की संख्या बनती है। लॉक लेने का क्रम इनपुट ऐरे का क्रम नहीं, बल्कि डेटाबेस का ORDER BY तय करता है। node-postgres: Queries
नई शुरू की गई तालिका में, जब कोई दूसरा लेखक न हो और दोनों बैच सफल हों, तो अपेक्षित संशोधन-संख्याएँ लेख 1 के लिए 1 और लेख 2 तथा 3 के लिए 2 हैं। ये कुल मान बैच की सदस्यता से तय होते हैं, चाहे कोई भी बैच पहले पूरा हो। स्क्रिप्ट को बाद में फिर चलाने पर मौजूदा मान फिर बढ़ेंगे। अनुरोधित ID में से कोई मौजूद न हो, तो स्क्रिप्ट केवल मिली पंक्तियों को चुपचाप अपडेट करने के बजाय उस बैच को रद्द कर देती है।
हर बैच एक अलग ट्रांज़ैक्शन है। यदि एक सफल हो और दूसरा स्थायी रूप से विफल हो जाए, तो सफल बैच कमिट रहता है। Promise.allSettled() दोनों प्रयासों को पूरा होने देता है, फिर प्रोग्राम पूल बंद करता है। अंतिम pool.query() दोनों में से किसी भी ट्रांज़ैक्शन के बाहर चलता है। पूल API में लिए गए क्लाइंट को रिलीज़ करना, ज़रूरत पड़ने पर क्लाइंट को नष्ट करना और स्क्रिप्ट का काम पूरा होने पर पूल समाप्त करना दस्तावेज़ित है। node-postgres: Pool
सामान्यीकरण की जाँचें और उदाहरण में संशोधन की गणना Node.js से चलाई गईं। शोध परिवेश में PostgreSQL उपलब्ध नहीं था, इसलिए SQL और डेटाबेस के समवर्ती व्यवहार को चलाकर परखने के बजाय दस्तावेज़ों के आधार पर जाँचा गया।
अपने कार्य के लिए आवश्यक आइसोलेशन स्तर बनाए रखें
यह प्रोग्राम निश्चित ID वाले अपडेट के लिए स्पष्ट रूप से READ COMMITTED का उपयोग करता है। इस स्तर पर, लॉकिंग स्टेटमेंट किसी अपडेट करने वाले की प्रतीक्षा कर सकता है और फिर अपडेट की गई पंक्ति-संस्करण पर लॉक लगा सकता है, बशर्ते वह अभी भी शर्त से मेल खाता हो। हर स्टेटमेंट नया स्नैपशॉट लेता है। इसलिए लॉक हासिल करने से पहले हटाई गई पंक्ति मौजूद न हो सकती है; प्रोग्राम इसे संख्या-जाँच से संभालता है। PostgreSQL: Transaction Isolation
केवल इस उदाहरण का उपयोग करने के लिए किसी मौजूदा ट्रांज़ैक्शन का आइसोलेशन स्तर कम न करें। REPEATABLE READ के तहत, ट्रांज़ैक्शन स्नैपशॉट के बाद बदली गई पंक्ति को लॉक करने या अपडेट करने की कोशिश से सीरियलाइज़ेशन विफलता हो सकती है। SERIALIZABLE के लिए भी दोबारा प्रयास संभालना आवश्यक है और यह कमिट किए गए सीरियलाइज़ेशन विसंगतियों को रोकता है; यह सामान्य पंक्ति लॉक का क्रम एकसमान रखने की जगह नहीं लेता। PostgreSQL: Transaction Isolation
ज्ञात पंक्तियों पर लॉक लगाने से यह सुनिश्चित नहीं होता कि किसी शर्त की सदस्यता अपरिवर्तित रहेगी। उदाहरण के लिए, यह उदाहरण “ऐसी हर लेख-पंक्ति अपडेट करें जिसका शीर्षक शर्त से मेल खाता हो, जिसमें समवर्ती रूप से जोड़ी गई पंक्तियाँ भी शामिल हों” लागू नहीं करता। यह एक अलग संगतता कार्य है। निश्चित-ID विधि को अपनाने से पहले इसके आइसोलेशन और समन्वय संबंधी आवश्यकताएँ चुनें।
क्रम तय करने वाला कॉलम भी अपरिवर्तनीय रखें। PostgreSQL के दस्तावेज़ बताते हैं कि READ COMMITTED के तहत क्रमबद्ध करना लॉक लगाने से पहले होता है; स्टेटमेंट की प्रतीक्षा के दौरान बदले गए मान लौटाई गई पंक्तियों को अव्यवस्थित दिखा सकते हैं। अपरिवर्तित ID का उपयोग करने से यह विशेष जटिलता नहीं आती। PostgreSQL: SELECT
पूरे ट्रांज़ैक्शन पर दोबारा प्रयास करें और बार-बार होने वाली विफलताओं की जाँच करें
PostgreSQL डेडलॉक विफलताओं को SQLSTATE 40P01 और सीरियलाइज़ेशन विफलताओं को 40001 के रूप में पहचानता है। पूरे ट्रांज़ैक्शन पर दोबारा प्रयास करें, जिसमें ऐसे डेटाबेस-निर्भर निर्णय भी शामिल हों जो उसके स्टेटमेंट या मान तय करते हैं। केवल अंतिम अपडेट पर दोबारा प्रयास करने से यह आवश्यकता पूरी नहीं होती। PostgreSQL: Serialization Failure Handling
यह उदाहरण कुल तीन प्रयासों की अनुमति देता है। इसमें उपयोग की गई कम अवधि वाली एक्सपोनेंशियल देरी और जिटर एप्लिकेशन के उदाहरणात्मक विकल्प हैं, PostgreSQL के डिफ़ॉल्ट नहीं। यह देरी से पहले क्लाइंट रिलीज़ करता है, अन्य त्रुटियों पर तुरंत रुकता है और प्रयास समाप्त होने की सूचना देता है। PostgreSQL चेतावनी देता है कि दोबारा प्रयास करने से अंततः पूरा होना सुनिश्चित नहीं होता। PostgreSQL: Serialization Failure Handling
कार्यान्वयन को अपनाते समय इस निर्णय-सहायिका का उपयोग करें:
| स्थिति | कार्रवाई |
|---|---|
| किसी अन्य बैच ने पहले ही अनुरोधित पंक्ति पर लॉक लगा रखा है | यदि कार्य के लिए हर अनुरोधित पंक्ति आवश्यक है, तो क्रमबद्ध लॉक लेने की प्रतीक्षा करें। |
| अनुरोधित ID मौजूद नहीं है | रोलबैक करें और लक्ष्य अनुपस्थित होने की सूचना दें। |
SQLSTATE 40P01 या 40001 है |
रोलबैक करें, फिर सीमित संख्या में पूरे पात्र ट्रांज़ैक्शन पर दोबारा प्रयास करें। |
| क्रमबद्ध लॉकिंग के बाद भी विफलताएँ जारी हैं | पहले के स्टेटमेंट, अन्य लेखक, ट्रिगर और अतिरिक्त तालिकाएँ जाँचें कि कहीं वे क्रम-अनुबंध से हट तो नहीं रहे। |
| ऑपरेशन कई तालिकाओं को छूता है | सभी प्रतिस्पर्धी पथों के लिए पंक्ति-क्रम के साथ साझा तालिका-क्रम भी तय करें। |
| बैच को परमाणु रूप से कमिट करना आवश्यक है | एक ही ट्रांज़ैक्शन बनाए रखें; अलग-अलग कमिट किए गए हिस्से कार्य को बदल देंगे। |
इस पूरे लक्ष्य-समूह वाले बैच में SKIP LOCKED न जोड़ें: जिन पंक्तियों पर तुरंत लॉक नहीं लग सकता, उन्हें छोड़ दिया जाएगा। PostgreSQL इसे कतार-जैसे उपभोक्ताओं के लिए उपयोगी और सामान्य संगत दृश्य के लिए अनुपयुक्त बताता है। NOWAIT परस्पर विरोधी पंक्ति लॉक की प्रतीक्षा करने के बजाय त्रुटि देता है; इससे प्रतीक्षा का व्यवहार बदलता है, लेकिन बैच पूरा नहीं होता। PostgreSQL: SELECT
कार्यान्वयन अपनाने के लिए तैयार है, यदि आप पूरे लक्ष्य-समूह की पहचान कर सकें, उसकी क्रम-कुंजियों को अपरिवर्तित रख सकें और प्रतिस्पर्धी ट्रांज़ैक्शन में लॉक लेने का एक ही नियम लागू कर सकें। अनुपस्थित पंक्तियों की जाँच और पूरे ट्रांज़ैक्शन पर सीमित दोबारा प्रयास बनाए रखें, ताकि प्रतिस्पर्धा या अधूरे इनपुट का स्पष्ट परिणाम मिले।
स्रोत
PostgreSQL 18: Explicit Locking — PostgreSQL Global Development Group.
PostgreSQL 18: UPDATE — PostgreSQL Global Development Group.
PostgreSQL 18: SELECT — PostgreSQL Global Development Group.
PostgreSQL 18: Transaction Isolation — PostgreSQL Global Development Group.
PostgreSQL 18: Serialization Failure Handling — PostgreSQL Global Development Group.
Transactions — node-postgres.
Connecting — node-postgres.
Queries — node-postgres.
Pool — node-postgres.