---
title: 'Önbellek Katmanları'
source: 'https://academia.sh/tr/kurslar/eszamansiz-isleme/onbellek-katmanlari'
course: 'Önbellekleme, Kuyruklar ve Eşzamansız İşleme'
language: tr
updated: '2026-08-17T18:06:46+00:00'
license: 'CC BY-SA 4.0'
---

# Önbellek Katmanları

Aynı yanıtı defalarca üreten bir okuma yolunun ölçülmesi ve istemci, kenar, sunucu ile veritabanı önbelleklerinin zincir olarak kurulması: her katmanın kaç isteği durdurduğu, paylaşılan ile özel önbellek ayrımı ve kaynağa ulaşan istek oranının düşürülmesi.

Veri Erişim Katmanı ve İş Mantığı kursu, bir isteğin veriye inen yolunu baştan sona kurdu:
depo kalıbı, işlem sınırı, sorgu bütçesi, alan kuralları. Kursun sonunda bir varsayım
kalmıştı: her istek verinin kaynağına gidiyor. Bu varsayım her zaman tutmaz. Kütüphane
sisteminde kitap ayrıntı sayfası günde binlerce kez açılır ve kitabın başlığı ile yazarı
aylardır değişmemiştir. Aynı sorgu aynı yanıtı üretiyorsa, yanıtın saklanması sorgunun
tekrarından ucuzdur.

Bu ders önce tekrarın ölçüsünü koyar, sonra yanıtın saklanabileceği yerleri sıralar. Saklama
yerlerinin her biri bir **önbellek** (cache) katmanıdır ve katmanlar bir zincir oluşturur:
istek zincirin ne kadar erken bir halkasında durursa o kadar ucuza karşılanır.

## Ölçülen Tekrar

Ölçümler kurs boyunca kullanılan kütüphane şeması üzerinde yapılır: şube, kitap, üye ve
ödünç bağıntıları. Aşağıdaki blok veritabanını sıfırdan kurar.

```sh
# kur.sh — kutuphane veritabanini kurar (M16/K04 semasinin ayni sutunlariyla)
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY,
                    kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 40 + 1), 1950 + n % 70, n % 4 + 1
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 300) SELECT n FROM s);
INSERT INTO uye SELECT n, 'Uye' || n, 'Soyad' || n, 'uye' || n || '@ornek.test', '2024-01-01'
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200) SELECT n FROM s);
INSERT INTO odunc SELECT n, n % 300 + 1, n % 200 + 1,
  '2025-' || substr('0' || (n % 12 + 1), -2) || '-' || substr('0' || (n % 28 + 1), -2),
  CASE WHEN n % 5 = 0 THEN NULL ELSE '2025-12-31' END
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 4000) SELECT n FROM s);
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX kitap_sube ON kitap(sube_id);
SQL
sqlite3 kutuphane.db "SELECT 'sube=' || (SELECT count(*) FROM sube) || ' kitap=' || (SELECT count(*) FROM kitap) || ' uye=' || (SELECT count(*) FROM uye) || ' odunc=' || (SELECT count(*) FROM odunc);"
```

```
sube=4 kitap=300 uye=200 odunc=4000
```

Kaynak, üç okumayı sunar: kitap ayrıntısı, şube raf sayısı ve üyenin ödünç geçmişi. Her
okuma sayılır — Başarım Sorunları konusundaki sorgu sayacının aynı yaklaşımı.

```js
// kaynak.mjs — kutuphane verisinin kaynagi; her okuma sayilir
import { DatabaseSync } from "node:sqlite";

export function kaynakAc(dosya = "kutuphane.db") {
  const db = new DatabaseSync(dosya);
  const sayac = { sorgu: 0 };
  const tek = (sql, ...d) => { sayac.sorgu += 1; return db.prepare(sql).get(...d); };
  return {
    sayac,
    kitapAyrinti: (id) => tek(
      "SELECT kitap_id, baslik, yazar, basim_yili, sube_id FROM kitap WHERE kitap_id = ?", id),
    subeStok: (subeId) => tek(
      `SELECT ? AS sube_id, count(*) AS raftaki FROM kitap k
       WHERE k.sube_id = ? AND NOT EXISTS
         (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.iade_tarihi IS NULL)`,
      subeId, subeId),
    uyeGecmisi: (uyeId) => tek(
      "SELECT ? AS uye_id, count(*) AS toplam FROM odunc WHERE uye_id = ?", uyeId, uyeId),
    kapat: () => db.close(),
  };
}

// Istek akisi: kitap ayrinti istekleri birkac baslikta yogunlasir (belirlenimci uretec).
export function istekAkisi(adet) {
  let tohum = 20250729;
  const rastgele = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
  const istekler = [];
  for (let i = 0; i < adet; i++) {
    const r = rastgele();
    if (r < 0.7) istekler.push(["kitap", Math.floor(rastgele() * 20) + 1]);
    else if (r < 0.9) istekler.push(["sube", Math.floor(rastgele() * 4) + 1]);
    else istekler.push(["uye", Math.floor(rastgele() * 50) + 1]);
  }
  return istekler;
}
```

Altı yüz isteklik akış doğrudan kaynağa uygulandığında ortaya çıkan sayı, tekrarın ölçüsüdür.

```js
// kaynaksiz.mjs — her istek kaynaga gidiyor: sorgu sayisi istek sayisina esit
import { kaynakAc, istekAkisi } from "./kaynak.mjs";

const kaynak = kaynakAc();
const istekler = istekAkisi(600);
const okuyucu = { kitap: kaynak.kitapAyrinti, sube: kaynak.subeStok, uye: kaynak.uyeGecmisi };

const t = performance.now();
for (const [tur, id] of istekler) okuyucu[tur](id);
const sure = performance.now() - t;

const tekil = new Set(istekler.map(([tur, id]) => `${tur}:${id}`)).size;
console.log(`istek=${istekler.length}  tekil_anahtar=${tekil}  kaynak_sorgusu=${kaynak.sayac.sorgu}`);
console.log(`ayni yaniti ureten tekrar sorgu = ${kaynak.sayac.sorgu - tekil}`);
console.log(`sure=${sure.toFixed(1)} ms (ortama bagli)`);
kaynak.kapat();
```

```
istek=600  tekil_anahtar=53  kaynak_sorgusu=600
ayni yaniti ureten tekrar sorgu = 547
sure=545.3 ms (ortama bagli)
```

Altı yüz istek yalnızca 53 farklı yanıt üretti. Kalan 547 sorgu, veritabanının daha önce
ürettiği bir sonucu yeniden üretmek için çalıştı. Süre alanı makineye bağlıdır ve her koşuda
değişir; 600 ile 53 sayıları değişmez, çünkü bunlar iş yükünün özellikleridir.

Buradaki fırsat sorgunun hızlandırılması değildir. Sorgu zaten dizinli ve planı düzgün.
Fırsat, sorgunun **çalıştırılmamasıdır**.

## Zincirdeki Dört Katman

Yanıt, kaynak ile kullanıcı arasındaki dört noktada saklanabilir.

**İstemci önbelleği** kullanıcının kendi aygıtındadır: tarayıcının HTTP önbelleği ya da
uygulamanın yerel deposu. Ağa hiç çıkmadığı için en ucuz katmandır. Karşılığında yalnız tek
kullanıcıya hizmet eder ve içeriği sunucunun denetiminden çıkmıştır — konan kopya, konarken
verilen süre dolana kadar orada kalır.

**Kenar önbelleği** (edge cache) kullanıcıya coğrafi olarak yakın, paylaşılan bir ara
noktadır. Frontend Kalitesi kursundaki Statik Barındırma dersinde statik dosyalar için
tanıtılmıştı; aynı katman API yanıtları için de çalışır. Paylaşılan olduğu için bir
kullanıcının ısıttığı kopyayı diğerleri kullanır. Aynı nedenle kişiye özel veriyi taşıyamaz:
oraya konan bir üye geçmişi başka bir üyeye sunulabilir.

**Sunucu önbelleği** uygulamanın kendi denetimindedir. İki biçimi vardır: süreç içi bellek
ve bütün örneklerin paylaştığı ayrı bir depo. Süreç içi olan en hızlısıdır ama her örnek
kendi kopyasını tutar; paylaşılan olan bir ağ turu ister ama tek bir doğruluk noktası verir.
Bu katman uygulamanın kodundadır, dolayısıyla geçersizleştirme kararını anında uygulayabilir.

**Veritabanı önbelleği** kaynağın içindedir: sayfa arabelleği ve sorgu planı önbelleği.
Yukarıdaki ölçümde bu katman zaten çalışıyordu — 600 sorgunun tamamı sıcak sayfalar üzerinde
koştu. Sonuç yine 600 sorgudur. Veritabanı önbelleği bir sorgunun **maliyetini** düşürür,
sorgu **sayısını** düşürmez. Bağlantı, ayrıştırma, plan seçimi ve gidiş-dönüş her çağrıda
yeniden ödenir.

## Zincirin Ölçülmesi

Dört katmanı bir zincir olarak kurup her katmanın kaç isteği durdurduğunu saymak, katman
tartışmasını sayıya çevirir. Aşağıdaki kurulum üç önbellek katmanını gerçekler; dördüncüsü
kaynağın içindedir. Her katmanın bir kapasitesi vardır: dolduğunda en eski girdi atılır.

```js
// katmanlar.mjs — dort katmanli zincir: her istek nerede duruyor?
import { kaynakAc } from "./kaynak.mjs";

// Kapasitesi dolunca en eski girdiyi atan kucuk bir onbellek.
function kucukOnbellek(kapasite) {
  const kutu = new Map();
  return {
    al: (a) => kutu.get(a),
    var: (a) => kutu.has(a),
    koy(a, d) {
      kutu.delete(a); kutu.set(a, d);
      if (kutu.size > kapasite) kutu.delete(kutu.keys().next().value);
    },
  };
}

const kaynak = kaynakAc();
const okuyucu = { kitap: kaynak.kitapAyrinti, sube: kaynak.subeStok, uye: kaynak.uyeGecmisi };
const GENEL = new Set(["kitap", "sube"]);          // her uyeye ayni donen kaynaklar

const istemciler = new Map();                       // kullanici basina ayri onbellek, 4 girdi
const kenar = kucukOnbellek(10);                    // paylasilan, yalniz genel kaynaklar
const sunucu = kucukOnbellek(200);                  // paylasilan, her sey
const sayac = { istemci: 0, kenar: 0, sunucu: 0, kaynak: 0 };

function oku(kullanici, tur, id) {
  const anahtar = `${tur}:${id}`;
  if (!istemciler.has(kullanici)) istemciler.set(kullanici, kucukOnbellek(4));
  const istemci = istemciler.get(kullanici);
  const yerlestir = (d) => {
    if (GENEL.has(tur)) kenar.koy(anahtar, d);
    istemci.koy(anahtar, d);
    return d;
  };
  if (istemci.var(anahtar)) { sayac.istemci += 1; return istemci.al(anahtar); }
  if (GENEL.has(tur) && kenar.var(anahtar)) { sayac.kenar += 1; return yerlestir(kenar.al(anahtar)); }
  const sunucuAnahtari = GENEL.has(tur) ? anahtar : `${anahtar}@${kullanici}`;
  if (sunucu.var(sunucuAnahtari)) { sayac.sunucu += 1; return yerlestir(sunucu.al(sunucuAnahtari)); }
  sayac.kaynak += 1;
  const deger = okuyucu[tur](id);
  sunucu.koy(sunucuAnahtari, deger);
  return yerlestir(deger);
}

let tohum = 20250729;
const rastgele = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
const ISTEK = 600;
for (let i = 0; i < ISTEK; i++) {
  const kullanici = Math.floor(rastgele() * 40) + 1;
  const r = rastgele();
  const tur = r < 0.7 ? "kitap" : r < 0.9 ? "sube" : "uye";
  const id = tur === "kitap" ? Math.floor(rastgele() * 20) + 1
    : tur === "sube" ? Math.floor(rastgele() * 4) + 1 : kullanici;
  oku(kullanici, tur, id);
}

console.log(`istek=${ISTEK}`);
for (const [ad, n] of Object.entries(sayac)) {
  console.log(`${ad.padEnd(8)} ${String(n).padStart(4)}  (%${((n / ISTEK) * 100).toFixed(1)})`);
}
console.log(`kaynak sorgusu = ${kaynak.sayac.sorgu}`);
kaynak.kapat();
```

```
istek=600
istemci    98  (%16.3)
kenar     189  (%31.5)
sunucu    260  (%43.3)
kaynak     53  (%8.8)
kaynak sorgusu = 53
```

Kaynağa ulaşan istek sayısı 600'den 53'e düştü; iş yükündeki her farklı anahtar kaynağı bir
kez gördü. Zincirin geri kalanı ilginç olan kısmıdır. En yakın katman isteklerin yalnız
%16'sını durdurdu, çünkü kapasitesi dört girdiyle sınırlı ve her kullanıcının kopyası
ayrıdır. Paylaşılan kenar katmanı %31'ini durdurdu. En çok işi %43 ile sunucu katmanı yaptı:
kapasitesi en büyük, kapsamı en geniş katman odur.

Sayıların kapasiteye bağlı olması bu ölçümün asıl dersidir. Kenar katmanının kapasitesi
büyütülürse payı artar, sunucununki azalır. Katmanlar birbirinin yerine geçmez; birbirinin
artığını toplar.

## Bir Veri Hangi Katmana Konur

Katman seçimi iki ölçütle yapılır.

Birincisi **paylaşım**tır. Bir yanıt bütün kullanıcılara aynı dönüyorsa paylaşılan katmanlara
konabilir. Kitap ayrıntısı ve şube raf sayısı böyledir. Üyenin ödünç geçmişi değildir; onu
kenar önbelleğine koymak, Kimlik Doğrulama ve Yetkilendirme kursunda üzerinde durulan
yetkilendirme sınırını delmek demektir. Ölçüm kodunda bu ayrım `GENEL` kümesiyle yapılmış ve
kişiye özel kayıtlar sunucu katmanında kullanıcı kimliğiyle anahtarlanmıştır.

İkincisi **bayatlık toleransıdır** (staleness). Önbellekteki kopya, kaynak değiştiğinde bir
süre eski kalır. Kitabın yazarı için bu süre saatler olabilir; şubedeki raf sayısı için
saniyeler. Ödünç verme işleminin kendisi için sıfırdır — bir yazma işleminin sonucu
önbellekten okunamaz.

Katmanın kullanıcıya yakınlığı ile denetlenebilirliği ters orantılıdır. Sunucu katmanındaki
bir girdi kod tarafından anında silinebilir. Kenar katmanındakini silmek ayrı bir işlem
ister. İstemcideki kopya için hiçbir yol yoktur: konarken verilen süre dolana kadar
kullanılır. Bu yüzden en yakın katmana en uzun ömür verilen veriler konur ve bu kararın
geri alınamayacağı bilinerek verilir.

## Özet

- Altı yüz isteklik iş yükü yalnız 53 farklı yanıt üretti; 547 sorgu daha önce üretilmiş bir
  sonucu yeniden üretti. Önbelleklemenin fırsatı sorguyu hızlandırmak değil, çalıştırmamaktır.
- Yanıt dört noktada saklanabilir: istemci, kenar, sunucu ve veritabanı önbelleği. İlk üçü
  sorgu sayısını düşürür; veritabanı önbelleği yalnız her sorgunun maliyetini düşürür.
- Zincir ölçüldüğünde kaynağa ulaşan istek 600'den 53'e indi; katmanların payı kapasitelerine
  göre dağıldı. Katmanlar birbirinin yerine geçmez, artığını toplar.
- Paylaşılan katmanlara yalnız bütün kullanıcılara aynı dönen veri konur; kişiye özel veri
  paylaşılan bir önbellekte yetkilendirme sınırını deler.
- Katman kullanıcıya yaklaştıkça ucuzlar ve denetlenemez hâle gelir; istemcideki kopya,
  konarken verilen süre dolana kadar geri alınamaz.

## Sonraki Adım

Bu derste önbellek, okuma yolunun içine elle yerleştirildi: bak, yoksa kaynaktan getir, koy.
Bu yazımın adı vardır ve tek seçenek değildir. Okuma yolunun yanında duran bir önbellek ile
bütün okumaların üzerinden geçtiği bir önbellek farklı sorumluluk dağılımları kurar; yazma
yolunda ise önbelleğin ne zaman güncelleneceği sorusu ortaya çıkar — yazma anında mı, sonra
mı. Sonraki ders bu üç stratejiyi aynı iş yükü üzerinde koşturur ve kaynağa giden okuma ve
yazma sayılarıyla tutarsızlık penceresini karşılaştırır.
