Makalenin mantıksal anahtarı üzerinde benzersiz bir kısıtlama ve INSERT ... ON CONFLICT kullanarak eşzamanlı kontrol-et-sonra-ekle yinelenmelerini önleyin. Mevcut satıra da ihtiyacınız varsa işlem yalıtım düzeyinize uygun bir alma stratejisi kullanın.

Bu eğitim, sentetik bir makaleyi bir kez oluşturup satırını eşzamanlı çağıranlara döndüren bir Node.js işlevine ihtiyaç duyan, macOS Terminal üzerinde çalışan geliştiricilere yöneliktir. Örneklerde ilk makalenin başlığı korunur. Güncel PostgreSQL belgeleri ve resmi node-postgres belgeleri kullanılmıştır.

Tek başına bir işlem neden yinelenen kayıtları önlemez

İki çağrının paper-shapes kısa adıyla bir makale oluşturmaya çalıştığını düşünün. Her biri önce bu kısa adın var olup olmadığını denetler, ardından sorgu sonuç döndürmezse ekleme yapar. PostgreSQL'in varsayılan Read Committed yalıtım düzeyinde normal bir sorgu, deyim anlık görüntüsündeki onaylanmış verileri görür. Bu nedenle her iki denetim de makalenin bulunmadığını bildirebilir. PostgreSQL: İşlem Yalıtımı

Bu örnek zamanlama, uygulama düzeyindeki yarış durumunu gösterir:

Adım Çağrı A Çağrı B
1 paper-shapes denetimi: yok
2 paper-shapes denetimi: yok
3 Makaleyi ekler
4 Aynı kısa adı eklemeye çalışır
5 İşlemi onaylar İşlemi onaylamaya çalışır

Kısa ad üzerinde benzersiz bir kısıtlama yoksa bu eklemeler ayrı satırlar oluşturabilir. Otomatik oluşturulan bir kimlik üzerindeki birincil anahtar, kısa adların benzersiz olması gerektiği kuralını ifade etmez. PostgreSQL, tek bir sütunda veya sütun birleşimlerinde benzersiz kısıtlamaları destekler. PostgreSQL: Kısıtlamalar

Denetimi ve eklemeyi BEGIN ile COMMIT içine almak, bulunmayan kısa adı ayırmaz. Boş bir aramaya FOR UPDATE eklemek de sorunu çözmez: bu ifade bulunan satırları kilitler; bulunmayan makale için kilitlenecek bir satır yoktur. Bu, PostgreSQL'in belgelenmiş satır kilitleme davranışından kaynaklanır. PostgreSQL: Açık Kilitleme

Yinelenen kayıt sayılan durumu tam olarak tanımlayın

Bu örnekte, tam eşleşen bir kısa ad tek bir makaleyi tanımlar. Başlık içeriktir; bu nedenle aynı kısa adla gelen ikinci bir istek, başlığı değiştirmek yerine kayıtlı makaleyi almalıdır.

Mevcut bir yerel PostgreSQL kurulumu, komut satırı araçları ve npm ile Node.js'in hazır olduğunu varsayarak başlayın. macOS Terminal içinde yeni bir demo veritabanı oluşturup SQL istemini açın:

createdb article_concurrency_demo
psql -d article_concurrency_demo

Yeni veritabanında bu şemayı bir kez çalıştırın:

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

Ayrı UNIQUE (slug) kısıtlaması makale kuralını uygular. NOT NULL da önemlidir: normal benzersiz kısıtlamalar varsayılan olarak birden çok null değere izin verir. PostgreSQL bu kısıtlama için otomatik olarak benzersiz bir dizin oluşturur. PostgreSQL: Kısıtlamalar

Mevcut bir tabloyu uyarlıyorsanız satırları otomatik olarak silmek yerine önce yinelenen anahtarları inceleyin. Aşağıdaki tanılama sorgusu yukarıdaki şemayı kullanır ve içeriği olduğu gibi bırakır:

SELECT slug, count(*) AS row_count
FROM articles
GROUP BY slug
HAVING count(*) > 1;

Kısıtlamayı mevcut bir tabloya eklemeden önce hangi kayıtlı içeriğin korunacağına karar verin. Ayrıca kullanılacak anahtarı belirleyin: iki makalenin farklı koleksiyonlarda aynı kısa adı kullanabilmesi gerekiyorsa kural, koleksiyon ve kısa ad gibi bileşik bir anahtar gerektirir. Aramayı ve çakışma hedefini aynı kuralla uyumlu tutun.

Gerekli çakışma davranışını seçin

Bu görevde çakışma, olağan bir durum dalıdır. SQL'i seçmeden önce ne anlama geldiğine karar verin. PostgreSQL, çakışan eklemeyi atlamayı veya mevcut satırı güncellemeyi destekler; RETURNING yalnızca gerçekten eklenen ya da güncellenen satırları döndürür. PostgreSQL: INSERT

İstenen sonuç Yaklaşım Döndürülen sonuç
Yoksa oluştur; varsa atla ON CONFLICT (slug) DO NOTHING RETURNING ile atlanan ekleme satır döndürmez
Yoksa oluştur; varsa başlığı değiştir ON CONFLICT (slug) DO UPDATE RETURNING ile eklenen veya güncellenen satır
Yoksa oluştur; varsa koruyup al DO NOTHING, ardından ayrı bir arama Sonraki bölümde uygulanmıştır

Yalnızca ekleme işlemi için aşağıdaki eksiksiz SQL deyimi demo şemasıyla çalışır:

INSERT INTO articles (slug, title)
VALUES ('paper-shapes', 'Paper Shapes')
ON CONFLICT (slug) DO NOTHING
RETURNING id, slug, title;

Başlığı değiştirmek istenen davranışsa bunun yerine şu deyimi kullanın:

INSERT INTO articles (slug, title)
VALUES ('paper-shapes', 'Paper Shapes — Revised')
ON CONFLICT (slug) DO UPDATE
SET title = EXCLUDED.title
RETURNING id, slug, title;

Burada EXCLUDED.title, önerilen başlığı ifade eder. Koşulsuz bir DO UPDATE, bağımsız hatalar ve yalıtım düzeyindeki hata işleme koşullarına tabi olarak belgelenmiş atomik ekleme veya güncelleme sonucunu sağlar. WHERE koşulu güncellemeyi reddeden koşullu bir güncelleme hiçbir satır döndürmeyebilir. Güncellemeyi yalnızca kayıtlı makaleyi değiştirmenin uygun olduğu durumlarda seçin. PostgreSQL: INSERT

Read Committed düzeyinde oluştur veya mevcut olanı döndür işlevini uygulayın

Bir görünürlük sorunu vardır: DO NOTHING, deyimin anlık görüntüsünde görünmeyen eşzamanlı bir satır nedeniyle eklemeyi atlayabilir. Bu yüzden aynı deyim içindeki geri dönüş araması da bu satırı bulamayabilir. Ayrı bir Read Committed sorgusu yeni bir anlık görüntü alır. Aşağıdaki uygulamanın dayanağı budur. PostgreSQL: İşlem Yalıtımı

\q yazarak SQL isteminden çıkın. Yeni bir çalışma klasöründe Node.js bağımlılığını kurun:

mkdir article-concurrency-demo
cd article-concurrency-demo
npm init -y
npm install pg

Aşağıdaki eksiksiz programı demo.mjs olarak kaydedin. Yukarıda oluşturulan veritabanı ve tabloyu varsayar. Havuz, belirtilen veritabanını ve diğer standart bağlantı ayarlarını kullanır; yerel ayarlar farklı olduğunda node-postgres PostgreSQL bağlantı ortam değişkenlerini destekler. node-postgres: Bağlanma

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

Programı bu klasörden çalıştırın:

node demo.mjs

Belirtilen kurulumla ve demo satırlarını değiştiren başka bir işlem yokken beklenen son çıktı şöyledir:

{ calls: 8, matchingRows: 1 }

Programın JavaScript sözdizimi Node.js ile denetlendi. PostgreSQL araçları mevcut olmadığından veritabanı demosu burada çalıştırılmadı; yukarıdaki çıktı beklenen sonucu gösterir.

Her işlem, tüm deyimler için havuzdan alınmış tek bir istemci kullanır. node-postgres bunu açıkça gerektirir: işlem deyimlerini pool.query üzerinden göndermek farklı istemcilere yönlendirilmelerine yol açabilir. Doğrulama sorgusu, işlemler tamamlandıktan sonra pool.query kullanır. node-postgres: İşlemler

Sorgu metni ve değerleri, sürücünün parametreli sorgu arayüzüne uygun olarak $1 ve $2 kullanılarak ayrı ayrı iletilir. node-postgres: Sorgular

Döngü, ek bir örnek zamanlama durumunu ele alır: arama satırı bulmadan önce başka bir işlem çakışan makaleyi silebilir. Aramanın sonuç vermemesi, ekleme denemesini yeniden başlatır; açık bir örnek sınırı olarak beş deneme kullanılır. Denemeler tükenirse tanımsız bir satır yerine hata üretilir.

Satır alındıktan sonra FOR UPDATE, bu işlem sona erene kadar başka bir işlemin satırı değiştirmesini veya silmesini engeller. Bu, mevcut bir sonucu korur; çakışan eklemeleri ise benzersiz kısıtlama yönetir. Onaylamadan sonra başka bir işlem satırı yeniden değiştirebilir. Bağımlı veritabanı işlemlerinin kilitli makaleyi kullanması gerekiyorsa bu işlemleri COMMIT öncesine yerleştirin. PostgreSQL: Açık Kilitleme

Yalıtım düzeylerinin sınırlarını koruyun

Örnek açıkça Read Committed seçer. Repeatable Read, ilk önemli deyimle oluşturulan bir işlem anlık görüntüsü kullanır; dolayısıyla başka bir sorgu bu görüntüyü basitçe yenileyemez. Serializable, serileştirilmiş eşdeğer onaylanmış sonuçlar sağlar; ancak işlemleri iptal edebilir ve belgelenmiş örtüşen durumlarda yine de benzersiz ihlalleri üretebilir. MERGE de aynı eşzamanlı ekleme veya güncelleme garantisini sağlamaz. PostgreSQL: İşlem Yalıtımı

Repeatable Read veya Serializable için SQLSTATE 40001 sonrasında, deyimlere yol açan uygulama kararları da dahil olmak üzere tüm işlemi yeniden denemeye hazırlıklı olun. Yalnızca son sorguyu yeniden denemek yeterli değildir. PostgreSQL, 40P01 değerini de yeniden denenebilecek bir kilitlenme hatası olarak tanımlar. PostgreSQL: Serileştirme Hatası İşleme

Her 23505 benzersiz ihlalini geçici kabul edip otomatik olarak yeniden denemeyin. Bu, kalıcı bir çakışmayı gösterebilir. Yeniden deneme ilkesi beklenen durumu tanımlamalı, deneme sayısını sınırlamalı ve sınır aşıldığında durumu bildirmelidir; PostgreSQL yeniden denemelerin sonunda başarılı olacağını garanti etmez. Demo, beklenen kısa ad çakışmasını ON CONFLICT ile ele alırken diğer hataları üst katmana iletir. PostgreSQL: Serileştirme Hatası İşleme

Bu oluştur veya mevcut olanı döndür görevi için ayrım açıktır: veritabanı kısıtlaması makale kimliğini ifade eder, çakışma eylemi mevcut makaleyle ne yapılacağını belirler, işlem ise almayı ve buna bağlı işleri denetler. Bu seçimleri uyumlu tutun; böylece eşzamanlı çağıranlar içeriği fark ettirmeden değiştirmeden tek bir makaleyi paylaşabilir.

Kaynaklar