İçeriğe geç
academia.sh

Ders 11 / 21

Etkisiz İşlemler

Yeniden denemenin güvenli kılınması: üç yazma biçiminin tekrar altındaki davranışının ölçülmesi, atama–türetme–çatışma davranışıyla tekrarı etkisiz kılma, tekillik anahtarının yanıtla birlikte aynı işlemde saklanması ve giden kutusu tüketicisinin düzeltilmesi.

İçindekiler

Önceki ders giden kutusu düzenini kurdu ve karşılanmamış bir koşul bıraktı: aynı iletinin ikinci kez işlenmesi ek etki üretmemeliydi. Ölçümde üyenin açık ödünç sayısı bir yerine ikiye çıktı; koşul sağlanmadı.

Aynı sorun her yeniden denemede belirir. İstemci zaman aşımı alıp isteği tekrarladığında, sunucunun ilk isteği işleyip işlemediği belli değildir. Yanıt kaybolmuş olabilir. Bu ders tekrarın ne zaman zararsız olduğunu tanımlar ve zararlı olduğu durumları zararsız hâle getirir.

Bir işlem, ikinci ve sonraki çalıştırmaları ilkinin ötesinde bir etki üretmiyorsa etkisiz işlemdir (idempotent operation). API Tasarımı kursunda aynı özellik HTTP yöntemleri için “değişmez yöntem” adıyla geçti; burada ilgilendiğimiz, o özelliğin veri erişim katmanında nasıl kurulduğudur.

Üç Yazma Biçimi

Ödünç verme üç yazma içerir: kitabın durumunun değişmesi, üyenin açık ödünç sayısının artması, ödünç satırının eklenmesi. Üçü aynı tekrar altında farklı davranır.

# kur.sh — olcum icin bos baslangic durumu
rm -f kutuphane.db
sqlite3 kutuphane.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 uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO kitap VALUES (1,'Körlük','rafta');
INSERT INTO uye VALUES (4,'Emre',0);
SQL
// uc-yazma.mjs — uc yazma bicimi, her biri ardarda uc kez calistirilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const yazmalar = {
  "durum atama   ": () => db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run(),
  "sayac artirma ": () => db.prepare("UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_id = 4").run(),
  "kayit ekleme  ": () => db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (1,4,'2025-07-20')").run(),
};

for (const [ad, yaz] of Object.entries(yazmalar)) {
  for (let i = 0; i < 3; i++) yaz();
}
const k = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get().durumu;
const u = db.prepare("SELECT acik_odunc AS n FROM uye WHERE uye_id = 4").get().n;
const o = db.prepare("SELECT count(*) AS n FROM odunc").get().n;
console.log(`uc kez calistiktan sonra:  kitap.durumu=${k}  uye.acik_odunc=${u}  odunc satiri=${o}`);
sh kur.sh
node uc-yazma.mjs
uc kez calistiktan sonra:  kitap.durumu=odunc  uye.acik_odunc=3  odunc satiri=3

Ayrım net. Atama etkisizdir: durumu = 'odunc' yazması sonucu hedef değere getirir, kaçıncı kez çalıştığı önemli değildir. Artırma etkisiz değildir, çünkü sonuç önceki değere bağlıdır. Ekleme etkisiz değildir, çünkü her çağrı yeni bir satır üretir.

Kural buradan çıkarılabilir: bir yazma, sonucu yalnız hedef duruma bağlıysa etkisizdir; sonucu kendi çalıştırılma sayısına bağlıysa değildir.

Etkisiz Biçime Çevirme

İki sorunlu yazma da biçim değiştirerek düzeltilebilir. Kayıt eklemede iş anlamı taşıyan bir benzersizlik kısıtı tanımlanır ve çatışma davranışı belirtilir. Sayaç artırma ise atamayla değiştirilir: sayaç bir birikim değil, mevcut satırlardan türetilen bir değer olur.

# kur2.sh — kayit ekleme icin is anlamli benzersizlik kisiti
rm -f kutuphane.db
sqlite3 kutuphane.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 uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE UNIQUE INDEX odunc_tekil ON odunc (kitap_id, uye_id, alis_tarihi);
INSERT INTO kitap VALUES (1,'Körlük','rafta');
INSERT INTO uye VALUES (4,'Emre',0);
SQL
// uc-yazma-etkisiz.mjs — ayni uc is, tekrari etki uretmeyen bicimde
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const yazmalar = {
  "durum atama": () => db.prepare("UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = 1").run(),
  "kayit ekleme": () => db.prepare(
    `INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (1,4,'2025-07-20')
     ON CONFLICT (kitap_id, uye_id, alis_tarihi) DO NOTHING`).run(),
  "sayac tureme": () => db.prepare(
    `UPDATE uye SET acik_odunc =
       (SELECT count(*) FROM odunc WHERE uye_id = 4 AND iade_tarihi IS NULL)
     WHERE uye_id = 4`).run(),
};

for (let i = 0; i < 3; i++) {
  const etkilenen = Object.entries(yazmalar).map(([ad, yaz]) => `${ad}=${yaz().changes}`);
  console.log(`tur ${i + 1}: ${etkilenen.join("  ")}`);
}
const k = db.prepare("SELECT durumu FROM kitap WHERE kitap_id = 1").get().durumu;
const u = db.prepare("SELECT acik_odunc AS n FROM uye WHERE uye_id = 4").get().n;
const o = db.prepare("SELECT count(*) AS n FROM odunc").get().n;
console.log(`uc turdan sonra:  kitap.durumu=${k}  uye.acik_odunc=${u}  odunc satiri=${o}`);
sh kur2.sh
node uc-yazma-etkisiz.mjs
tur 1: durum atama=1  kayit ekleme=1  sayac tureme=1
tur 2: durum atama=1  kayit ekleme=0  sayac tureme=1
tur 3: durum atama=1  kayit ekleme=0  sayac tureme=1
uc turdan sonra:  kitap.durumu=odunc  uye.acik_odunc=1  odunc satiri=1

Üç tur sonunda tek bir ödünç satırı ve doğru sayaç değeri kaldı. Etkilenen satır sayıları mekanizmayı gösteriyor: ikinci turdan itibaren kayıt eklemenin etkisi sıfır. Sayaç türemesi her turda bir satırı etkilemeye devam ediyor, ama yazdığı değer değişmiyor; etkisizlik, etkilenen satır sayısının sıfır olması değil, sonucun değişmemesi demektir.

Benzersizlik kısıtının hangi sütunlardan kurulacağı bir iş kararıdır. Buradaki kısıt “aynı üye aynı kitabı aynı gün iki kez ödünç alamaz” der. Kural yanlışsa etkisizlik de yanlış kurulmuş olur; kısıt gerçekten aynı iş niyetini tanımlayan sütunlardan seçilmelidir.

Tekillik Anahtarı

İş anlamı taşıyan bir benzersizlik her zaman bulunamaz. Aynı üye aynı kitabı aynı gün iade edip yeniden alabilir; bu durumda ikinci istek yinelenen istek değil, ayrı bir iştir. Niyeti ayırt etmenin yolu, isteği yapanın ona bir ad vermesidir. API Tasarımı kursunda tanıtılan tekillik anahtarı, veri erişim katmanında bir bağıntıya yazılır.

Kritik ayrıntı yazma anıdır: anahtar ile etkinin aynı işlemde yazılması gerekir. Ayrı işlemlerde yazılırsa, ikisinin arasında çöken bir süreç ya etkisiz bir anahtar ya da anahtarsız bir etki bırakır.

# kur3.sh — tekillik bagintili kurulum
rm -f kutuphane.db
sqlite3 kutuphane.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 odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE tekillik (anahtar TEXT PRIMARY KEY, yanit TEXT NOT NULL);
INSERT INTO kitap VALUES (1,'Körlük','rafta'),(2,'Tutunamayanlar','rafta');
SQL
// tekillik.mjs — anahtar ile yanit, etkiyle ayni islemde yazilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

function oduncVer(anahtar, kitapId, uyeId, tarih) {
  db.exec("BEGIN IMMEDIATE");
  const onceki = db.prepare("SELECT yanit FROM tekillik WHERE anahtar = ?").get(anahtar);
  if (onceki !== undefined) {
    db.exec("ROLLBACK");
    return { ...JSON.parse(onceki.yanit), yeniden: true };
  }
  const s = db.prepare(
    "UPDATE kitap SET durumu = 'odunc' WHERE kitap_id = ? AND durumu = 'rafta'").run(kitapId);
  let yanit;
  if (s.changes !== 1) {
    yanit = { durum: "reddedildi", sebep: "kitap rafta degil" };
  } else {
    const id = Number(db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)")
      .run(kitapId, uyeId, tarih).lastInsertRowid);
    yanit = { durum: "verildi", oduncId: id };
  }
  db.prepare("INSERT INTO tekillik (anahtar, yanit) VALUES (?,?)").run(anahtar, JSON.stringify(yanit));
  db.exec("COMMIT");
  return { ...yanit, yeniden: false };
}

for (const [anahtar, kitapId] of [["OD-7f21", 1], ["OD-7f21", 1], ["OD-7f21", 1], ["OD-9c04", 1]]) {
  console.log(`${anahtar} kitap=${kitapId} ->`, JSON.stringify(oduncVer(anahtar, kitapId, 4, "2025-07-20")));
}
console.log("odunc satiri:", db.prepare("SELECT count(*) AS n FROM odunc").get().n);
console.log("tekillik satiri:", db.prepare("SELECT count(*) AS n FROM tekillik").get().n);
sh kur3.sh
node tekillik.mjs
OD-7f21 kitap=1 -> {"durum":"verildi","oduncId":1,"yeniden":false}
OD-7f21 kitap=1 -> {"durum":"verildi","oduncId":1,"yeniden":true}
OD-7f21 kitap=1 -> {"durum":"verildi","oduncId":1,"yeniden":true}
OD-9c04 kitap=1 -> {"durum":"reddedildi","sebep":"kitap rafta degil","yeniden":false}
odunc satiri: 1
tekillik satiri: 2

Aynı anahtarla gelen üç çağrı tek bir ödünç kaydı üretti ve üçü de aynı kimliği döndürdü. Yanıtın saklanması, yalnız etkiyi engellemekten fazlasını yapıyor: yinelenen istek de başarılı bir yanıt alıyor. Yanıt saklanmasaydı, ikinci çağrının “zaten yapılmış” demesi gerekirdi ve istemci bunu bir hata olarak yorumlayabilirdi.

Dördüncü satır önemli bir ayrımı gösteriyor. Farklı anahtarla gelen istek yinelenen istek değildir; işlendi ve iş kuralı gereği reddedildi. Ret de bir sonuçtur ve o da saklandı — tekillik bağıntısında iki satır var. Saklanmasaydı, aynı reddedilen isteğin yeniden denenmesi kuralın yeniden çalıştırılmasına yol açardı.

Tekillik bağıntısı sınırsız büyür. Anahtarların bir saklama penceresi olmalıdır; pencere, istemcilerin yeniden deneme süresinden uzun, depolama maliyetinin kabul edilebilir olduğu kadar kısa seçilir.

Giden Kutusunun Tüketicisi

Önceki dersteki ölçüm artık düzeltilebilir. Tüketici, iletiyi işlerken ileti kimliğini de aynı işlemde kaydeder.

# kur4.sh — tuketici tarafi
rm -f uyelik.db
sqlite3 uyelik.db >/dev/null <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL);
CREATE TABLE islenen_ileti (ileti_id INTEGER PRIMARY KEY, islenme TEXT NOT NULL);
INSERT INTO uye VALUES (6,'Burak',0);
SQL
// tuket.mjs — ileti kimligi ile etki ayni islemde yazilir; ikinci teslim etkisiz kalir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("uyelik.db");
const iletiId = Number(process.argv[2]);
const uyeId = Number(process.argv[3]);

db.exec("BEGIN IMMEDIATE");
const yeni = db.prepare(
  "INSERT INTO islenen_ileti (ileti_id, islenme) VALUES (?, datetime('now')) ON CONFLICT DO NOTHING")
  .run(iletiId);
if (yeni.changes === 0) {
  db.exec("ROLLBACK");
  console.log(`ileti ${iletiId}: daha once islenmis, etki uygulanmadi`);
} else {
  db.prepare("UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_id = ?").run(uyeId);
  db.exec("COMMIT");
  console.log(`ileti ${iletiId}: islendi`);
}
console.log(`  uye ${uyeId} acik odunc =`,
  db.prepare("SELECT acik_odunc AS n FROM uye WHERE uye_id = ?").get(uyeId).n);
sh kur4.sh
node tuket.mjs 1 6
node tuket.mjs 1 6
node tuket.mjs 2 6
ileti 1: islendi
  uye 6 acik odunc = 1
ileti 1: daha once islenmis, etki uygulanmadi
  uye 6 acik odunc = 1
ileti 2: islendi
  uye 6 acik odunc = 2

Bir numaralı iletinin iki kez teslimi tek bir etki üretti; iki numaralı ileti ayrı bir iş olarak işlendi. Önceki derste ikiye çıkan sayaç artık bir. Sayaç artırma yazmasının kendisi hâlâ etkisiz değil; etkisiz olan, tüketicinin tamamıdır. Etkisizlik yazma biçiminden ya da bir bekçi kaydından gelir; ikisinden biri yeterlidir.

Bekçi kaydının etkiyle aynı işlemde yazılması burada da zorunludur. Kayıt önce yazılıp işlem sonra çökerse ileti işlenmiş sayılır ama etkisi uygulanmamış olur; etki önce uygulanıp kayıt yazılamazsa ileti yeniden işlenir. İkisini tek sınıra almak bu iki hatayı da kapatır.

Özet

  • Bir yazma, sonucu yalnız hedef duruma bağlıysa etkisizdir; ölçümde durum ataması üç çalıştırmadan sonra aynı sonucu verdi, sayaç artırma 3, kayıt ekleme 3 satır üretti.
  • Kayıt ekleme iş anlamlı bir benzersizlik kısıtı ve çatışma davranışıyla, sayaç artırma türetilmiş bir atamayla etkisiz biçime çevrildi; üç turdan sonra tek satır kaldı.
  • İş anlamlı benzersizlik bulunamadığında istek kendi adını taşır; tekillik anahtarı etkiyle aynı işlemde yazılır.
  • Yanıtın saklanması yinelenen isteğin de başarılı sonuç almasını sağlar; reddedilen sonuç da saklanır.
  • Giden kutusu tüketicisi ileti kimliğini etkiyle aynı işlemde kaydettiğinde iki kez teslim edilen ileti tek etki üretti.

Sonraki Adım

İşlemler konusu, yazmanın doğruluğunu kuran dört soruyu kapattı: sınır nereye çizilir, okuma neyi görür, eşzamanlı güncelleme nasıl çözülür, yeniden deneme neden güvenlidir. Bu sorular doğrulukla ilgiliydi; hiçbiri hızla ilgili değildi. Doğru çalışan bir veri erişim katmanı yine de yavaş olabilir ve yavaşlığın kaynağı çoğunlukla sorgunun kendisi değil, sorgu sayısıdır. İlk derste kurulan eşleyici üç kayıt için on sorgu çalıştırmıştı. Sonraki konu o sayıyı ölçer, kayıt sayısıyla nasıl büyüdüğünü gösterir ve toplu getirmeyle nasıl sabitlendiğini uygulayarak kanıtlar.

İ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