Cegah pendua akibat semakan-kemudian-sisipan serentak dengan kekangan unik pada kunci logik artikel dan INSERT ... ON CONFLICT. Apabila anda juga memerlukan baris sedia ada, gunakan strategi mendapatkan semula yang sepadan dengan tahap pengasingan transaksi anda.
Tutorial ini ditujukan kepada pembangun yang bekerja dalam macOS Terminal dan memerlukan fungsi Node.js yang mencipta artikel sintetik sekali sahaja serta memulangkan barisnya kepada pemanggil serentak. Contoh-contoh ini mengekalkan tajuk artikel pertama. Contoh ini menggunakan dokumentasi PostgreSQL semasa dan dokumentasi rasmi node-postgres.
Mengapa transaksi sahaja tidak mencegah pendua
Bayangkan dua panggilan cuba mencipta artikel dengan slug paper-shapes. Setiap panggilan mula-mula menyemak sama ada slug itu wujud, kemudian menyisipkannya jika pertanyaan tidak memulangkan apa-apa. Pada tahap pengasingan Read Committed lalai PostgreSQL, pertanyaan biasa melihat data yang telah dikomit setakat syot kilat penyataannya. Oleh itu, kedua-dua semakan boleh melaporkan bahawa artikel itu tiada. PostgreSQL: Transaction Isolation
Jadual ilustrasi ini menunjukkan perlumbaan pada peringkat aplikasi:
| Langkah | Panggilan A | Panggilan B |
|---|---|---|
| 1 | Menyemak paper-shapes: tiada |
|
| 2 | Menyemak paper-shapes: tiada |
|
| 3 | Menyisipkan artikel | |
| 4 | Cuba menyisipkan slug yang sama | |
| 5 | Mengomit | Cuba mengomit |
Tanpa kekangan unik pada slug, sisipan itu boleh menghasilkan baris yang berasingan. Kunci primer pada ID yang dijana secara automatik tidak menyatakan peraturan bahawa slug mesti unik. PostgreSQL menyokong kekangan unik pada satu lajur atau gabungan beberapa lajur. PostgreSQL: Constraints
Membalut semakan dan sisipan dalam BEGIN dan COMMIT tidak memperuntukkan slug yang tiada itu. Menambahkan FOR UPDATE pada carian yang kosong juga tidak menyelesaikan masalah tersebut: ia mengunci baris yang diperoleh, jadi artikel yang tiada tidak menyediakan baris untuk dikunci. Hal ini selaras dengan tingkah laku kunci baris yang didokumenkan oleh PostgreSQL. PostgreSQL: Explicit Locking
Tentukan dengan tepat perkara yang dianggap pendua
Untuk contoh ini, satu slug yang tepat mengenal pasti satu artikel. Tajuk ialah kandungan, jadi permintaan kedua dengan slug yang sama sepatutnya menerima artikel yang tersimpan dan bukannya menggantikan tajuknya.
Mulakan dengan pemasangan PostgreSQL setempat yang sedia ada, alat baris perintahnya dan Node.js dengan npm. Dalam macOS Terminal, cipta pangkalan data demonstrasi baharu dan buka gesaan SQLnya:
createdb article_concurrency_demo
psql -d article_concurrency_demo
Jalankan skema ini sekali dalam pangkalan data baharu itu:
CREATE TABLE articles (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slug text NOT NULL,
title text NOT NULL,
CONSTRAINT articles_slug_key UNIQUE (slug)
);
Kekangan UNIQUE (slug) yang berasingan menguatkuasakan peraturan artikel tersebut. NOT NULL juga penting: secara lalai, kekangan unik biasa membenarkan beberapa nilai null. PostgreSQL mencipta indeks unik secara automatik untuk kekangan ini. PostgreSQL: Constraints
Jika menyesuaikan jadual sedia ada, periksa kunci yang berulang dahulu dan bukannya memadam baris secara automatik. Pertanyaan diagnostik ini menggunakan skema di atas dan mengekalkan semua kandungan:
SELECT slug, count(*) AS row_count
FROM articles
GROUP BY slug
HAVING count(*) > 1;
Tentukan kandungan tersimpan yang hendak dikekalkan sebelum menambahkan kekangan pada jadual sedia ada. Tentukan juga kunci yang dimaksudkan: jika dua artikel boleh berkongsi slug dalam koleksi yang berbeza, peraturan itu memerlukan kunci gabungan seperti koleksi bersama slug. Pastikan carian dan sasaran konflik selaras dengan peraturan yang sama.
Pilih tingkah laku konflik yang diperlukan
Konflik ialah cabang biasa bagi tugas ini. Tentukan maksudnya sebelum memilih SQL. PostgreSQL menyokong sama ada melangkau sisipan yang bercanggah atau mengemas kini baris sedia ada; RETURNING memulangkan baris yang benar-benar disisipkan atau dikemas kini. PostgreSQL: INSERT
| Hasil yang diperlukan | Pendekatan | Hasil yang dipulangkan |
|---|---|---|
| Cipta jika tiada; jika tidak, langkau | ON CONFLICT (slug) DO NOTHING |
Dengan RETURNING, sisipan yang dilangkau tidak memulangkan baris |
| Cipta jika tiada; jika tidak, gantikan tajuk | ON CONFLICT (slug) DO UPDATE |
Dengan RETURNING, baris yang disisipkan atau dikemas kini |
| Cipta jika tiada; jika tidak, kekalkan dan dapatkan semula | DO NOTHING, kemudian carian berasingan |
Dilaksanakan dalam bahagian seterusnya |
Untuk operasi sisipan sahaja, penyata SQL lengkap ini berfungsi dengan skema demonstrasi:
INSERT INTO articles (slug, title)
VALUES ('paper-shapes', 'Paper Shapes')
ON CONFLICT (slug) DO NOTHING
RETURNING id, slug, title;
Jika penggantian tajuk memang tingkah laku yang dimaksudkan, gunakan penyata alternatif ini:
INSERT INTO articles (slug, title)
VALUES ('paper-shapes', 'Paper Shapes — Revised')
ON CONFLICT (slug) DO UPDATE
SET title = EXCLUDED.title
RETURNING id, slug, title;
Di sini, EXCLUDED.title merujuk kepada tajuk yang dicadangkan. DO UPDATE tanpa syarat memberikan hasil sisip-atau-kemas-kini atomik yang didokumenkan, tertakluk pada ralat bebas dan pengendalian kegagalan tahap pengasingan. Kemas kini bersyarat yang syarat WHERE-nya menolak kemas kini mungkin tidak memulangkan apa-apa. Pilih kemas kini jika mengubah artikel tersimpan memang wajar. PostgreSQL: INSERT
Laksanakan cipta-atau-pulangkan-yang-sedia-ada pada Read Committed
Terdapat perangkap keterlihatan: DO NOTHING boleh melangkau sisipan kerana baris serentak tidak kelihatan dalam syot kilat penyataan itu. Akibatnya, carian sandaran dalam penyataan yang sama juga boleh terlepas daripadanya. Pertanyaan Read Committed yang berasingan mendapat syot kilat baharu. Inilah asas pelaksanaan di bawah. PostgreSQL: Transaction Isolation
Keluar daripada gesaan SQL dengan \q. Dalam folder kerja baharu, pasang kebergantungan Node.js:
mkdir article-concurrency-demo
cd article-concurrency-demo
npm init -y
npm install pg
Simpan program lengkap berikut sebagai demo.mjs. Program ini mengandaikan pangkalan data dan jadual yang dicipta di atas. Kolam sambungannya menggunakan pangkalan data yang dinamakan serta tetapan sambungan standard yang lain; node-postgres menyokong pemboleh ubah persekitaran sambungan PostgreSQL apabila tetapan setempat berbeza. node-postgres: Connecting
import pg from 'pg';
import assert from 'node:assert/strict';
const { Pool } = pg;
const pool = new Pool({
database: 'article_concurrency_demo',
max: 8
});
async function getOrCreateArticle(slug, title) {
const client = await pool.connect();
try {
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
for (let attempt = 0; attempt < 5; attempt += 1) {
const inserted = await client.query(`
INSERT INTO articles (slug, title)
VALUES ($1, $2)
ON CONFLICT (slug) DO NOTHING
RETURNING id, slug, title
`, [slug, title]);
if (inserted.rows.length === 1) {
await client.query('COMMIT');
return inserted.rows[0];
}
const existing = await client.query(`
SELECT id, slug, title
FROM articles
WHERE slug = $1
FOR UPDATE
`, [slug]);
if (existing.rows.length === 1) {
await client.query('COMMIT');
return existing.rows[0];
}
}
throw new Error('Article kept disappearing; retry limit reached');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
try {
const results = await Promise.allSettled(
Array.from({ length: 8 }, () =>
getOrCreateArticle('paper-shapes', 'Paper Shapes'))
);
const failures = results.filter(result => result.status === 'rejected');
if (failures.length) {
throw new AggregateError(failures.map(result => result.reason));
}
const rows = results.map(result => result.value);
assert.equal(new Set(rows.map(row => row.id)).size, 1);
const count = await pool.query(
'SELECT count(*)::integer AS n FROM articles WHERE slug = $1',
['paper-shapes']
);
assert.equal(count.rows[0].n, 1);
console.log({ calls: rows.length, matchingRows: count.rows[0].n });
} finally {
await pool.end();
}
Jalankannya dari folder tersebut:
node demo.mjs
Dengan persediaan yang dinyatakan dan tanpa proses lain yang mengubah baris demonstrasi, output akhir yang dijangka ialah:
{ calls: 8, matchingRows: 1 }
Sintaks JavaScript program ini telah diperiksa dengan Node.js. Demonstrasi pangkalan data tidak dijalankan di sini kerana alat PostgreSQL tidak tersedia; output di atas ialah hasil yang dijangka.
Setiap transaksi menggunakan satu klien yang diperoleh untuk semua penyataan. node-postgres menyatakan keperluan ini secara jelas: mengeluarkan penyataan transaksi melalui pool.query boleh menghantarkannya kepada klien yang berbeza. Pertanyaan pengesahan menggunakan pool.query hanya selepas transaksi selesai. node-postgres: Transactions
Teks pertanyaan dan nilai dihantar secara berasingan menggunakan $1 dan $2, selaras dengan antara muka pertanyaan berparameter pemacu tersebut. node-postgres: Queries
Gelung ini mengendalikan satu lagi jalinan pelaksanaan ilustrasi: transaksi lain mungkin memadam artikel yang bercanggah sebelum carian mendapatkannya. Jika carian tidak menemui apa-apa, percubaan sisipan diulangi, dengan lima percubaan sebagai had contoh yang jelas. Jika semua percubaan habis, ralat akan terhasil dan bukannya baris undefined.
Selepas diperoleh, FOR UPDATE menghalang baris itu daripada diubah atau dipadam oleh transaksi lain sehingga transaksi ini berakhir. Ia melindungi hasil sedia ada; kekangan unik mengendalikan sisipan yang bersaing. Selepas komit, transaksi lain boleh mengubah baris itu sekali lagi. Jika kerja pangkalan data yang bergantung padanya perlu menggunakan artikel yang dikunci, letakkan kerja itu sebelum COMMIT. PostgreSQL: Explicit Locking
Kekalkan had tahap pengasingan
Contoh ini memilih Read Committed secara jelas. Repeatable Read menggunakan syot kilat transaksi yang ditetapkan oleh penyataan substantif pertamanya, jadi pertanyaan lain tidak boleh sekadar menyegarkannya. Serializable memberikan hasil terkomit yang setara dengan pelaksanaan bersiri, tetapi boleh membatalkan transaksi dan masih boleh menghasilkan pelanggaran unik dalam kes bertindih yang didokumenkan. MERGE juga tidak memberikan jaminan sisip-atau-kemas-kini serentak yang sama. PostgreSQL: Transaction Isolation
Untuk Repeatable Read atau Serializable, bersedia untuk mencuba semula keseluruhan transaksi selepas SQLSTATE 40001, termasuk keputusan aplikasi yang menghasilkan penyataannya. Mencuba semula pertanyaan terakhir sahaja tidak mencukupi. PostgreSQL juga mengenal pasti 40P01 sebagai kegagalan kebuntuan yang mungkin boleh dicuba semula. PostgreSQL: Serialization Failure Handling
Jangan anggap secara automatik setiap pelanggaran unik 23505 sebagai sementara. Ia mungkin menandakan konflik yang berterusan. Dasar percubaan semula sepatutnya mengenal pasti keadaan yang dijangka, mengehadkan percubaan dan melaporkan apabila percubaan habis; PostgreSQL tidak menjamin percubaan semula akhirnya akan berjaya. Demonstrasi ini mengendalikan pertembungan slug yang dijangka melalui ON CONFLICT, manakala ralat lain diteruskan. PostgreSQL: Serialization Failure Handling
Untuk tugas cipta-atau-pulangkan ini, pembahagiannya mudah: kekangan pangkalan data menyatakan identiti artikel, tindakan konflik menyatakan perkara yang perlu dilakukan terhadap artikel sedia ada, dan transaksi mengawal proses mendapatkan semula serta sebarang kerja yang bergantung padanya. Selaraskan pilihan ini, maka pemanggil serentak boleh berkongsi satu artikel tanpa kandungannya diganti secara senyap.
Sumber
- 5.5. Constraints — PostgreSQL Global Development Group
- INSERT — PostgreSQL Global Development Group
- 13.2. Transaction Isolation — PostgreSQL Global Development Group
- 13.3. Explicit Locking — PostgreSQL Global Development Group
- 13.5. Serialization Failure Handling — PostgreSQL Global Development Group
- Transactions — node-postgres
- Queries — node-postgres
- Connecting — node-postgres