Ders 18 / 19
Olay Kaynaklı Kalıcılık
Durum yerine olay dizisinin saklanması: dört sorunun durum tablosundan ve olay dizisinden yanıtlanabilirliğinin karşılaştırılması, durumun olaylardan yeniden kurulması için okunan olay sayısının sayılması, anlık görüntü aralığı değiştikçe okunan olay ile yazılan görüntü sayısının nasıl değiştiğinin ölçülmesi ve iki kalıcılığın bayt boyunun karşılaştırılması.
İçindekiler
Önceki dersin okuma modeli komutların ürettiği farklardan kuruldu, farkların kendisi ise uygulandıktan sonra atıldı. Yazma tarafında kalan tek şey son durumdu. Bu, sonradan sorulan soruları yanıtsız bırakır: bir teslimatın adresi kaç kez düzeltildi, kaç aktarma noktasından geçti, teslim kaç denemede oldu? Bu soruların hiçbiri kaydın şemasında yoktu, çünkü şema izleme ekranı için tasarlanmıştı.
Olay kaynaklı kalıcılık (event sourcing) sıralamayı tersine çevirir: saklanan asıl şey durum değil, durumu üreten olayların dizisidir. Durum bir türev hâline gelir — olaylar sırayla katlanarak yeniden kurulur.
Olayın Kalıcılık Biçimi Olması
Olayların kuyruğa konması, tüketiciye ulaşması ve teslim garantileri Önbellekleme, Kuyruklar ve Eşzamansız İşleme kursunda ele alındı. Bu dersin konusu o değil: burada olay dizisi bir kalıcılık biçimi olarak ele alınıyor ve tek soru şudur — durum bu diziden yeniden kurulabiliyor mu, kurulurken kaç olay okunuyor?
Olayların adları da bu kursun Alan Olayları dersinde kurulan ölçüte uyar: aktarmaya-girdi,
adres-duzeltildi, teslim-denendi alan uzmanının kullandığı sözcüklerdir, veritabanı
işlemlerinin adları değildir. Her olayın karşılığı bir geçiştir.
// durum-kur.mjs — alan diline ait gecisler ve olay dizisinden durumun katlanarak kurulmasi export const BOS = { durum: "yok", nokta: null, adres: null, aktarmaSayisi: 0, denemeSayisi: 0, adresDuzeltmesi: 0, }; const GECIS = { "kabul-edildi": (d, o) => ({ ...d, durum: "kabul-edildi", adres: o.adres }), "aktarmaya-girdi": (d, o) => ({ ...d, durum: "aktarmada", nokta: o.nokta, aktarmaSayisi: d.aktarmaSayisi + 1 }), "aktarmadan-cikti": (d) => ({ ...d, durum: "yolda" }), "dagitima-cikildi": (d) => ({ ...d, durum: "dagitimda" }), "teslim-denendi": (d) => ({ ...d, denemeSayisi: d.denemeSayisi + 1 }), "adres-duzeltildi": (d, o) => ({ ...d, adres: o.adres, adresDuzeltmesi: d.adresDuzeltmesi + 1 }), "teslim-edildi": (d) => ({ ...d, durum: "teslim-edildi" }), }; export function durumKur(baslangic, olaylar) { let d = baslangic; for (const o of olaylar) { const gecis = GECIS[o.tur]; if (gecis === undefined) throw new RangeError(`bilinmeyen olay: ${o.tur}`); d = gecis(d, o); } return d; }
Katlama işlevi saf fonksiyondur ve başlangıç durumunu parametre olarak alır. Bu ayrıntı anlık görüntüyü olanaklı kılan şeydir: dizinin ortasındaki bir durumdan başlanabilir.
Durum Saklayan Kalıcılık
Karşılaştırılacak öbür kalıcılık yalnız son durumu tutar. Şema izleme ekranının sorularını yanıtlayacak biçimde seçilmiştir: paket nerede, durumu ne, adresi ne.
// durum-modeli.mjs — yalniz son durumu tutan kalicilik: izleme ekrani icin tasarlanmis semasi const SATIR = new Map(); export const durumModeli = { uygula(id, olay) { const s = SATIR.get(id) ?? { id, durum: "yok", nokta: null, adres: null }; if (olay.tur === "kabul-edildi") { s.durum = "kabul-edildi"; s.adres = olay.adres; } if (olay.tur === "aktarmaya-girdi") { s.durum = "aktarmada"; s.nokta = olay.nokta; } if (olay.tur === "aktarmadan-cikti") s.durum = "yolda"; if (olay.tur === "dagitima-cikildi") s.durum = "dagitimda"; if (olay.tur === "adres-duzeltildi") s.adres = olay.adres; if (olay.tur === "teslim-edildi") s.durum = "teslim-edildi"; SATIR.set(id, s); }, oku: (id) => structuredClone(SATIR.get(id) ?? null), boyut: () => JSON.stringify([...SATIR.values()]).length, };
Olay Deposu ve Anlık Görüntü
Olay deposu yalnız ekleme kabul eder; yazılmış bir olay değiştirilmez. Anlık görüntü (snapshot) belirli bir olay sayısında hesaplanmış durumu bir rafta tutar, böylece yeniden kurma dizinin başından değil o noktadan başlar.
// olay-deposu.mjs — eklemeli olay dizisi, anlik goruntu rafi ve yeniden kurma sayaci import { BOS, durumKur } from "./durum-kur.mjs"; const DIZI = new Map(); const GORUNTU = new Map(); export const olayDeposu = { ekle(id, olay) { const dizi = DIZI.get(id) ?? []; dizi.push(olay); DIZI.set(id, dizi); }, kimlikler: () => [...DIZI.keys()], olaySayisi: (id) => DIZI.get(id).length, toplamOlay: () => [...DIZI.values()].reduce((s, d) => s + d.length, 0), boyut: () => JSON.stringify([...DIZI.entries()]).length, goruntuleriHazirla(aralik) { GORUNTU.clear(); if (aralik === 0) return 0; for (const [id, dizi] of DIZI) { for (let sinir = aralik; sinir <= dizi.length; sinir += aralik) { GORUNTU.set(`${id}:${sinir}`, durumKur(BOS, dizi.slice(0, sinir))); } } return GORUNTU.size; }, yenidenKur(id, aralik) { const dizi = DIZI.get(id); const sinir = aralik === 0 ? 0 : Math.floor(dizi.length / aralik) * aralik; const goruntu = GORUNTU.get(`${id}:${sinir}`); const kalan = dizi.slice(sinir); return { durum: durumKur(goruntu ?? BOS, kalan), okunanOlay: kalan.length, okunanGoruntu: goruntu === undefined ? 0 : 1, }; }, };
Gerçek bir sistemde görüntüler olay eklenirken alınır. Ölçümde aralık bir değişken olduğu için
goruntuleriHazirla hepsini bir kerede hesaplar ve kaç görüntü yazıldığını döndürür; bu sayı
bedelin karşılığıdır.
Aynı Geçmişin İki Kalıcılığa Yazılması
Karşılaştırmanın geçerli olması için iki kalıcılığın aynı olayları görmesi gerekir. Üretici sabit bir sayı dizisiyle çalışır; her yirmi beşinci teslimat çok kez yönlendirilmiş sorunlu bir gönderidir.
// gecmis-uret.mjs — ayni olaylari iki kaliciliga da yazan sabit uretec import { olayDeposu } from "./olay-deposu.mjs"; import { durumModeli } from "./durum-modeli.mjs"; const NOKTALAR = ["34", "06", "35", "01", "16"]; export function gecmisUret(teslimatSayisi) { let x = 13; const sonraki = () => (x = (x * 48271 + 11) % 2147483647); for (let i = 1; i <= teslimatSayisi; i += 1) { const id = `T${i}`; const yaz = (olay) => { olayDeposu.ekle(id, olay); durumModeli.uygula(id, olay); }; yaz({ tur: "kabul-edildi", adres: `A${(sonraki() % 900) + 100}` }); const aktarma = i % 25 === 0 ? 14 + (sonraki() % 8) : 1 + (sonraki() % 4); for (let a = 0; a < aktarma; a += 1) { yaz({ tur: "aktarmaya-girdi", nokta: NOKTALAR[sonraki() % NOKTALAR.length] }); yaz({ tur: "aktarmadan-cikti" }); } yaz({ tur: "dagitima-cikildi" }); const deneme = 1 + (sonraki() % 3); for (let d = 0; d < deneme; d += 1) { yaz({ tur: "teslim-denendi" }); if (d < deneme - 1 && sonraki() % 2 === 0) yaz({ tur: "adres-duzeltildi", adres: `A${(sonraki() % 900) + 100}` }); } yaz({ tur: "teslim-edildi" }); } return olayDeposu.toplamOlay(); }
Ölçüm
Ölçüm üç şeyi sayar. Birincisi yanıtlanabilirlik: dört sorunun kaçı hangi kalıcılıktan çıkıyor. İkincisi yeniden kurma maliyeti: dört ayrı anlık görüntü aralığında okunan olay, okunan görüntü ve yazılan görüntü sayıları. Üçüncüsü bayt boyu. Yeniden kurmanın süresi donanıma ve koşum ortamına bağlı olduğu için ölçü olarak süre değil okunan olay sayısı kullanılıyor; iş yükü doğrudan bu sayıyla artar.
// olay-say.mjs — dort sorunun yanitlanabilirligi, yeniden kurma icin okunan olay ve anlik goruntu etkisi import { olayDeposu } from "./olay-deposu.mjs"; import { durumModeli } from "./durum-modeli.mjs"; import { gecmisUret } from "./gecmis-uret.mjs"; const toplam = gecmisUret(400); console.log(`teslimat = 400, olay = ${toplam}, olay dizisi boyutu = ${olayDeposu.boyut()} bayt` + `, durum tablosu boyutu = ${durumModeli.boyut()} bayt`); const SORULAR = [ ["su anki durum", (d) => d.durum, (s) => s.durum], ["adres duzeltmesi", (d) => d.adresDuzeltmesi, (s) => s.adresDuzeltmesi], ["aktarma sayisi", (d) => d.aktarmaSayisi, (s) => s.aktarmaSayisi], ["teslim denemesi", (d) => d.denemeSayisi, (s) => s.denemeSayisi], ]; const enUzun = olayDeposu.kimlikler().reduce((a, b) => (olayDeposu.olaySayisi(a) >= olayDeposu.olaySayisi(b) ? a : b)); console.log(`en uzun gecmis = ${enUzun}, olay = ${olayDeposu.olaySayisi(enUzun)}`); const olaydan = olayDeposu.yenidenKur(enUzun, 0).durum; const satirdan = durumModeli.oku(enUzun); console.log(`${enUzun} icin dort soru:`); let yanitlanan = 0; for (const [ad, olayYolu, satirYolu] of SORULAR) { const o = olayYolu(olaydan), s = satirYolu(satirdan); if (s !== undefined) yanitlanan += 1; console.log(` ${ad.padEnd(17)} olay dizisi = ${String(o).padEnd(13)} durum tablosu = ${s ?? "yanit yok"}`); } console.log(`durum tablosunun yanitladigi soru = ${yanitlanan} / 4, olay dizisinin = 4 / 4`); const tam = JSON.stringify(olayDeposu.kimlikler().map((id) => olayDeposu.yenidenKur(id, 0).durum)); for (const aralik of [0, 20, 10, 5]) { const yazilan = olayDeposu.goruntuleriHazirla(aralik); let okunanOlay = 0, okunanGoruntu = 0; const durumlar = olayDeposu.kimlikler().map((id) => { const r = olayDeposu.yenidenKur(id, aralik); okunanOlay += r.okunanOlay; okunanGoruntu += r.okunanGoruntu; return r.durum; }); const tek = olayDeposu.yenidenKur(enUzun, aralik).okunanOlay; console.log(`aralik = ${String(aralik).padStart(2)} yazilan goruntu = ${String(yazilan).padStart(4)}` + `, okunan olay = ${String(okunanOlay).padStart(4)}, okunan goruntu = ${String(okunanGoruntu).padStart(3)}` + `, ${enUzun} icin okunan olay = ${String(tek).padStart(2)}, durum ayni mi = ${JSON.stringify(durumlar) === tam}`); }
node olay-say.mjs
teslimat = 400, olay = 4699, olay dizisi boyutu = 150754 bayt, durum tablosu boyutu = 26293 bayt en uzun gecmis = T375, olay = 49 T375 icin dort soru: su anki durum olay dizisi = teslim-edildi durum tablosu = teslim-edildi adres duzeltmesi olay dizisi = 1 durum tablosu = yanit yok aktarma sayisi olay dizisi = 21 durum tablosu = yanit yok teslim denemesi olay dizisi = 3 durum tablosu = yanit yok durum tablosunun yanitladigi soru = 1 / 4, olay dizisinin = 4 / 4 aralik = 0 yazilan goruntu = 0, okunan olay = 4699, okunan goruntu = 0, T375 icin okunan olay = 49, durum ayni mi = true aralik = 20 yazilan goruntu = 26, okunan olay = 4179, okunan goruntu = 16, T375 icin okunan olay = 9, durum ayni mi = true aralik = 10 yazilan goruntu = 297, okunan olay = 1729, okunan goruntu = 255, T375 icin okunan olay = 9, durum ayni mi = true aralik = 5 yazilan goruntu = 776, okunan olay = 819, okunan goruntu = 400, T375 icin okunan olay = 4, durum ayni mi = true
Sayıların Okunması
Dört soru ayrımı gösteriyor. Durum tablosu 4 sorunun 1’ini yanıtladı; kalan üçü şemasında bulunmadığı için yanıtsız kaldı. Olay dizisi 4’ünü de yanıtladı ve bunu şemasına yeni bir alan eklemeden yaptı: adres düzeltmesi 1, aktarma sayısı 21, teslim denemesi 3. Aradaki fark saklanan bilgi miktarı değil, hangi soruların önceden bilinmek zorunda olduğudur. Durum saklayan kalıcılıkta soru şema tasarlanırken bilinmelidir; olay dizisinde sorulduğu anda yanıtlanır.
Yeniden kurma satırları anlık görüntünün etkisini veriyor. Görüntü olmadan 400 teslimatın durumunu kurmak 4699 olay okuyor. Aralık 20 iken toplam yalnız 4179’a iniyor: ortalama geçmiş uzunluğu 11,7 olduğu için teslimatların çoğu yirmi olaya hiç ulaşmıyor, 400 teslimattan yalnız 16’sı bir görüntü kullanabiliyor. Aralık 10 iken okunan olay 1729’a, aralık 5 iken 819’a iniyor. Kural tek cümleyle yazılır: anlık görüntü aralığı ortalama geçmiş uzunluğunun altına inmedikçe okunan olay sayısı düşmez.
Bedel öbür sütunda duruyor. Aralık 5’e indiğinde okunan olay 4699’dan 819’a düşerken yazılan görüntü sayısı 0’dan 776’ya çıkıyor ve her yeniden kurma bir de görüntü okuması ekliyor. Yani görüntü, okuma yükünü yazma yüküne çeviren bir takastır.
En uzun geçmiş bunu tek teslimatta gösteriyor. T375’in 49 olayı var; görüntüsüz yeniden kurma 49 olay okuyor, aralık 20 ile 9, aralık 10 ile 9, aralık 5 ile 4 olay okuyor. Dört aralıkta da kurulan durum tam yeniden kurmayla aynı çıktı; görüntü bir kısayoldur, ayrı bir doğruluk kaynağı değildir.
Bedel ve Sınır
Olay dizisi 150754 bayt, durum tablosu 26293 bayt yer tuttu: yaklaşık 5,7 kat. Bu oran zamanla büyür, çünkü olay dizisi yalnız uzar. Durum tablosu ise teslimat sayısıyla büyür, geçmiş uzunluğuyla değil.
İkinci bedel şema evrimidir. Yazılmış bir olay değiştirilmediği için bir olay türünün alanları
sonradan değişirse eski olaylar eski biçimiyle kalır ve katlama işlevi iki biçimi de
tanımak zorundadır; GECIS tablosunda olay türü başına birden çok sürüm belirir. Durum saklayan
kalıcılıkta aynı değişiklik tek bir göç işlemiyle biter.
Üçüncüsü bir alan kısıtıdır: olay dizisi bir kaydın geçmişini kalıcı hâle getirir. Bir kaydın tamamen silinmesi gereken durumlarda dizinin eklemeli olması bir engeldir ve bu, tasarım kararının alanla birlikte verilmesi gerektiğini gösterir.
Kazanç bu üç bedele karşı iki koşulda doğar: geçmişin kendisinin alan için bir değeri olması ve sonradan sorulacak soruların önceden bilinmemesi. Yalnız son durumun sorulduğu bir kayıtta olay dizisi 5,7 kat yer ve bir katlama işlevi karşılığında hiçbir yeni yanıt üretmez.
Özet
- Olay kaynaklı kalıcılık durumu değil onu üreten olay dizisini saklar; durum katlama işleviyle türetilir ve katlama saf fonksiyon olduğu için dizinin ortasından başlatılabilir.
- Dört sorudan durum tablosu 1’ini, olay dizisi 4’ünü yanıtladı; fark saklanan bilgi miktarı değil, soruların şema tasarlanırken bilinmek zorunda olup olmamasıdır.
- 400 teslimatın durumunu kurmak görüntüsüz 4699 olay okudu; aralık 20, 10 ve 5 iken bu sayı 4179, 1729 ve 819 oldu. Aralık ortalama geçmiş uzunluğunun altına inmedikçe kazanç küçüktür.
- Görüntü okuma yükünü yazma yüküne çevirir: okunan olay 819’a inerken yazılan görüntü 776 oldu. Dört aralıkta da kurulan durum tam yeniden kurmayla aynı çıktı.
- Bedel 5,7 katlık yer, olay türü başına biriken sürüm sayısı ve eklemeli dizinin silmeyi güçleştirmesidir; kazanç geçmişin alan için değerli olmasına bağlıdır.
Sonraki Adım
Bu konunun dört dersi de aynı biçimde ilerledi: bir model kuruldu, bir ölçü seçildi, iki düzen aynı ölçüyle karşılaştırıldı. Ama kararların hiçbiri bir kez verilip bitmedi. Kullanım senaryosundaki karar sayısı, bağlantı noktalarının adları, okuma modelinin satırları ve olay türlerinin listesi — bunların her biri alan uzmanıyla konuşuldukça değişir ve her değişiklik kodda bir bedel ister. Bir model iyileştirmesinin gerçekten iyileştirme olup olmadığı, ancak o bedel sayıldığında bilinir. Sonraki ders modelin gerçekleştirimle birlikte evrimini ele alır: alan uzmanının bir cümlesinin modelde karşılığı olmadığında yapılan iyileştirmeyi, kuralın yazım yeri sayısındaki düşüşü, adların örtüşme oranını ve iyileştirmenin düzenlettiği dosya sayısını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.