Eşzamanlı PostgreSQL güncellemeleri çakıştığında kilitlenmeleri önlemek için güncellemeden önce her işlemin hedef kümesinin tamamını aynı değişmez anahtar sırasına göre kilitleyin. İşlemi tek bir node-postgres istemcisinde tutun ve uygun hatalarda baştan yeniden deneyin.

Bu kılavuz, sentetik makale satırlarını tamamen güncellemesi gereken iki eşzamanlı toplu işlem üzerinde macOS Terminal'de çalışan bir geliştirici içindir. Örnekte PostgreSQL 18'i belirten güncel PostgreSQL belgeleri ve güncel node-postgres belgeleri kullanılır. Kapsam, eşzamanlı çalışma sırasında kimliklerin değişmeden kaldığı, mevcut makale kimliklerinden oluşan bilinen bir listedir. PostgreSQL: Açık Kilitleme

Çakışan güncellemeler neden kilitlenmeye yol açabilir

İki işlemi ele alalım: A önce makale 1'i, sonra makale 2'yi günceller; B önce makale 2'yi, sonra makale 1'i günceller. Her biri ikinciyi istemeden önce ilk satır kilidini alırsa diğerinin tuttuğu kilidi bekler. PostgreSQL döngüyü algılar ve işlemlerden birini iptal eder; uygulama hangisinin iptal edileceğini seçemez. Açık kilitleme ifadeleri kullanılmasa bile sıradan güncellemeler bu duruma yol açabilir. PostgreSQL: Açık Kilitleme

Burada önemli olan, çakışma ile döngü arasındaki farktır. Çakışma beklemeyi gerektirebilir. Döngü ise hiçbir işlemin ilerlemesini engeller. PostgreSQL, uygulamalar genelinde tutarlı bir kilit edinme sırası kullanılmasını ve gereken en güçlü kilit kipinin önce edinilmesini önerir. PostgreSQL: Açık Kilitleme

Bu makaledeki iş yükü için tek bir kural benimseyin: herhangi bir makaleyi değiştirmeden önce, istenen tüm makale satırlarını artan id sırasına göre kilitleyin. Bu kuralı, makalelere dokunan daha önceki ifadeler de dahil olmak üzere tüm işlem boyunca uygulayın.

Güncellemeden önce hedef kümenin tamamını kilitleyin

Bir JavaScript dizisini sıralamak, toplu SQL güncellemesine satırlara bu sırada erişmesi talimatını vermez. PostgreSQL'in UPDATE söz diziminde doğrudan bir ORDER BY yan tümcesi yoktur. Belgelerinde, seçim sorgusu aracılığıyla sıralı kilitleme gösterilir ve ortak bir sıralamanın güncelleme işlemleri arasındaki kilitlenmeleri önleyebileceği açıklanır. PostgreSQL: UPDATE

İşlem içinde ayrı ve tamamlanmasını beklediğiniz bir kilitleme ifadesi kullanın. Aşağıdaki örnek SQL işlemi 1, 2 ve 3 numaralı makaleleri hedefler; kurulum tablosu sonraki bölümde yer alır.

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;

İlk ifade edinme sırasını belirler. Ardından gelen güncelleme, bu işlem tarafından zaten kilitlenmiş satırlar üzerinde çalışır. Satır kilitleri normalde işlem bitene kadar tutulur. Bu aralığı kısa tutun: girdileri işleme başlamadan önce hazırlayın ve kilitler tutulurken masaüstü girdisini beklemeyin. PostgreSQL: Açık Kilitleme

Aşağıdaki koşullar, örneğin sıralama sözleşmesini tanımlar:

  • Rekabet eden tüm toplu işlemler artan makale id sırasını kullanır.
  • Eşzamanlı çalışma sırasında kimlikler değişmez.
  • Her toplu işlem kilitlemeden önce hedef kümesinin tamamını belirler.
  • Daha önceki hiçbir ifade makale satırlarını farklı bir sırada kilitlememiştir.
  • Örnek tabloda ek kilit bağımlılıkları oluşturan tetikleyiciler veya ilişkili tablo işlemleri yoktur.

Bunlar, bu iş yüküne ilişkin çıkarımları uygulamak için gereken ön koşullardır. Önce 9 numaralı makaleyi kilitleyip daha sonra 1'den 3'e kadar olan makaleleri isteyen bir işlem, son sorgusu ORDER BY id içerse bile sözleşmeden sapmış olur.

Mac'inizde node-postgres ile eksiksiz bir örnek çalıştırın

Ön koşullar: çalışan bir yerel PostgreSQL sunucusu, mevcut bir deneme veritabanı, npm yüklü Node.js ve Terminal'de kullanılabilir psql. Mevcut veritabanı bağlantınızı PostgreSQL ortam değişkenleri üzerinden yapılandırın. node-postgres havuzları ve psql aynı bağlantı değişkenlerini kullanır; her iki komutun da aynı sunucuya ulaşması için gerekirse PGHOST değişkenini açıkça ayarlayın. node-postgres: Bağlanma

Yeni bir çalışma klasöründe bağımlılığı yükleyin:

npm init -y
npm install pg

Aşağıdaki eksiksiz kurulum kodunu setup.sql adıyla kaydedin. Yeni bir demo tablosu ve üç sentetik makale oluşturur. Deneme veritabanında bir kez çalıştırın. Mevcut içerikleri değiştirmek yerine, tablo zaten varsa bilerek hata verir.

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

Sonraki eksiksiz programı update-articles.mjs adıyla kaydedin. Bir denemeye ait tüm ifadeler pool.connect() tarafından döndürülen istemciyi kullanır. node-postgres, işlem boyunca tek bir istemcinin kullanılmasını açıkça şart koşar; işlem ifadelerini pool.query() üzerinden göndermek bu şartı sağlamaz. node-postgres: İşlemler

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

Dizi tek parametre olarak aktarılır ve açıkça integer[] türüne dönüştürülür; node-postgres, JavaScript dizilerinin PostgreSQL dizi değerlerine dönüştürülmesini belgeler. Tekilleştirme, istenen sayının farklı makaleleri ifade etmesini sağlar. Kilit edinme sırasını girdi dizisinin sırası değil, veritabanındaki ORDER BY belirler. node-postgres: Sorgular

Yeni başlatılmış bir tabloda, başka yazıcı işlemler yokken ve her iki toplu işlem de başarıyla tamamlandığında, 1 numaralı makalenin beklenen sürüm değeri 1, 2 ve 3 numaralı makalelerin beklenen sürüm değeri ise 2'dir. Bu toplamlar, hangi toplu işlemin önce tamamlandığından bağımsız olarak toplu işlem üyeliğinden çıkar. Betiğin sonraki çalıştırmaları mevcut değerleri yeniden artırır. İstenen bir kimlik eksikse toplu işlem, yalnızca bulunan satırları sessizce güncellemek yerine iptal edilir.

Her toplu işlem ayrı bir işlemdir. Biri başarılı olur, diğeri kalıcı olarak başarısız olursa başarılı olan toplu işlem kaydedilmiş olarak kalır. Promise.allSettled(), program havuzu kapatmadan önce her iki denemenin de tamamlanmasını sağlar. Son pool.query() ifadesi işlemlerin dışında çalışır. Havuz API'si, alınan istemcilerin serbest bırakılmasını, gerektiğinde istemcinin yok edilmesini ve betik işi tamamlandıktan sonra havuzun kapatılmasını belgeler. node-postgres: Havuz

Normalleştirme kontrolleri ve örnek sürüm aritmetiği Node.js ile yürütüldü. Araştırma ortamında PostgreSQL kullanılamadığından SQL ve veritabanı eşzamanlılık davranışı çalıştırılmak yerine belgelerle karşılaştırılarak denetlendi.

Görevinizin gerektirdiği yalıtım düzeyini koruyun

Program, sabit kimlikli güncellemeler için açıkça READ COMMITTED kullanır. Bu düzeyde bir kilitleme ifadesi, bir güncelleyiciyi bekleyebilir ve ardından güncellenmiş satır sürümünü, koşula hâlâ uyuyorsa kilitleyebilir. Her ifade yeni bir anlık görüntü alır. Bu nedenle, edinme gerçekleşmeden önce silinen bir satır bulunmayabilir; program bunu satır sayısı kontrolüyle ele alır. PostgreSQL: İşlem Yalıtımı

Bu örneği kullanabilmek için mevcut bir işlemin yalıtım düzeyini düşürmeyin. REPEATABLE READ altında, işlem anlık görüntüsünden bu yana değiştirilmiş bir satırı kilitleme veya güncelleme girişimi serileştirme hatasına yol açabilir. SERIALIZABLE da yeniden deneme işlemlerini gerektirir ve kaydedilmiş serileştirme anormalliklerini önler; sıradan satır kilitleri için tutarlı bir sıralama ihtiyacını ortadan kaldırmaz. PostgreSQL: İşlem Yalıtımı

Bilinen satırları kilitlemek, bir koşulun üyelerinin değişmeden kalacağını garanti etmez. Örneğin, bu örnek “koşula uyan başlığa sahip tüm makaleleri, eşzamanlı eklemeler de dahil, güncelle” işlemini gerçekleştirmez. Bu farklı bir tutarlılık görevidir. Sabit kimlik tarifini uyarlamadan önce bu görevin yalıtım ve eşgüdüm gereksinimlerini belirleyin.

Ayrıca sıralama sütununu değişmez tutun. PostgreSQL, READ COMMITTED altında sıralamanın kilitlemeden önce yapıldığını belgeler; ifade beklerken değişen değerler, döndürülen satırların sırasının bozulmuş görünmesine neden olabilir. Değişmeyen kimliklerin kullanılması bu özel durumu önler. PostgreSQL: SELECT

İşlemlerin tamamını yeniden deneyin ve yinelenen hataları inceleyin

PostgreSQL, kilitlenme hatalarını SQLSTATE 40P01, serileştirme hatalarını ise 40001 olarak tanımlar. İfadeleri veya değerleri belirleyen, veritabanına bağlı kararlar da dahil olmak üzere işlemin tamamını yeniden deneyin. Yalnızca son güncellemeyi yeniden denemek bu gereksinimi karşılamaz. PostgreSQL: Serileştirme Hatalarını Ele Alma

Örnek, toplam üç denemeye izin verir. Kısa üstel gecikme ve rastgele pay, PostgreSQL varsayılanları değil, örnek amaçlı uygulama tercihleridir. Gecikmeden önce istemciyi serbest bırakır, diğer hatalarda hemen durur ve denemeler tükendiğinde bunu bildirir. PostgreSQL, yeniden denemelerin işlemin eninde sonunda tamamlanacağını garanti etmediği konusunda uyarır. PostgreSQL: Serileştirme Hatalarını Ele Alma

Uygulamayı uyarlarken bu karar yardımını kullanın:

Durum Eylem
Başka bir toplu işlem istenen bir satırı zaten tutuyor Görev istenen her satırı gerektiriyorsa sıralı edinmenin beklemesine izin verin.
İstenen bir kimlik mevcut değil Geri alın ve eksik hedef durumunu bildirin.
SQLSTATE 40P01 veya 40001 Geri alın, ardından koşulları karşılayan işlemin tamamını belirlenen sınırlar içinde yeniden deneyin.
Sıralı kilitlemeye rağmen hatalar sürüyor Önceki ifadelerin, diğer yazıcıların, tetikleyicilerin ve ek tabloların sıralama sözleşmesinden sapıp sapmadığını inceleyin.
İşlem birkaç tabloya dokunuyor Rekabet eden tüm yollar için satır sırasının yanı sıra ortak bir tablo sırası da belirleyin.
Toplu işlem atomik olarak kaydedilmek zorunda Tek işlemi koruyun; bağımsız olarak kaydedilen parçalar görevi değiştirir.

Hedeflerin tümünü kapsayan bu toplu işleme SKIP LOCKED eklemeyin: hemen kilitlenemeyen satırları atlar. PostgreSQL bunun kuyruk benzeri tüketiciler için yararlı olduğunu ve genel, tutarlı bir görünüm elde etmek için uygun olmadığını belirtir. NOWAIT, çakışan bir satır kilidini beklemek yerine hata bildirir; bekleme davranışını değiştirir ama toplu işlemi tamamlamaz. PostgreSQL: SELECT

Hedef kümesinin tamamını belirleyebildiğiniz, sıralama anahtarlarını değişmeden tutabildiğiniz ve rekabet eden işlemler boyunca aynı edinme kuralını uygulayabildiğinizde uygulama uyarlanmaya hazırdır. Çakışma veya eksik girdi açıkça ele alınsın diye eksik satır kontrolünü ve işlemin tamamına yönelik sınırlı yeniden denemeleri koruyun.

Kaynaklar