---
title: 'Dağıtık İşlem Sorunu'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani/dagitik-islem-sorunu'
course: 'Veri Erişim Katmanı ve İş Mantığı'
language: tr
updated: '2026-08-17T18:06:51+00:00'
license: 'CC BY-SA 4.0'
---

# Dağıtık İşlem Sorunu

Atomikliğin servis sınırını geçememesi: iki ayrı veritabanına yapılan yazmaların yarım kalması, telafi adımlarıyla geri sarma, telafinin de başarısız olabilmesi ve durum değişikliğiyle iletinin aynı işlemde yazıldığı giden kutusu düzeni.

Önceki dersteki iki çözüm de tek bir veritabanının içinde çalıştı. Kilidi de sürüm sayacını
da aynı motor yönetti; `ROLLBACK` çağrısı bütün yazmaları birlikte geri aldı.

Ödünç servisi tek başına değilse bu dayanak kalkar. Kitap kataloğu bir serviste, üye
kayıtları başka bir serviste duruyorsa "ödünç ver" isteği iki ayrı veritabanına dokunur ve
hiçbiri ötekinin işlemini göremez. Bu ders o durumu kurar, kaybı ölçer ve iki düzeni
gösterir.

## Modelin Sınırı

Aşağıdaki düzenek gerçek bir dağıtık sistem değildir. İki servis aynı makinede, iki ayrı
SQLite dosyası üzerinde çalışan iki modüldür; aralarında ağ yoktur, çağrılar süreç içinde
yapılır. **Modelin taşıdığı tek özellik, iki veritabanının ortak bir işlem sınırı
paylaşmamasıdır.** Atomikliğin kaybı bu özellikten doğar; ağ gecikmesi, kısmi hata ve zaman
aşımı gibi gerçek dağıtık sistem sorunları modelde yoktur ve modelden çıkarılamaz.

```sh
# kur.sh — iki servisin ayri veritabanlari
rm -f katalog.db uyelik.db
sqlite3 katalog.db >/dev/null <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    durumu TEXT NOT NULL CHECK (durumu IN ('rafta','odunc')));
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL,
                           kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL,
                           durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta'),(3,'Kum Kitabı','rafta');
SQL
sqlite3 uyelik.db >/dev/null <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL,
                  acik_odunc INTEGER NOT NULL, sinir INTEGER NOT NULL);
INSERT INTO uye VALUES (4,'Emre',2,3),(5,'Selin',3,3),(6,'Burak',0,9);
SQL
```

Her servis kendi işlem sınırını kendi içinde kurar. Dışarıya yalnız iş anlamı taşıyan
çağrılar verir.

```js
// servisler.mjs — iki ayri veritabani, iki ayri servis; ortak islem yok
import { DatabaseSync } from "node:sqlite";

export const katalog = {
  db: new DatabaseSync("katalog.db"),
  oduncIsaretle(kitapId) {
    this.db.exec("BEGIN IMMEDIATE");
    const s = this.db.prepare(
      "UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ? AND durumu = 'rafta'").run(kitapId);
    this.db.exec(s.changes === 1 ? "COMMIT" : "ROLLBACK");
    if (s.changes !== 1) throw new Error("katalog: kitap rafta degil");
  },
  raftaIsaretle(kitapId) {
    if (process.env.KATALOG_ERISILEMEZ === "1") throw new Error("katalog: servise ulasilamiyor");
    this.db.exec("BEGIN IMMEDIATE");
    this.db.prepare("UPDATE kitap SET durumu = 'rafta' WHERE kitap_id = ?").run(kitapId);
    this.db.exec("COMMIT");
  },
  durum(kitapId) {
    return this.db.prepare("SELECT durumu FROM kitap WHERE kitap_id = ?").get(kitapId).durumu;
  },
};

export const uyelik = {
  db: new DatabaseSync("uyelik.db"),
  oduncEkle(uyeId) {
    this.db.exec("BEGIN IMMEDIATE");
    const s = this.db.prepare(
      "UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_id = ? AND acik_odunc < sinir")
      .run(uyeId);
    this.db.exec(s.changes === 1 ? "COMMIT" : "ROLLBACK");
    if (s.changes !== 1) throw new Error("uyelik: odunc siniri asildi");
  },
  sayi(uyeId) {
    return this.db.prepare("SELECT acik_odunc FROM uye WHERE uye_id = ?").get(uyeId).acik_odunc;
  },
};
```

## Atomikliğin Kaybı

En doğrudan yazım iki çağrıyı sırayla yapar.

```js
// telafisiz.mjs — iki servisi sirayla cagirir; ikincisi basarisiz olursa geri donus yok
import { katalog, uyelik } from "./servisler.mjs";
const [kitapId, uyeId] = process.argv.slice(2).map(Number);
try {
  katalog.oduncIsaretle(kitapId);
  uyelik.oduncEkle(uyeId);
  console.log(`odunc verildi: kitap ${kitapId} -> uye ${uyeId}`);
} catch (h) {
  console.log("basarisiz:", h.message);
}
console.log(`  katalog: kitap ${kitapId} durumu = ${katalog.durum(kitapId)}`);
console.log(`  uyelik : uye ${uyeId} acik odunc = ${uyelik.sayi(uyeId)}`);
```

Üye 4'ün iki açık kaydı ve üç sınırı var; isteği geçmeli. Üye 5 sınırında; isteği
reddedilmeli.

```sh
sh kur.sh
node telafisiz.mjs 1 4
echo "---"
node telafisiz.mjs 2 5
```

```
odunc verildi: kitap 1 -> uye 4
  katalog: kitap 1 durumu = odunc
  uyelik : uye 4 acik odunc = 3
---
basarisiz: uyelik: odunc siniri asildi
  katalog: kitap 2 durumu = odunc
  uyelik : uye 5 acik odunc = 3
```

İkinci istek reddedildi, ama katalogdaki kitap ödünçte kaldı. Ortada kimsenin almadığı,
hiçbir üyenin hesabında görünmeyen bir ödünç kitap var. Kimse onu ödünç alamaz; iade de
edilemez, çünkü iade edecek bir kayıt yok.

`try` bloğu bunu düzeltmez. Hata yakalandı, kod hatayı bildirdi; katalogdaki değişiklik
zaten kesinleşmişti. **Atomiklik işlem sınırının içinde geçerlidir; iki ayrı sınır arasında
geçerli değildir.**

Klasik çözüm iki evreli kesinleştirmedir: bir eşgüdümcü bütün katılımcılara "hazır mısın"
diye sorar, hepsi olumlu yanıt verirse "kesinleştir" der. Bedeli, katılımcıların oy ile
karar arasında kilitlerini tutmak zorunda kalmasıdır; eşgüdümcü bu aralıkta erişilemez
duruma gelirse katılımcılar belirsiz durumda bekler. Bu maliyet, servis sınırlarının
ayrıldığı sistemlerde çoğunlukla kabul edilmez ve aşağıdaki iki düzen tercih edilir.

## Telafi Düzeni

Birinci düzen atomikliği taklit eder. Her adımın etkisini geri alan bir **telafi adımı**
tanımlanır; bir adım başarısız olunca daha önce tamamlananlar tersten telafi edilir. Bu
düzene **saga** denir.

```js
// saga.mjs — her adimin bir telafi adimi vardir; hata cikinca tersten yurutulur
import { katalog, uyelik } from "./servisler.mjs";
const [kitapId, uyeId] = process.argv.slice(2).map(Number);

const adimlar = [
  { ad: "katalog.oduncIsaretle", yap: () => katalog.oduncIsaretle(kitapId),
    telafi: () => katalog.raftaIsaretle(kitapId) },
  { ad: "uyelik.oduncEkle", yap: () => uyelik.oduncEkle(uyeId), telafi: () => {} },
];

const yapilan = [];
try {
  for (const a of adimlar) { a.yap(); yapilan.push(a); console.log(`  adim tamam: ${a.ad}`); }
  console.log(`odunc verildi: kitap ${kitapId} -> uye ${uyeId}`);
} catch (h) {
  console.log("  hata:", h.message);
  let eksik = 0;
  for (const a of yapilan.reverse()) {
    try { a.telafi(); console.log(`  telafi edildi: ${a.ad}`); }
    catch (t) { eksik += 1; console.log(`  TELAFI BASARISIZ: ${a.ad} -> ${t.message}`); }
  }
  console.log(eksik === 0 ? "istek geri alindi" : `istek yarim kaldi (${eksik} telafi bekliyor)`);
}
console.log(`  katalog: kitap ${kitapId} durumu = ${katalog.durum(kitapId)}`);
console.log(`  uyelik : uye ${uyeId} acik odunc = ${uyelik.sayi(uyeId)}`);
```

```sh
sh kur.sh
node saga.mjs 2 5
```

```
  adim tamam: katalog.oduncIsaretle
  hata: uyelik: odunc siniri asildi
  telafi edildi: katalog.oduncIsaretle
istek geri alindi
  katalog: kitap 2 durumu = rafta
  uyelik : uye 5 acik odunc = 3
```

Kitap rafa döndü. Dışarıdan bakınca sonuç geri almaya benziyor, ama aynı şey değil. Geri
almada değişiklik hiç görünmez; telafide değişiklik bir süre görünür ve sonra ters bir
değişiklikle kapatılır. Bu aralıkta kitabı sorgulayan biri onu ödünçte görür. Sistem bu
aralıkta tutarsızdır ve tutarlılığa **sonradan** ulaşır.

Telafi adımının kendisi de bir iş kararıdır. "Ödünç işaretini kaldır" telafi edilebilir bir
adımdır. "Üyeye bildirim gönder" edilemez; gönderilmiş bir bildirim geri alınamaz, ancak
ikinci bir bildirimle düzeltilebilir. Bu yüzden telafi edilemeyen adımlar sıralamanın
sonuna konur.

## Telafinin Sınırı

Telafi çağrısı da bir uzak çağrıdır ve başarısız olabilir.

```sh
sh kur.sh
KATALOG_ERISILEMEZ=1 node saga.mjs 2 5
```

```
  adim tamam: katalog.oduncIsaretle
  hata: uyelik: odunc siniri asildi
  TELAFI BASARISIZ: katalog.oduncIsaretle -> katalog: servise ulasilamiyor
istek yarim kaldi (1 telafi bekliyor)
  katalog: kitap 2 durumu = odunc
  uyelik : uye 5 acik odunc = 3
```

Baştaki tutarsız durumun aynısına geri dönüldü. Saga düzeni sorunu kaldırmadı, sorumluluğu
taşıdı: artık sistemde "telafi bekliyor" durumundaki bir istek var ve birinin onu yeniden
denemesi gerekiyor. Bu yüzden telafi adımları kalıcı olarak kaydedilir; süreç çöktüğünde
bile bekleyen telafinin izi kalmalıdır.

## Giden Kutusu

İkinci düzen sorunu başka bir yerden yakalar. Katalog servisi durumu değiştirirken, "bu
oldu" bilgisini de **aynı işlemde** kendi veritabanına yazar. İki yazma tek bir sınırın
içinde olduğu için ya ikisi de olur ya hiçbiri.

```js
// kutuya-yaz.mjs — durum degisikligi ile iletiyi ayni islemde yazar
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("katalog.db");
const [kitapId, uyeId] = process.argv.slice(2).map(Number);

db.exec("BEGIN IMMEDIATE");
const s = db.prepare(
  "UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ? AND durumu = 'rafta'").run(kitapId);
if (s.changes !== 1) { db.exec("ROLLBACK"); console.log("kitap rafta degil"); process.exit(1); }
db.prepare("INSERT INTO giden_kutusu (tur, kitap_id, uye_id) VALUES ('odunc_verildi',?,?)")
  .run(kitapId, uyeId);
db.exec("COMMIT");
console.log(`katalog: kitap ${kitapId} oduncte, ileti kutuya yazildi`);
```

İletiyi hedefe götüren iş ayrı bir süreçtir. Bu düzene **giden kutusu** (outbox) denir.

```js
// aktarici.mjs — kutudaki iletileri hedef servise gonderir; --cokme isaretleme oncesi durur
import { DatabaseSync } from "node:sqlite";
import { uyelik } from "./servisler.mjs";
const cokme = process.argv.includes("--cokme");
const db = new DatabaseSync("katalog.db");

const bekleyenler = db.prepare(
  "SELECT ileti_id, kitap_id, uye_id FROM giden_kutusu WHERE durum = 'bekliyor' ORDER BY ileti_id").all();

for (const i of bekleyenler) {
  uyelik.oduncEkle(i.uye_id);
  console.log(`aktarici: ileti ${i.ileti_id} gonderildi (uye ${i.uye_id})`);
  if (cokme) { console.log("aktarici: isaretleme oncesi coktu"); process.exit(0); }
  db.prepare("UPDATE giden_kutusu SET durum = 'gonderildi' WHERE ileti_id = ?").run(i.ileti_id);
}
console.log(`aktarici: ${bekleyenler.length} ileti islendi`);
```

Kazanç şudur: durum değiştiyse ileti mutlaka vardır. Aktarıcı çökerse ileti kutuda bekler,
aktarıcı yeniden başladığında gönderilir. Bilgi kaybolmaz.

## En Az Bir Kez Teslim

Kazancın bir bedeli var. Aktarıcı iletiyi gönderdikten sonra, kutuyu işaretlemeden önce
çökebilir.

```sh
sh kur.sh
node kutuya-yaz.mjs 3 6
node aktarici.mjs --cokme
echo "-- yeniden calistir --"
node aktarici.mjs
sqlite3 uyelik.db "SELECT uye_id, acik_odunc FROM uye WHERE uye_id = 6;"
sqlite3 katalog.db "SELECT ileti_id, durum FROM giden_kutusu;"
```

```
katalog: kitap 3 oduncte, ileti kutuya yazildi
aktarici: ileti 1 gonderildi (uye 6)
aktarici: isaretleme oncesi coktu
-- yeniden calistir --
aktarici: ileti 1 gonderildi (uye 6)
aktarici: 1 ileti islendi
6|2
1|gonderildi
```

Tek bir ödünç verildi, üyenin açık ödünç sayısı ikiye çıktı. İleti iki kez teslim edildi ve
etkisi iki kez uygulandı.

Bu kaçınılmazdır. Aktarıcının "gönderdim" bilgisini yazması ile hedefin iletiyi işlemesi
iki ayrı veritabanında olur; yani ilk bölümdeki sorunun aynısı, bir katman aşağıda yeniden
karşımızdadır. Sonsuz gerilemeyi kesen tek yol, garantiyi düşürmek ve sonucu düzeltmektir:
teslim **en az bir kez** yapılır, alıcı da aynı iletiyi ikinci kez aldığında ek bir etki
üretmez. Alıcının bu özelliği taşıması, giden kutusu düzeninin karşılanması gereken
koşuludur.

## Özet

- İki ayrı veritabanına yapılan yazmalar ortak bir işlem sınırı paylaşmaz; ikinci çağrı
  başarısız olduğunda birinci kesinleşmiş kaldı ve kimsenin almadığı bir ödünç kitap oluştu.
- İki evreli kesinleştirme atomikliği korur, bedeli katılımcıların karar beklerken kilit
  tutmasıdır.
- Telafi düzeni tamamlanan adımları tersten geri sarar; geri almadan farkı, tutarsız
  durumun bir süre görünür olmasıdır.
- Telafi çağrısı da başarısız olabilir; bu durumda düzen sorunu kaldırmaz, bekleyen bir
  telafi kaydı bırakır ve yeniden denenmesini gerektirir.
- Giden kutusu, durum değişikliğiyle iletiyi aynı işlemde yazarak bilgi kaybını önler;
  bedeli, iletinin en az bir kez — bazen birden çok kez — teslim edilmesidir.

## Sonraki Adım

Giden kutusu düzeninin karşılanmamış tek koşulu kaldı: aynı iletinin ikinci kez işlenmesi
ek bir etki üretmemeli. Bu ölçümde üyenin açık ödünç sayısı ikiye çıktı, yani koşul
sağlanmadı. Aynı sorun ağ üzerinden gelen her yeniden denemede belirir; istemci zaman aşımı
alıp isteği tekrarladığında sunucunun ilk isteği işlemiş olup olmadığını bilmesi gerekir.
Sonraki ders bir işlemin tekrarının neden ek etki üretmediğini tanımlar, doğal olarak bu
özelliği taşıyan yazmalarla taşımayanları ayırır ve taşımayanları tekillik anahtarıyla
güvenli hâle getirir.
