---
title: 'Çok Sürümlü Eşzamanlılık Denetimi'
source: 'https://academia.sh/tr/kurslar/veritabani-yonetimi/cok-surumlu-eszamanlilik-denetimi'
course: 'İlişkisel Veritabanı Yönetimi'
language: tr
updated: '2026-08-17T18:08:56+00:00'
license: 'CC BY-SA 4.0'
---

# Ç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.

Ö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.

```sh
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.

```sh
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ı.

```sh
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.
