Untuk mengelakkan deadlock apabila kemas kini PostgreSQL serentak bertindih, kunci set sasaran lengkap setiap transaksi mengikut susunan kunci tidak berubah yang sama sebelum membuat kemas kini. Kekalkan transaksi pada satu klien node-postgres dan cuba semula kegagalan yang layak dari awal.
Panduan ini ditujukan kepada pembangun yang bekerja dalam Terminal macOS dan perlu memastikan dua kelompok serentak mengemas kini baris artikel sintetik sepenuhnya. Contoh ini menggunakan dokumentasi PostgreSQL semasa, yang mengenal pasti PostgreSQL 18, serta dokumentasi node-postgres semasa. Skopnya ialah senarai ID artikel sedia ada yang diketahui, dengan ID kekal tidak berubah semasa kerja serentak. PostgreSQL: Penguncian Eksplisit
Sebab kemas kini bertindih boleh menyebabkan deadlock
Pertimbangkan dua transaksi: A mengemas kini artikel 1, kemudian artikel 2; B mengemas kini artikel 2, kemudian artikel 1. Jika setiap transaksi memperoleh kunci baris pertama sebelum meminta kunci kedua, setiap transaksi menunggu kunci yang dipegang oleh transaksi yang satu lagi. PostgreSQL mengesan kitaran itu lalu membatalkan salah satu transaksi; aplikasi tidak dapat memilih transaksi yang mana. Kemas kini biasa boleh menyebabkan keadaan ini tanpa penyataan penguncian eksplisit. PostgreSQL: Penguncian Eksplisit
Perbezaan penting ialah antara pertindihan dengan kitaran. Pertindihan mungkin menyebabkan transaksi perlu menunggu. Kitaran menghalang kedua-dua transaksi daripada diteruskan. PostgreSQL mengesyorkan susunan pemerolehan kunci yang konsisten merentas aplikasi, dengan mod kunci paling kuat yang diperlukan diperoleh terlebih dahulu. PostgreSQL: Penguncian Eksplisit
Untuk beban kerja artikel ini, gunakan satu peraturan: peroleh semua kunci baris artikel yang diminta mengikut susunan menaik id sebelum mengubah mana-mana artikel. Gunakan peraturan itu sepanjang transaksi, termasuk untuk sebarang penyataan terdahulu yang menyentuh artikel.
Kunci keseluruhan set sasaran sebelum kemas kini
Mengisih tatasusunan JavaScript tidak mengarahkan kemas kini SQL pukal untuk melawati baris mengikut susunan itu. Sintaks PostgreSQL UPDATE tidak mempunyai klausa ORDER BY secara langsung. Dokumentasinya menunjukkan penguncian tersusun melalui pertanyaan pemilihan dan menerangkan bahawa susunan yang sama boleh mencegah deadlock antara operasi kemas kini. PostgreSQL: UPDATE
Gunakan penyataan penguncian berasingan yang ditunggu sehingga selesai di dalam transaksi. Transaksi SQL ilustrasi ini menyasarkan artikel 1, 2 dan 3; jadual persediaan ditunjukkan dalam bahagian seterusnya.
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;
Penyataan pertama menetapkan susunan pemerolehan kunci. Kemas kini kemudian beroperasi pada baris yang sudah dikunci oleh transaksi itu. Kunci baris biasanya kekal sehingga transaksi tamat. Pastikan tempoh ini singkat: sediakan input sebelum memulakan transaksi dan elakkan menunggu input desktop semasa kunci dipegang. PostgreSQL: Penguncian Eksplisit
Syarat berikut mentakrifkan kontrak susunan contoh ini:
- Setiap kelompok yang bersaing menggunakan susunan menaik artikel
id. - ID kekal tidak berubah semasa kerja serentak.
- Setiap kelompok mengenal pasti keseluruhan set sasarannya sebelum mengunci.
- Tiada penyataan terdahulu yang telah mengunci baris artikel dalam susunan lain.
- Jadual contoh tidak mempunyai pencetus atau operasi jadual berkaitan yang memperkenalkan kebergantungan kunci tambahan.
Ini ialah prasyarat untuk menggunakan pertimbangan ini pada beban kerja tersebut. Transaksi yang mengunci artikel 9 terlebih dahulu kemudian meminta artikel 1 hingga 3 sudah menyimpang daripada kontrak, walaupun pertanyaan akhirnya mengandungi ORDER BY id.
Jalankan contoh node-postgres lengkap pada Mac anda
Prasyaratnya ialah pelayan PostgreSQL setempat yang sedang berjalan, pangkalan data percubaan sedia ada, Node.js dengan npm, dan psql tersedia dalam Terminal. Konfigurasikan sambungan pangkalan data sedia ada melalui pemboleh ubah persekitaran PostgreSQL. Himpunan sambungan node-postgres dan psql menggunakan pemboleh ubah sambungan yang sama; tetapkan PGHOST secara eksplisit jika perlu supaya kedua-dua arahan sampai ke pelayan yang sama. node-postgres: Menyambung
Dalam folder kerja baharu, pasang kebergantungan:
npm init -y
npm install pg
Simpan persediaan lengkap berikut sebagai setup.sql. Ia mencipta jadual demonstrasi baharu dan tiga artikel sintetik. Jalankannya sekali dalam pangkalan data percubaan. Skrip ini sengaja gagal jika jadual tersebut sudah wujud, bukannya menggantikan kandungannya.
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
Simpan program lengkap seterusnya sebagai update-articles.mjs. Semua penyataan dalam sesuatu percubaan menggunakan klien yang dikembalikan oleh pool.connect(). node-postgres secara jelas mensyaratkan penggunaan satu klien sepanjang transaksi; menghantar penyataan transaksi melalui pool.query() tidak memenuhi syarat itu. node-postgres: Transaksi
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
Tatasusunan itu dihantar sebagai satu parameter dan ditaip secara eksplisit kepada integer[]; node-postgres mendokumenkan penukaran tatasusunan JavaScript kepada nilai tatasusunan PostgreSQL. Penyahgandaan menjadikan bilangan yang diminta merujuk kepada artikel yang berbeza. ORDER BY pangkalan data, bukannya susunan tatasusunan input, menetapkan susunan pemerolehan kunci. node-postgres: Pertanyaan
Pada jadual yang baru dimulakan, tanpa penulis lain dan dengan kedua-dua kelompok berjaya, semakan yang dijangka ialah 1 untuk artikel 1 dan 2 untuk artikel 2 dan 3. Jumlah ini ditentukan oleh keanggotaan kelompok, tanpa mengira kelompok mana yang selesai dahulu. Pelaksanaan skrip seterusnya akan menambah nilai sedia ada sekali lagi. ID yang diminta tetapi tiada akan membatalkan kelompok itu dan bukannya mengemas kini hanya baris yang ditemui secara senyap.
Setiap kelompok ialah transaksi berasingan. Jika satu berjaya dan satu lagi gagal secara kekal, kelompok yang berjaya kekal dikomit. Promise.allSettled() membolehkan kedua-dua percubaan selesai sebelum program menutup himpunan sambungan. pool.query() terakhir berjalan di luar kedua-dua transaksi. API himpunan sambungan mendokumenkan cara melepaskan klien yang telah digunakan, memusnahkan klien jika perlu dan menamatkan himpunan sambungan selepas kerja skrip selesai. node-postgres: Himpunan Sambungan
Semakan penormalan dan aritmetik semakan ilustrasi telah dilaksanakan dengan Node.js. PostgreSQL tidak tersedia dalam persekitaran penyelidikan, jadi SQL dan tingkah laku keserentakan pangkalan data disemak berdasarkan dokumentasi dan bukannya dilaksanakan.
Kekalkan tahap pengasingan yang diperlukan oleh tugas anda
Program ini menggunakan READ COMMITTED secara eksplisit untuk kemas kini ID tetap. Pada tahap ini, penyataan penguncian mungkin menunggu pengemas kini, kemudian mengunci versi baris yang telah dikemas kini jika versi itu masih memenuhi syarat. Setiap penyataan mengambil syot kilat baharu. Oleh itu, baris yang dipadam sebelum pemerolehan kunci mungkin tiada; program mengendalikannya melalui semakan bilangannya. PostgreSQL: Pengasingan Transaksi
Jangan turunkan tahap pengasingan transaksi sedia ada semata-mata untuk menggunakan contoh ini. Dengan REPEATABLE READ, percubaan untuk mengunci atau mengemas kini baris yang berubah sejak syot kilat transaksi boleh menyebabkan kegagalan pensirian. SERIALIZABLE juga memerlukan pengendalian percubaan semula dan mencegah anomali pensirian yang dikomit; ia tidak menggantikan susunan konsisten bagi kunci baris biasa. PostgreSQL: Pengasingan Transaksi
Mengunci baris yang diketahui tidak memastikan keanggotaan predikat kekal tidak berubah. Contohnya, contoh ini tidak melaksanakan “kemas kini setiap artikel yang tajuknya sepadan dengan syarat, termasuk penambahan serentak.” Itu ialah tugas konsistensi yang berbeza. Tentukan keperluan tahap pengasingan dan penyelarasan sebelum menyesuaikan resipi ID tetap ini.
Pastikan juga lajur susunan tidak berubah. PostgreSQL mendokumenkan bahawa di bawah READ COMMITTED, pengisihan berlaku sebelum penguncian; nilai yang berubah semasa penyataan menunggu boleh menyebabkan baris yang dikembalikan kelihatan tidak mengikut susunan. Menggunakan ID yang tidak berubah mengelakkan kerumitan khusus itu. PostgreSQL: SELECT
Cuba semula transaksi lengkap dan siasat kegagalan berulang
PostgreSQL mengenal pasti kegagalan deadlock sebagai SQLSTATE 40P01 dan kegagalan pensirian sebagai 40001. Cuba semula keseluruhan transaksi, termasuk sebarang keputusan bergantung pada pangkalan data yang menentukan penyataan atau nilainya. Mencuba semula hanya kemas kini terakhir tidak memenuhi syarat itu. PostgreSQL: Pengendalian Kegagalan Pensirian
Contoh ini membenarkan tiga percubaan keseluruhannya. Jeda eksponen pendek dan rawak kecil ialah pilihan aplikasi ilustrasi, bukannya tetapan lalai PostgreSQL. Klien dilepaskan sebelum jeda, percubaan dihentikan serta-merta bagi ralat lain, dan kegagalan selepas semua percubaan dilaporkan. PostgreSQL memberi amaran bahawa percubaan semula tidak menjamin penyelesaian akhirnya. PostgreSQL: Pengendalian Kegagalan Pensirian
Gunakan panduan keputusan ini apabila menyesuaikan pelaksanaan:
| Situasi | Tindakan |
|---|---|
| Kelompok lain sudah memegang baris yang diminta | Benarkan pemerolehan tersusun menunggu jika tugas memerlukan semua baris yang diminta. |
| ID yang diminta tiada | Buat rollback dan laporkan keadaan sasaran yang tiada. |
SQLSTATE ialah 40P01 atau 40001 |
Buat rollback, kemudian cuba semula keseluruhan transaksi yang layak dalam had yang ditetapkan. |
| Kegagalan berterusan selepas penguncian tersusun | Periksa penyataan terdahulu, penulis lain, pencetus dan jadual tambahan untuk mengesan penyimpangan daripada kontrak susunan. |
| Operasi menyentuh beberapa jadual | Tetapkan susunan jadual yang sama selain susunan baris untuk semua laluan yang bersaing. |
| Kelompok mesti dikomit secara atomik | Kekalkan satu transaksi; bahagian yang dikomit secara berasingan akan mengubah tugas. |
Elakkan menambah SKIP LOCKED pada kelompok yang menyasarkan semua baris ini: ia meninggalkan baris yang tidak dapat dikunci serta-merta. PostgreSQL menerangkannya sebagai berguna untuk pengguna gaya baris gilir dan tidak sesuai untuk paparan umum yang konsisten. NOWAIT melaporkan ralat dan bukannya menunggu kunci baris yang bercanggah; ia mengubah tingkah laku menunggu tetapi tidak melengkapkan kelompok. PostgreSQL: SELECT
Pelaksanaan ini sedia disesuaikan apabila anda dapat menyatakan keseluruhan set sasaran, mengekalkan kunci susunannya tanpa perubahan, dan menggunakan peraturan pemerolehan yang sama merentas transaksi yang bersaing. Kekalkan semakan baris yang tiada serta percubaan semula seluruh transaksi yang terhad supaya pertentangan atau input tidak lengkap menghasilkan hasil yang jelas.
Sumber
PostgreSQL 18: Penguncian Eksplisit — Kumpulan Pembangunan PostgreSQL.
PostgreSQL 18: UPDATE — Kumpulan Pembangunan PostgreSQL.
PostgreSQL 18: SELECT — Kumpulan Pembangunan PostgreSQL.
PostgreSQL 18: Pengasingan Transaksi — Kumpulan Pembangunan PostgreSQL.
PostgreSQL 18: Pengendalian Kegagalan Pensirian — Kumpulan Pembangunan PostgreSQL.
Transaksi — node-postgres.
Menyambung — node-postgres.
Pertanyaan — node-postgres.
Himpunan Sambungan — node-postgres.