İçeriğe geç
academia.sh

Ders 09 / 21

İyimser ve Kötümser Kilitleme

Eşzamanlı güncelleme çatışmasının iki çözümü: sürüm sütunuyla çatışmayı yazma anında saptayan iyimser kilitleme, kararı kilit altında veren kötümser kilitleme, yükseltme çakışmasının beklemeyle çözülememesi ve iki yaklaşımın deneme sayısı ile süre karşılaştırması.

İçindekiler

Yalıtım, okunan verinin kendi içinde tutarlı olmasını sağlar. Okunan değere dayanarak yapılan yazmanın doğruluğunu garanti etmez.

İki üye aynı anda son kalan kopyayı ödünç almak isterse ikisi de “kitap rafta” görür. İkisinin gördüğü doğrudur; okudukları anda kitap gerçekten raftaydı. Sorun karar ile yazma arasındaki aralıkta ortaya çıkar. Bu ders o aralığı kapatmanın iki yolunu kurar, çatışmayı gerçekten üretir ve ikisinin maliyetini ölçer.

Kayıp Güncelleme

Şema tek bir kitap ve bir sürüm sütunu içeriyor. Sürüm sütunu ilk bölümde kullanılmayacak; ikinci bölümde devreye girecek.

# kur.sh — tek kitapli yaris duzenegi
rm -f kutuphane.db kutuphane.db-wal kutuphane.db-shm
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 CHECK (durumu IN ('rafta','odunc')),
                    surum INTEGER NOT NULL DEFAULT 1);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, iade_tarihi TEXT);
INSERT INTO kitap VALUES (1,'Körlük','rafta',1);
SQL
// say.mjs — bir kitap icin acik odunc kaydi sayisini ve surumu yazdirir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const k = db.prepare("SELECT durumu, surum FROM kitap WHERE kitap_id = 1").get();
const n = db.prepare("SELECT count(*) AS n FROM odunc WHERE kitap_id = 1 AND iade_tarihi IS NULL").get().n;
console.log(`kitap durumu=${k.durumu} surum=${k.surum}  acik odunc kaydi=${n}`);

İlk sürüm okur, karar verir, yazar. Yazma işlemi bir sınırın içindedir; önceki dersin kuralına uyuyor. Yine de yetmez.

// korumasiz.mjs — oku, karar ver, yaz; arada koruma yok
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 5000");
const uyeId = Number(process.argv[2]);
const gecikmeMs = Number(process.argv[3] ?? 120);

const kitap = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get();
const bitis = Date.now() + gecikmeMs;
while (Date.now() < bitis) { /* karar verme suresinin modeli */ }

if (kitap.durumu === "rafta") {
  db.exec("BEGIN IMMEDIATE");
  db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run();
  db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (1,?,NULL)").run(uyeId);
  db.exec("COMMIT");
  console.log(`uye ${uyeId}: kitabi aldi`);
} else {
  console.log(`uye ${uyeId}: kitap oduncte, reddedildi`);
}
sh kur.sh
node korumasiz.mjs 4 & node korumasiz.mjs 5 & wait
node say.mjs
uye 4: kitabi aldi
uye 5: kitabi aldi
kitap durumu=odunc surum=1  acik odunc kaydi=2

Tek nüshalık kitap iki üyeye birden verildi. Bu, İlişkisel Kuram kursunda tanımlanan kayıp güncellemenin uygulamadaki hâlidir: ikinci yazma, birinci yazmanın dayandığı gerçeği geçersiz kıldığı hâlde geçti. Okuma ile yazmanın ayrı ayrı doğru olması yetmiyor; ikisinin arasındaki varsayımın hâlâ geçerli olduğunun sınanması gerekiyor.

gecikmeMs döngüsü bir modeldir; kararın anlık olmadığını, kural denetimi ve dış doğrulamalar için zaman geçtiğini temsil eder. Aralık daralınca çatışma seyrekleşir, ama yok olmaz.

İyimser Kilitleme

Birinci çözüm çatışmayı önlemeye çalışmaz; olduğunda saptar. Kayıt, okunduğu andaki sürümüyle birlikte alınır ve yazma koşuluna o sürüm konur. Sürüm değişmişse yazma hiçbir satırı etkilemez.

// iyimser.mjs — okunan surum yazma kosuluna konur; etkilenen satir sayisi cakismayi soyler
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 5000");
const uyeId = Number(process.argv[2]);
const gecikmeMs = Number(process.argv[3] ?? 120);

const kitap = db.prepare("SELECT durumu, surum FROM kitap WHERE kitap_id = 1").get();
const bitis = Date.now() + gecikmeMs;
while (Date.now() < bitis) { /* karar verme suresinin modeli */ }

if (kitap.durumu !== "rafta") {
  console.log(`uye ${uyeId}: kitap oduncte, reddedildi`);
} else {
  db.exec("BEGIN IMMEDIATE");
  const sonuc = db.prepare(
    "UPDATE kitap SET durumu = 'odunc', surum = surum + 1 WHERE kitap_id = 1 AND surum = ?")
    .run(kitap.surum);
  if (sonuc.changes === 0) {
    db.exec("ROLLBACK");
    console.log(`uye ${uyeId}: cakisma (okunan surum ${kitap.surum}, etkilenen satir 0)`);
  } else {
    db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (1,?,NULL)").run(uyeId);
    db.exec("COMMIT");
    console.log(`uye ${uyeId}: kitabi aldi (surum ${kitap.surum} -> ${kitap.surum + 1})`);
  }
}
sh kur.sh
node iyimser.mjs 4 & node iyimser.mjs 5 & wait
node say.mjs
uye 5: kitabi aldi (surum 1 -> 2)
uye 4: cakisma (okunan surum 1, etkilenen satir 0)
kitap durumu=odunc surum=2  acik odunc kaydi=1

Hangi üyenin kazandığı çalıştırmadan çalıştırmaya değişir; değişmeyen, birinin kazanıp ötekinin çatışma bildirmesidir. Açık ödünç kaydı bire indi.

Yöntemin çalışma ilkesi tek bir sayıdadır: etkilenen satır sayısı. Bir tane olması kaydın okunduğu hâlde bulunduğunu, sıfır olması araya birinin girdiğini söyler. Bu sayı sürücüden okunabildiği için hiçbir ek altyapı gerekmez.

Sürüm sütununun yerine son değişiklik zaman damgası da kullanılabilir, ama zaman damgasının çözünürlüğü yetmezse iki değişiklik aynı değeri taşır ve çatışma kaçar. Tek yönde artan bir tam sayı bu riski taşımaz.

Çatışma saptandığında ne yapılacağı ayrı bir karardır. Kullanıcıya “kayıt değişti” demek, işlemi baştan okuyup yeniden denemek ya da iki değişikliği birleştirmek seçenekleri vardır. Ödünç örneğinde doğru davranış yeniden denemektir; ikinci denemede kitap ödünçte görüneceği için istek düzgün biçimde reddedilir.

Kötümser Kilitleme

İkinci çözüm çatışmayı hiç oluşturmaz. Karar verilmeden önce kayıt kilitlenir; ikinci istemci ya bekler ya reddedilir.

// kotumser.mjs — karar suresi boyunca yazma kilidi tutulur
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
db.exec(`PRAGMA busy_timeout = ${process.env.BEKLEME_MS ?? 0}`);
const uyeId = Number(process.argv[2]);
const gecikmeMs = Number(process.argv[3] ?? 120);

try {
  db.exec("BEGIN IMMEDIATE");
} catch (h) {
  console.log(`uye ${uyeId}: kilit alinamadi -> ${h.message}`);
  process.exit(0);
}
const kitap = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get();
const bitis = Date.now() + gecikmeMs;
while (Date.now() < bitis) { /* karar verme suresinin modeli */ }

if (kitap.durumu === "rafta") {
  db.prepare("UPDATE kitap SET durumu = 'odunc', surum = surum + 1 WHERE kitap_id = 1").run();
  db.prepare("INSERT INTO odunc (kitap_id, uye_id, iade_tarihi) VALUES (1,?,NULL)").run(uyeId);
  db.exec("COMMIT");
  console.log(`uye ${uyeId}: kitabi aldi`);
} else {
  db.exec("COMMIT");
  console.log(`uye ${uyeId}: kitap oduncte, reddedildi`);
}

Bekleme süresi sıfırken ikinci istemci kilidi hiç beklemez.

sh kur.sh
node kotumser.mjs 4 & sleep 0.05; node kotumser.mjs 5 & wait
node say.mjs
uye 5: kilit alinamadi -> database is locked
uye 4: kitabi aldi
kitap durumu=odunc surum=2  acik odunc kaydi=1

Hata metni sürücünün verdiği metindir ve motora göre değişir; ortak olan, kilidin alınamadığını söyleyen ayrı bir hata sınıfının bulunmasıdır. Uygulama bu hatayı öteki hatalardan ayırmak zorundadır: kilit hatası yeniden denenebilir, kısıt ihlali denenemez.

Bekleme süresi verildiğinde ikinci istemci kilidi bekler ve sırası geldiğinde güncel veriyi okur.

sh kur.sh
BEKLEME_MS=5000 node kotumser.mjs 4 & sleep 0.05; BEKLEME_MS=5000 node kotumser.mjs 5 & wait
node say.mjs
uye 4: kitabi aldi
uye 5: kitap oduncte, reddedildi
kitap durumu=odunc surum=2  acik odunc kaydi=1

İkinci üye “çatışma” değil “kitap ödünçte” yanıtı aldı. Fark önemlidir: iyimser yaklaşımda uygulama bir çatışmayı ele almak zorundaydı, kötümserde iş kuralının normal reddi devreye girdi. Bedeli, ikinci üyenin karar süresi boyunca beklemesidir.

Yükseltme Çakışması

Kötümser kilitlemenin doğru kurulması, kilidin okumadan önce alınmasına bağlıdır. Önce okuyup sonra yazmaya çalışan iki işlem, bekleyerek çözülemeyen bir çakışma üretir.

// yukseltme.mjs — once okuyup sonra yazmaya calisan iki islem
import { DatabaseSync } from "node:sqlite";
import { writeFileSync, existsSync } from "node:fs";
const rol = process.argv[2];
const db = new DatabaseSync("kutuphane.db");
db.exec("PRAGMA busy_timeout = 3000");

db.exec("BEGIN");                                  // ertelenmis: okumada anlik goruntu alinir
db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get();
if (rol === "a") { writeFileSync("a_okudu", ""); while (!existsSync("b_okudu")) { /* bekle */ } }
else { writeFileSync("b_okudu", ""); while (!existsSync("a_okudu")) { /* bekle */ } }

if (rol === "b") { while (!existsSync("a_yazdi")) { /* A once yazsin */ } }
try {
  db.prepare("UPDATE kitap SET surum = surum + 1 WHERE kitap_id = 1").run();
  db.exec("COMMIT");
  if (rol === "a") writeFileSync("a_yazdi", "");
  console.log(`${rol}: yazdi ve kesinlestirdi`);
} catch (h) {
  db.exec("ROLLBACK");
  console.log(`${rol}: yukseltme basarisiz -> ${h.message}`);
}
sh kur.sh
rm -f a_okudu b_okudu a_yazdi
node yukseltme.mjs a & node yukseltme.mjs b & wait
node say.mjs
b: yukseltme basarisiz -> database is locked
a: yazdi ve kesinlestirdi
kitap durumu=rafta surum=2  acik odunc kaydi=0

B’nin bekleme süresi üç saniyeydi; buna rağmen beklemeden hata aldı. Nedeni şudur: B’nin işlemi bir anlık görüntü üzerinde açıldı ve o görüntü artık eskimiş durumda. Beklemek görüntüyü tazelemez, çünkü işlem boyunca sabit kalması gerekir. Tek çözüm işlemi geri alıp baştan başlamaktır.

İleri SQL kursunda yükseltme çakışması olarak geçen durum budur. Uygulama tarafındaki kuralı verir: yazma niyeti varsa kilit okumadan önce alınır (BEGIN IMMEDIATE gibi bir yazma niyeti bildirimiyle), yoksa çatışma iyimser yöntemle ele alınır. İkisinin arasında kalan üçüncü bir seçenek yoktur.

Maliyetin Ölçülmesi

İki yaklaşımın farkı çekişme altında görünür. Aşağıdaki düzenek dört işçiyle tek bir sayacı artırır; her artırma bir okuma, üç milisaniyelik bir karar süresi ve bir yazmadır.

# sayac-kur.sh — tek satirli sayac
rm -f sayac.db sayac.db-wal sayac.db-shm
sqlite3 sayac.db >/dev/null <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE sayac (ad TEXT PRIMARY KEY, deger INTEGER NOT NULL, surum INTEGER NOT NULL);
INSERT INTO sayac VALUES ('odunc_sayisi', 0, 1);
SQL
// sayac-iyimser.mjs — oku, karar ver, yaz; cakismada yeniden dene
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("sayac.db");
db.exec("PRAGMA busy_timeout = 10000");
const adet = Number(process.argv[2]);
const dusunmeMs = Number(process.argv[3]);
const dusun = () => { const b = Date.now() + dusunmeMs; while (Date.now() < b) { /* karar suresi */ } };

let deneme = 0;
for (let i = 0; i < adet; i++) {
  for (;;) {
    deneme += 1;
    const s = db.prepare("SELECT deger, surum FROM sayac WHERE ad = 'odunc_sayisi'").get();
    dusun();
    db.exec("BEGIN IMMEDIATE");
    const sonuc = db.prepare(
      "UPDATE sayac SET deger = ?, surum = surum + 1 WHERE ad = 'odunc_sayisi' AND surum = ?")
      .run(s.deger + 1, s.surum);
    db.exec(sonuc.changes === 0 ? "ROLLBACK" : "COMMIT");
    if (sonuc.changes === 1) break;
  }
}
console.log(`iyimser isci: ${adet} artirma, ${deneme} deneme`);
// sayac-kotumser.mjs — kilit once alinir, karar kilit altinda verilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("sayac.db");
db.exec("PRAGMA busy_timeout = 10000");
const adet = Number(process.argv[2]);
const dusunmeMs = Number(process.argv[3]);
const dusun = () => { const b = Date.now() + dusunmeMs; while (Date.now() < b) { /* karar suresi */ } };

for (let i = 0; i < adet; i++) {
  db.exec("BEGIN IMMEDIATE");
  const s = db.prepare("SELECT deger FROM sayac WHERE ad = 'odunc_sayisi'").get();
  dusun();
  db.prepare("UPDATE sayac SET deger = ?, surum = surum + 1 WHERE ad = 'odunc_sayisi'")
    .run(s.deger + 1);
  db.exec("COMMIT");
}
console.log(`kotumser isci: ${adet} artirma, ${adet} deneme`);
# sayac-olc.sh — dort isci, isci basina 40 artirma, 3 ms karar suresi
for yaklasim in iyimser kotumser; do
  sh sayac-kur.sh
  baslangic=$(date +%s%N)
  isciler=""
  for i in 1 2 3 4; do node sayac-$yaklasim.mjs 40 3 & isciler="$isciler $!"; done
  wait $isciler
  bitis=$(date +%s%N)
  printf '%s: toplam %s ms, son deger = %s\n\n' "$yaklasim" $(( (bitis-baslangic)/1000000 )) \
    "$(sqlite3 sayac.db 'SELECT deger FROM sayac;')"
done
sh sayac-olc.sh
iyimser isci: 40 artirma, 127 deneme
iyimser isci: 40 artirma, 143 deneme
iyimser isci: 40 artirma, 145 deneme
iyimser isci: 40 artirma, 150 deneme
iyimser: toplam 508 ms, son deger = 160

kotumser isci: 40 artirma, 40 deneme
kotumser isci: 40 artirma, 40 deneme
kotumser isci: 40 artirma, 40 deneme
kotumser isci: 40 artirma, 40 deneme
kotumser: toplam 663 ms, son deger = 160

Son değer iki yaklaşımda da 160; ikisi de doğru. Süreler ve deneme sayıları makineye ve zamanlamaya göre değişir, ilişki değişmez. İyimser yaklaşım her kırk artırma için yüz yirmiden çok deneme harcadı; işin dörtte üçü boşa gitti. Buna karşılık işçiler karar sürelerini paralel geçirdiği için toplam süre daha kısa çıktı. Kötümser yaklaşımda hiç boşa deneme yok, ama karar süreleri sıraya girdi.

Seçim kuralı buradan çıkar. Çatışma seyrekse iyimser yaklaşım ucuzdur; boşa giden deneme sayısı düşük kalır ve hiç kimse beklemez. Çatışma sıksa iyimser yaklaşım aynı işi defalarca yapar; kötümser yaklaşım sırayı baştan kurarak bu israfı keser. Karar süresi uzunsa kötümser yaklaşımın bedeli hızla büyür, çünkü kilit o süre boyunca tutulur.

Özet

  • Korumasız oku–karar ver–yaz akışı tek nüshalık kitabı iki üyeye verdi; kayıp güncelleme yazmanın işlem içinde olmasına rağmen oluştu.
  • İyimser kilitleme okunan sürümü yazma koşuluna koyar; etkilenen satır sayısının sıfır olması çatışmanın işaretidir ve çatışan taraf yeniden dener.
  • Kötümser kilitleme kararı kilit altında verir; bekleme süresi sıfırken kilit hatası, bekleme verildiğinde güncel veriyle normal ret üretti.
  • Önce okuyup sonra yazmaya çalışan işlem yükseltme çakışması aldı; bu hata beklemekle çözülmez, işlem baştan başlatılır.
  • Ölçümde iyimser yaklaşım kırk artırma için yüz yirmiden çok deneme harcadı ama daha kısa sürdü; seçim çatışma sıklığına ve karar süresine bağlıdır.

Sonraki Adım

Bu 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. Ödünç servisi tek başına değilse durum değişir. Kitap kataloğu bir serviste, üye kayıtları başka bir serviste, ceza tahsilatı üçüncü bir serviste duruyorsa, “ödünç ver” işlemi üç ayrı veritabanına dokunur ve hiçbiri ötekinin işlemini göremez. Sonraki ders atomikliğin servis sınırını geçemediğini gösterir, kaybın ne anlama geldiğini modelleyerek ölçer ve telafi ile giden kutusu düzenlerini kurar.

İ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