İçeriğe geç
academia.sh

Ders 05 / 25

Çok Sürümlü Eşzamanlılık Denetimi

Kilit tabanlı ve sürüm tabanlı eşzamanlılık denetiminin farkı, aynı okumanın iki günlük kipinde yazarı engelleyip engellememesinin ölçülmesi, sürüm zinciri ile görünürlük kuralının model üzerinden kurulması, eski sürümlerin ne zaman kaldırılabildiği ve çok sürümlülüğün ortadan kaldırmadığı çakışmalar.

İçindekiler

Önceki dersin sonunda bir sınırlama kaldı: denetim noktası, o anda okuma yapan işlemlerin gördüğü sayfaların üzerine yazamaz. İleri SQL kursunda da benzer bir gözlem yapılmıştı — bir işlem içindeki okuma, dışarıda kesinleştirilen değişikliklerden etkilenmiyordu. İki gözlem de aynı mekanizmaya işaret eder ve o mekanizma henüz kurulmadı.

Soru şudur: bir satır güncellenirken aynı satırı okumakta olan bir işlem varsa, o okuma neyi görür? İki tutarlı cevap vardır. Ya okuyucu yazarı bekletir, ya da satırın eski hâli bir yerde saklanır. Bu ders ikinci yolu ele alır ve ilk yoldan farkını ölçer.

İki Yaklaşım

Kilit tabanlı denetim, tutarlılığı beklemeyle sağlar. Bir satırı okuyan işlem o satır üzerinde paylaşımlı bir kilit tutar; değiştirmek isteyen işlem dışlayıcı kilit ister ve okuma bitene kadar bekler. Sonuç doğrudur ama okuma ile yazma birbirini sıraya sokar.

Çok sürümlü eşzamanlılık denetimi (multiversion concurrency control), tutarlılığı saklamayla sağlar. Bir satır güncellendiğinde eski hâli silinmez; yeni bir sürüm eklenir ve her sürüm hangi işlem tarafından oluşturulup hangi işlem tarafından geçersiz kılındığını taşır. Okuyan işlem, kendi başlangıç anına uygun sürümü seçer. Yazar beklemez, okuyucu beklemez; iki taraf farklı sürümlere bakar.

İleri SQL kursunda tanıtılan anlık görüntü (snapshot) kavramının karşılığı budur. Anlık görüntü, kopyalanmış bir veritabanı değildir; hangi sürümlerin görüleceğini belirleyen bir ölçüttür.

Ölçülen Fark

Fark gözlemlenebilir. Aşağıdaki koşum aynı senaryoyu iki günlük kipiyle çalıştırır: bir okuyucu işlem açar ve okur, sonra ayrı bir bağlantıdaki yazar aynı tabloyu güncellemeye çalışır.

rm -f e.db e.db-wal e.db-shm e.db-journal
cat > engel.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
import { rmSync } from 'node:fs';

function dene(kip) {
  for (const ek of ['', '-wal', '-shm', '-journal']) rmSync('e.db' + ek, { force: true });
  const kur = new DatabaseSync('e.db');
  kur.exec(`PRAGMA journal_mode = ${kip}`);
  kur.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, iade_tarihi TEXT)');
  kur.exec("INSERT INTO odunc VALUES (1,4,NULL),(2,4,NULL),(3,7,'2024-03-09')");
  kur.close();

  const okuyucu = new DatabaseSync('e.db');
  const yazar = new DatabaseSync('e.db');
  const acik = (b) => b.prepare('SELECT count(*) AS c FROM odunc WHERE iade_tarihi IS NULL').get().c;

  okuyucu.exec('BEGIN');                       // okuyucu islemi acik kaliyor
  const once = acik(okuyucu);
  let yazma;
  try { yazar.exec("UPDATE odunc SET iade_tarihi = '2024-03-20' WHERE odunc_id = 1"); yazma = 'başarılı'; }
  catch (h) { yazma = 'engellendi: ' + h.message.split('\n')[0]; }
  const sonra = acik(okuyucu);
  okuyucu.exec('COMMIT');
  const bitince = acik(okuyucu);
  okuyucu.close(); yazar.close();
  console.log(`[${kip}] okuyucu işlem içinde: ${once}${sonra} | yazar: ${yazma} | işlem bitince: ${bitince}`);
}
dene('delete');
dene('wal');
JS
node engel.mjs
[delete] okuyucu işlem içinde: 2 → 2 | yazar: engellendi: database is locked | işlem bitince: 2
[wal] okuyucu işlem içinde: 2 → 2 | yazar: başarılı | işlem bitince: 1

Okuyucunun gördüğü sayı iki durumda da işlem boyunca değişmiyor: 2 → 2. Yalıtım her iki düzende de sağlanmış. Değişen, bunun nasıl sağlandığıdır.

Geri alma günlüğü kipinde yazar hiç ilerleyemedi: veritabanı kilitli. Okuma bitmeden yazma yapılamaz. Yazma öncesi günlük kipinde ise yazar işini tamamladı ve kesinleştirdi; okuyucu buna rağmen eski değeri görmeye devam etti ve ancak kendi işlemi bittikten sonra yeni değeri gördü (1). İki taraf da beklemedi.

Buradaki gözlem, çok sürümlülüğün gözlenen özelliğini doğrular: okuyucu ile yazıcı birbirini engellemez. Bu özelliğin nasıl gerçekleştirildiği motora göre değişir; kullanılan araçta yol, satır başına sürüm zincirleri değil, günlük üzerinden tanımlanan anlık görüntülerdir. Mekanizmanın kendisi bir sonraki bölümde model üzerinden kurulacak.

Sürüm Zinciri ve Görünürlük Kuralı

Satır başına sürüm tutan bir motorda her sürüm iki ek alan taşır: onu oluşturan işlemin numarası ve onu silen işlemin numarası. Silme alanı boşsa sürüm hâlâ geçerlidir. Güncelleme, tek bir işlemde iki şey yapar: mevcut sürümün silen alanını doldurur ve yeni bir sürüm ekler. Silme ise yalnız birincisini yapar.

Görünürlük kuralı tek cümledir: bir sürüm, anlık görüntüde oluşturan işlemi kesinleşmişse ve silen işlemi kesinleşmemişse görünür. Aşağıdaki model kuralı işletir. Gerçek motorların kuralı daha ayrıntılıdır — kendi işlemin görmediği değişiklikler, iptal edilmiş işlemler ve alt işlemler ayrıca ele alınır — ama çekirdek aynıdır.

cat > surum.mjs <<'JS'
// Model: satir surumleri ve gorunurluk kurali. Gercek motorun kendisi degil.
class Depo {
  constructor() { this.surumler = []; this.kesinlesen = new Set(); this.sonrakiIslem = 10; }
  islemBaslat() { const t = this.sonrakiIslem; this.sonrakiIslem += 10; return t; }
  kesinlestir(t) { this.kesinlesen.add(t); }
  anlikGoruntu() { return new Set(this.kesinlesen); }        // o andaki kesinlesmis islemler
  ekle(t, satir_id, deger) { this.surumler.push({ satir_id, deger, olusturan: t, silen: null }); }
  guncelle(t, satir_id, deger) {
    const eski = this.surumler.find((s) => s.satir_id === satir_id && s.silen === null);
    if (eski) eski.silen = t;
    this.surumler.push({ satir_id, deger, olusturan: t, silen: null });
  }
  gorunur(s, anlik) { return anlik.has(s.olusturan) && (s.silen === null || !anlik.has(s.silen)); }
  oku(satir_id, anlik) {
    const s = this.surumler.find((v) => v.satir_id === satir_id && this.gorunur(v, anlik));
    return s ? s.deger : '(yok)';
  }
  temizlenebilir(anliklar) {                                  // hicbir anlik goruntunun gormedigi surumler
    return this.surumler.filter((s) => anliklar.every((a) => !this.gorunur(s, a)));
  }
}

const d = new Depo();
const t1 = d.islemBaslat();                       // odunc kaydini olusturan islem
d.ekle(t1, 501, 'iade_tarihi = NULL'); d.kesinlestir(t1);

const okuyucuAnlik = d.anlikGoruntu();            // uzun okuma buradan basliyor
const t2 = d.islemBaslat();
d.guncelle(t2, 501, "iade_tarihi = '2024-03-20'"); d.kesinlestir(t2);
const yeniAnlik = d.anlikGoruntu();
const t3 = d.islemBaslat();
d.guncelle(t3, 501, "iade_tarihi = '2024-03-21'"); d.kesinlestir(t3);
const enYeniAnlik = d.anlikGoruntu();

console.log('sürüm zinciri (satır 501):');
for (const s of d.surumler)
  console.log(`  oluşturan=${s.olusturan}  silen=${s.silen ?? '—'}  ${s.deger}`);
console.log('okuyucunun anlık görüntüsü  →', d.oku(501, okuyucuAnlik));
console.log('ikinci anlık görüntü        →', d.oku(501, yeniAnlik));
console.log('en yeni anlık görüntü       →', d.oku(501, enYeniAnlik));
console.log('okuyucu açıkken temizlenebilir sürüm sayısı :',
            d.temizlenebilir([okuyucuAnlik, enYeniAnlik]).length);
console.log('okuyucu kapandıktan sonra                   :',
            d.temizlenebilir([enYeniAnlik]).length);
JS
node surum.mjs
sürüm zinciri (satır 501):
  oluşturan=10  silen=20  iade_tarihi = NULL
  oluşturan=20  silen=30  iade_tarihi = '2024-03-20'
  oluşturan=30  silen=—  iade_tarihi = '2024-03-21'
okuyucunun anlık görüntüsü  → iade_tarihi = NULL
ikinci anlık görüntü        → iade_tarihi = '2024-03-20'
en yeni anlık görüntü       → iade_tarihi = '2024-03-21'
okuyucu açıkken temizlenebilir sürüm sayısı : 1
okuyucu kapandıktan sonra                   : 2

Aynı ödünç kaydının üç sürümü aynı anda depoda duruyor ve üç ayrı anlık görüntü üçünü ayrı ayrı görüyor. Hiçbir bekleme yok; her okuyucu kendi ölçütüne uyan sürümü buluyor.

Bu düzenin bir sonucu, UPDATE deyiminin fiziksel olarak bir güncelleme olmamasıdır. Tek bir sütunun tek bir baytını değiştiren bir güncelleme bile satırın tamamının yeni bir kopyasını yazar. İleri SQL kursunda “anahtar sütunu güncelleyen UPDATE için de aynı bedel geçerlidir” denen dizin maliyetinin arkasında da bu vardır: yeni sürüm yeni bir yerdedir ve o satıra işaret eden dizin girdilerinin de güncellenmesi gerekir.

Sürümler Ne Zaman Kalkabilir

Modelin son iki satırı asıl yönetim sorununu gösteriyor. Eski sürümler sonsuza kadar tutulamaz; artık hiçbir anlık görüntünün görmediği bir sürüm kaldırılabilir. Ölçüt de buradan çıkar: en eski açık anlık görüntüden daha eski olan sürümler temizlenebilir.

Modelde okuyucunun anlık görüntüsü açıkken yalnız bir sürüm temizlenebilir durumda; okuyucu kapandığında sayı ikiye çıkıyor. Tek bir uzun okuma, üç sürümlük bir zincirin iki sürümünü yerinde tutmaya yetiyor.

Bu sonucun ölçeği önemlidir. Saatlerce açık kalan bir işlem, o süre boyunca yapılan bütün güncellemelerin eski sürümlerini yaşatır. Tablo satır sayısı değişmese bile diskte kapladığı yer sürekli büyür. Önceki derste denetim noktasının okuyucular tarafından geciktirilmesi de aynı kuralın başka bir yüzüdür.

Çok Sürümlülüğün Kaldırmadığı Çakışma

Okuyucu ile yazıcının birbirini engellememesi, bütün çakışmaların kalktığı anlamına gelmez. Aşağıdaki koşum iki noktayı birlikte gösterir: iki yazarın çakışması ve açık bir okumanın denetim noktasını durdurması.

rm -f y.db y.db-wal y.db-shm
cat > engel2.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
import { rmSync } from 'node:fs';
for (const ek of ['', '-wal', '-shm']) rmSync('y.db' + ek, { force: true });
const kur = new DatabaseSync('y.db');
kur.exec('PRAGMA journal_mode = WAL');
kur.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, iade_tarihi TEXT)');
kur.exec(`INSERT INTO odunc SELECT n, 1+(n*13)%500, NULL
          FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<20000) SELECT n FROM s)`);
kur.exec('PRAGMA wal_checkpoint(TRUNCATE)');
kur.close();

const a = new DatabaseSync('y.db'), b = new DatabaseSync('y.db');
a.exec('BEGIN IMMEDIATE');
a.exec("UPDATE odunc SET iade_tarihi = '2024-04-01' WHERE odunc_id = 1");
try { b.exec('BEGIN IMMEDIATE'); b.exec("UPDATE odunc SET iade_tarihi = '2024-04-02' WHERE odunc_id = 2"); console.log('iki yazar: ikisi de ilerledi'); }
catch (h) { console.log('iki yazar → ikinci yazar:', h.message.split('\n')[0]); }
a.exec('COMMIT');
a.close(); b.close();

// uzun okuma denetim noktasini geciktiriyor mu
const okuyucu = new DatabaseSync('y.db'), yazar = new DatabaseSync('y.db');
yazar.exec('PRAGMA wal_autocheckpoint = 0');
okuyucu.exec('BEGIN');
okuyucu.prepare('SELECT count(*) AS c FROM odunc').get();
for (let i = 1; i <= 3000; i++) yazar.exec(`UPDATE odunc SET iade_tarihi='2024-05-01' WHERE odunc_id=${1+(i*37)%20000}`);
console.log('okuyucu açıkken denetim noktası :', JSON.stringify(yazar.prepare('PRAGMA wal_checkpoint(PASSIVE)').get()));
okuyucu.exec('COMMIT');
console.log('okuyucu bitince denetim noktası :', JSON.stringify(yazar.prepare('PRAGMA wal_checkpoint(PASSIVE)').get()));
okuyucu.close(); yazar.close();
JS
node engel2.mjs
iki yazar → ikinci yazar: database is locked
okuyucu açıkken denetim noktası : {"busy":0,"log":3532,"checkpointed":0}
okuyucu bitince denetim noktası : {"busy":0,"log":3532,"checkpointed":3532}

Birinci satır sınırı gösteriyor: iki işlem aynı anda yazmaya kalktığında ikincisi ilerleyemiyor. Kullanılan araçta bu sınır veritabanının tamamı için geçerlidir; satır düzeyinde çakışma çözen motorlarda ise sınır yalnız aynı satırı güncelleyen işlemler arasındadır. Her iki durumda da ilke aynıdır: sürümler okumayı serbestleştirir, aynı veriyi değiştiren iki yazma yine sıraya girer. İleri SQL kursundaki kilitlenme tartışması bu yüzden geçerliliğini korur.

İkinci ve üçüncü satırlar önceki dersin açık bıraktığı noktayı kapatıyor. Okuyucunun işlemi açıkken günlükte 3.532 çerçeve birikmiş ve denetim noktası bunların sıfırını işleyebilmiş. Okuyucu işlemini bitirdikten sonra aynı çağrı 3.532 çerçevenin tamamını işledi. Sebep tektir: o çerçeveler okuyucunun gördüğü sürümleri taşıyor ve okuyucu bitmeden üzerlerine yazılamaz.

Özet

  • Kilit tabanlı denetim tutarlılığı beklemeyle, çok sürümlü denetim ise satırın eski hâlini saklayarak sağlar.
  • Ölçümde geri alma günlüğü kipinde yazar engellendi; yazma öncesi günlük kipinde aynı yazar tamamlandı ve okuyucu işlemi boyunca eski değeri görmeye devam etti.
  • Her sürüm kendisini oluşturan ve silen işlemleri taşır; bir sürüm, oluşturanı kesinleşmiş ve sileni kesinleşmemişse anlık görüntüde görünür.
  • Bir sürüm ancak hiçbir açık anlık görüntü tarafından görülmediğinde kaldırılabilir; tek bir uzun okuma modelde üç sürümün ikisini yerinde tuttu.
  • Çok sürümlülük okuma ile yazmayı ayırır, aynı veriyi değiştiren iki yazmayı ayırmaz; açık bir okuma denetim noktasını da durdurur (3.532 çerçevenin sıfırı işlenebildi).

Sonraki Adım

Eski sürümlerin kaldırılabilir hâle gelmesi, kendiliğinden kaldırıldıkları anlamına gelmez. Kaldırma işini yapan ayrı bir süreç vardır ve bu süreç yetişemezse tablo, satır sayısı hiç artmasa bile büyümeye devam eder: her güncelleme yeni bir sürüm bırakır, eskisi yerinde durur. Sonraki ders bu birikmeyi ele alır: ölü satırların ölçülmesi, boşluğun geri kazanılması, geri kazanılan alanın neden her zaman işletim sistemine dönmediği ve temizlik süreci ile şişme oranı arasındaki denge.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat