Ders 07 / 21
İşlem Sınırları
İş biriminin sınırının nereye çizileceği: sınırsız yazmaların bıraktığı yarım durum, depo başına işlem açmanın bileşememesi, işlem sınırını tek yerde tutan iş birimi, iç içe sınırlarda kaydetme noktası ve sınır genişliğinin bekleme süresine etkisi.
İçindekiler
Göç yürütücüsü her adımı BEGIN ile açtı, COMMIT ile kapattı ve hata durumunda
ROLLBACK çağırdı. Adımın kendisiyle sürüm kaydının birlikte yazılmasını sağlayan buydu.
Aynı soru uygulamanın normal çalışması için de geçerlidir. Bir ödünç verme isteği tek bir yazma değildir: ödünç kaydı eklenir, kitabın durumu değişir, hareket kütüğüne satır yazılır. Bunların hepsi birlikte kesinleşmeli midir, sınır nereye çizilir ve sınırı kim açar? İşlem kavramı İlişkisel Kuram kursunda tanımlandı; bu ders sınırın uygulama katmanındaki yerini kurar.
Üç Yazmalı Bir İş
Aşağıdaki şema kitabın durumunu ve hareket kütüğünü ekler. Hareket türü bir denetim koşuluyla sınırlıdır; ders boyunca hatayı bu kısıt üretecek.
# kur.sh — dersin veritabanini sifirdan kurar rm -f kutuphane.db sqlite3 kutuphane.db >/dev/null <<'SQL' PRAGMA journal_mode = WAL; CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, durumu TEXT NOT NULL DEFAULT 'rafta' CHECK (durumu IN ('rafta','odunc'))); CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id), uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT); CREATE TABLE hareket (hareket_id INTEGER PRIMARY KEY, odunc_id INTEGER NOT NULL, tur TEXT NOT NULL CHECK (tur IN ('verildi','iade')), tarih TEXT NOT NULL); INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta'),(3,'Kum Kitabı','rafta'); SQL
Durumu her adımda görebilmek için küçük bir okuyucu gerekir.
// durum.mjs — uc bagintideki satir sayilarini ve kitap durumunu yazdirir import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const say = (t) => db.prepare(`SELECT count(*) AS n FROM ${t}`).get().n; console.log(`odunc=${say("odunc")} hareket=${say("hareket")} kitap1_durumu=${ db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get().durumu}`);
Sınır Olmadan
İlk sürüm üç yazmayı arka arkaya çalıştırır; aralarında hiçbir sınır yoktur.
// islem-yok.mjs — uc yazma, islem siniri yok import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const tur = process.argv[2] ?? "verildi"; const sonuc = db.prepare( "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(1, 4, "2025-07-20"); const oduncId = Number(sonuc.lastInsertRowid); db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(1); try { db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)") .run(oduncId, tur, "2025-07-20"); console.log("uc yazma da tamamlandi"); } catch (h) { console.log("ucuncu yazma basarisiz:", h.message); }
Üçüncü yazmaya geçersiz bir tür verilerek hata üretiliyor.
sh kur.sh node islem-yok.mjs "gecikme" node durum.mjs
ucuncu yazma basarisiz: CHECK constraint failed: tur IN ('verildi','iade')
odunc=1 hareket=0 kitap1_durumu=odunc
Ortaya çıkan durum tutarsızdır. Ödünç kaydı var, kitap ödünçte görünüyor, ama hareket kütüğünde karşılığı yok. Uygulama açısından bu, “verilmiş ama verildiği kaydedilmemiş” bir kitaptır; hiçbir raporda görünmez, iade akışında hangi kuralın uygulanacağı belirsizdir.
Sınırın İçinde
Aynı üç yazma bir işlem sınırının içine alındığında sonuç değişir.
// islemli.mjs — ayni uc yazma, tek islem sinirinin icinde import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const tur = process.argv[2] ?? "verildi"; db.exec("BEGIN"); try { const sonuc = db.prepare( "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(1, 4, "2025-07-20"); const oduncId = Number(sonuc.lastInsertRowid); db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(1); db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)") .run(oduncId, tur, "2025-07-20"); db.exec("COMMIT"); console.log("islem kesinlestirildi"); } catch (h) { db.exec("ROLLBACK"); console.log("islem geri alindi:", h.message); }
sh kur.sh
node islemli.mjs "gecikme"
node durum.mjs
node islemli.mjs verildi
node durum.mjs
islem geri alindi: CHECK constraint failed: tur IN ('verildi','iade')
odunc=0 hareket=0 kitap1_durumu=rafta
islem kesinlestirildi
odunc=1 hareket=1 kitap1_durumu=odunc
İlk çağrıda hiçbir yazma kalmadı, ikinci çağrıda üçü birden kaldı. Aradaki durum yok. Bu, iş biriminin (unit of work) tanımıdır: dışarıdan tek bir değişiklik gibi görünen, ya tamamı ya hiçbiri uygulanan yazma kümesi.
İş biriminin sınırı iş kuralının sınırıdır, teknik bir sınır değil. “Ödünç verme” bir iş birimidir çünkü yarısı anlamsızdır. “Gün sonu raporu hazırlama” ile “ödünç verme” ayrı iş birimleridir, çünkü birinin başarısızlığı ötekini geçersiz kılmaz.
Sınır Depoya Çizilirse
Sınırın nereye çizilmeyeceği, çizildiğinde ne olduğuna bakılarak görülür. Her depo yöntemi kendi işlemini açıp kapatırsa iki yöntem birlikte atomik olamaz.
// kendi-islemi.mjs — her depo yontemi kendi islemini acip kapatirsa ne olur import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const oduncDeposu = { ekle(kitapId, uyeId, tarih) { db.exec("BEGIN"); const s = db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)") .run(kitapId, uyeId, tarih); db.exec("COMMIT"); return Number(s.lastInsertRowid); }, }; const hareketDeposu = { ekle(oduncId, tur, tarih) { db.exec("BEGIN"); try { db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)") .run(oduncId, tur, tarih); db.exec("COMMIT"); } catch (h) { db.exec("ROLLBACK"); throw h; } }, }; try { const id = oduncDeposu.ekle(1, 4, "2025-07-20"); hareketDeposu.ekle(id, "gecikme", "2025-07-20"); } catch (h) { console.log("servis hata aldi:", h.message); } console.log("dis islem acmayi dene:"); db.exec("BEGIN"); try { oduncDeposu.ekle(2, 5, "2025-07-21"); } catch (h) { console.log(" ->", h.message); db.exec("ROLLBACK"); }
sh kur.sh
node kendi-islemi.mjs
node durum.mjs
servis hata aldi: CHECK constraint failed: tur IN ('verildi','iade')
dis islem acmayi dene:
-> cannot start a transaction within a transaction
odunc=1 hareket=0 kitap1_durumu=rafta
İki ayrı sorun aynı çıktıda görünüyor. Birincisi: ödünç kaydı kesinleşmiş, hareket yazılamamış; servis hata almış olsa bile veritabanında yarım bir durum kalmış. İkincisi: servis kendisi bir işlem açarak sorunu çözmeye çalıştığında depo yöntemi içeriden ikinci bir işlem açamamış ve hata vermiş.
Sonuç kuraldır: işlemi depo açmaz. Depo, verilen bağlantı üzerinde çalışır; sınırın nerede başlayıp bittiğine bir üst katman karar verir.
İş Birimi
Sınırı taşıyan yapı, depoların kullanacağı bağlantıyı da veren yapıdır.
// is-birimi.mjs — islem sinirini tek yerde tutan is birimi; depolar baglantiyi disaridan alir export class IsBirimi { constructor(db) { this.db = db; this.derinlik = 0; } calistir(is) { const icIse = this.derinlik > 0; const nokta = `nokta_${this.derinlik}`; this.db.exec(icIse ? `SAVEPOINT ${nokta}` : "BEGIN"); this.derinlik += 1; try { const sonuc = is(this.db); this.db.exec(icIse ? `RELEASE ${nokta}` : "COMMIT"); return sonuc; } catch (h) { this.db.exec(icIse ? `ROLLBACK TO ${nokta}` : "ROLLBACK"); throw h; } finally { this.derinlik -= 1; } } } export const oduncDeposu = { ekle(db, kitapId, uyeId, tarih) { return Number(db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)") .run(kitapId, uyeId, tarih).lastInsertRowid); }, }; export const kitapDeposu = { oduncIsaretle(db, kitapId) { db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ?").run(kitapId); }, }; export const hareketDeposu = { ekle(db, oduncId, tur, tarih) { db.prepare("INSERT INTO hareket (odunc_id, tur, tarih) VALUES (?,?,?)") .run(oduncId, tur, tarih); }, };
Derinlik sayacı iç içe çağrıları ayırır. En dıştaki çağrı gerçek işlemi açar; içerideki çağrılar İleri SQL kursunda tanıtılan kaydetme noktası kurar. Böylece iç bir birimin başarısızlığı yalnız kendi yazmalarını geri alır, dış birimi düşürmez.
// servis.mjs — is birimi uc depo cagrisini tek sinirda toplar import { DatabaseSync } from "node:sqlite"; import { IsBirimi, oduncDeposu, kitapDeposu, hareketDeposu } from "./is-birimi.mjs"; const isBirimi = new IsBirimi(new DatabaseSync("kutuphane.db")); function oduncVer(kitapId, uyeId, tarih, tur) { return isBirimi.calistir((db) => { const oduncId = oduncDeposu.ekle(db, kitapId, uyeId, tarih); kitapDeposu.oduncIsaretle(db, kitapId); hareketDeposu.ekle(db, oduncId, tur, tarih); return oduncId; }); } for (const [kitapId, tur] of [[1, "gecikme"], [2, "verildi"]]) { try { console.log(`kitap ${kitapId} verildi, odunc_id =`, oduncVer(kitapId, 4, "2025-07-20", tur)); } catch (h) { console.log(`kitap ${kitapId} basarisiz:`, h.message); } }
sh kur.sh node servis.mjs node durum.mjs sqlite3 -header -column kutuphane.db "SELECT kitap_id, durumu FROM kitap;"
kitap 1 basarisiz: CHECK constraint failed: tur IN ('verildi','iade')
kitap 2 verildi, odunc_id = 1
odunc=1 hareket=1 kitap1_durumu=rafta
kitap_id durumu
-------- ------
1 rafta
2 odunc
3 rafta
Birinci istek tamamen geri alındı: kitap 1 rafta kaldı. İkinci istek üç yazmayı birlikte
kesinleştirdi. Servis kodunda BEGIN geçmiyor; sınır calistir çağrısının kendisidir.
İç içe sınırın davranışı ayrıca görülebilir.
// ic-ice.mjs — ic is birimi basarisiz olsa da dis is birimi kesinlesir import { DatabaseSync } from "node:sqlite"; import { IsBirimi, oduncDeposu, kitapDeposu, hareketDeposu } from "./is-birimi.mjs"; const isBirimi = new IsBirimi(new DatabaseSync("kutuphane.db")); isBirimi.calistir((db) => { const oduncId = oduncDeposu.ekle(db, 3, 6, "2025-07-22"); kitapDeposu.oduncIsaretle(db, 3); hareketDeposu.ekle(db, oduncId, "verildi", "2025-07-22"); try { isBirimi.calistir((ic) => hareketDeposu.ekle(ic, oduncId, "bildirim", "2025-07-22")); } catch (h) { console.log("ic is birimi geri alindi:", h.message); } console.log("dis is birimi devam ediyor, odunc_id =", oduncId); });
sh kur.sh
node ic-ice.mjs
node durum.mjs
ic is birimi geri alindi: CHECK constraint failed: tur IN ('verildi','iade')
dis is birimi devam ediyor, odunc_id = 1
odunc=1 hareket=1 kitap1_durumu=rafta
İç birimin yazması geri alındı, dıştaki üç yazma kesinleşti. Hareket sayısı bir; iki olsaydı iç birim geri alınmamış olurdu.
Sınırın Genişliği
Sınırı geniş çizmek atomikliği artırır, ama bedeli vardır: işlem açık kaldığı sürece tuttuğu kilitler serbest kalmaz. Bedel ölçülebilir.
// uzun-islem.mjs — yazma islemini verilen sure boyunca acik tutar import { DatabaseSync } from "node:sqlite"; import { writeFileSync, rmSync } from "node:fs"; const db = new DatabaseSync("kutuphane.db"); const sureMs = Number(process.argv[2]); db.exec("BEGIN IMMEDIATE"); db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run(); writeFileSync("hazir", ""); // B artik deneyebilir const bitis = Date.now() + sureMs; while (Date.now() < bitis) { /* dis cagri beklemesinin modeli */ } db.exec("COMMIT"); rmSync("hazir"); console.log(`A: islem ${sureMs} ms acik kaldi ve kesinlestirildi`);
// bekleyen-yazma.mjs — ayni satira yazmak isteyen ikinci baglantinin bekleme suresi import { DatabaseSync } from "node:sqlite"; import { existsSync } from "node:fs"; while (!existsSync("hazir")) { /* A'nin islemi acmasini bekle */ } const db = new DatabaseSync("kutuphane.db"); db.exec("PRAGMA busy_timeout = 10000"); const t = performance.now(); db.exec("BEGIN IMMEDIATE"); db.prepare("UPDATE kitap SET durumu = 'rafta' WHERE kitap_id = 1").run(); db.exec("COMMIT"); console.log(`B: yazma icin ${(performance.now() - t).toFixed(0)} ms bekledi`);
# sinir-genisligi.sh — islem ne kadar acik kalirsa ikinci yazan o kadar bekler for sure in 100 400 800; do sh kur.sh >/dev/null rm -f hazir node bekleyen-yazma.mjs & bekleyen=$! node uzun-islem.mjs $sure wait $bekleyen done
sh sinir-genisligi.sh
A: islem 100 ms acik kaldi ve kesinlestirildi B: yazma icin 114 ms bekledi A: islem 400 ms acik kaldi ve kesinlestirildi B: yazma icin 403 ms bekledi A: islem 800 ms acik kaldi ve kesinlestirildi B: yazma icin 844 ms bekledi
İkinci yazanın beklemesi, birinci işlemin açık kalma süresini izliyor. Süreler makineye
bağlıdır; değişmeyen, ilişkinin doğrusal olmasıdır. uzun-islem.mjs içindeki döngü bir
modeldir ve dışarıya yapılan bir çağrının — bildirim gönderme, ödeme servisini arama, dosya
yazma — işlem içinde beklenmesini temsil eder.
Buradan iki kural çıkar. İşlem sınırı içinde dış çağrı yapılmaz; dış çağrı ya sınırdan önce ya sonra yürütülür. Ve sınır, iş kuralının gerektirdiğinden bir yazma bile geniş tutulmaz.
Özet
- Üç yazmadan biri başarısız olduğunda sınırsız sürüm yarım bir durum bıraktı: ödünç kaydı ve kitap durumu değişti, hareket kütüğü boş kaldı.
- Aynı yazmalar tek bir işlem sınırına alındığında sonuç ya tamamı ya hiçbiri oldu; iş birimi bu kümenin adıdır ve sınırı iş kuralı belirler.
- Depo başına işlem açmak iki sorun üretti: çağrılar bileşemedi ve dıştan açılan işlemle çakıştı. İşlemi depo açmaz, üst katman açar.
- İş birimi en dışta gerçek işlemi, içeride kaydetme noktası kurar; iç birimin başarısızlığı dış birimi düşürmedi.
- İşlem açık kaldığı sürece ikinci yazan bekler: 100, 400 ve 800 ms açık kalan işlemler sırasıyla 114, 403 ve 844 ms bekleme üretti. Dış çağrı işlem sınırının içine alınmaz.
Sonraki Adım
Bu derste iki bağlantı aynı satıra yazmak istedi ve biri diğerini bekledi. Yazma–yazma çakışması en görünür durumdur; okuma tarafında olanlar daha sessizdir. Bir işlem sürerken başka bir işlem aynı satırları okursa ne görür: değişikliğin öncesini mi, sonrasını mı, yoksa duruma göre ikisini birden mi? Yanıt yalıtım düzeyine bağlıdır ve seçilen düzey uygulamanın gördüğü veriyi değiştirir. Sonraki ders aynı sorguyu iki farklı düzeyde çalıştırarak tekrarlanamayan okumanın ve hayalet satırın uygulamadan nasıl göründüğünü ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.