Tizim dizayni kursi · 4-mavzu · DEEP / SENIOR
TuzdiDinMuhammad

Caching — Senior chuqurlik

Bu — 4-mavzuning 2-qatlami. Core darslikni o‘qib bo‘lgan deb hisoblanadi. Bu yerda eng nozik va eng qimmatli narsalar: stampede’ga real yechimlar, race conditionlar, multi-level kesh, Redis ichki tuzilishi, consistent hashing, hot key va operatsiya — junior’dan seniorgacha.

Daraja: SeniorMavzular: Lock · XFetch · Redis Cluster · CASOld shart: 4-mavzu (Core)

Bu qatlamda nima bor

Core darslik asoslar va eng muhim muammolarni berdi. Bu Deep qatlam senior darajadagi chuqur mexanikani ochadi: stampede yechimlari, invalidatsiya race’lari, multi-level kesh, Redis ichki ishlashi, taqsimlangan kesh va consistent hashing, operatsion kuzatuv va concurrency testi. Oxirida — senior tayyorlik cheklisti.

01
Deep · Stampede

Thundering herd — lock, XFetch, SWR

lock · probabilistic
🐃

Senior haqiqat: Core’da stampede’ni "lock ishlat" deb o‘tdik. Lekin uchta turli yechim bor, har biri turli stsenariyga. To‘g‘risini tanlash — senior qarori.

Eslatma — muammo

Mashhur kalit TTL tugab o‘chdi. O‘sha lahzada 5000 so‘rov birato‘la miss bo‘lib, hammasi bazaga yuguradi — baza cho‘kadi (thundering herd).

Yechim 1 · Lock / mutex (eng aniq)

Faqat bitta so‘rov qulfni egallab bazaga boradi va keshni to‘ldiradi; qolganlar qisqa kutib, keshdan oladi. Redis SET NX PX bilan taqsimlangan qulf:

// STAMPEDE himoyasi: faqat BITTA so'rov bazaga boradi, qolganlar kutadi
const RELEASE = `if redis.call("get",KEYS[1])==ARGV[1]
                 then return redis.call("del",KEYS[1]) else return 0 end`;

async getWithLock<T>(key: string, ttlMs: number, loader: () => Promise<T>): Promise<T> {
  const hit = await this.redis.get(key);
  if (hit) return JSON.parse(hit);                          // HIT

  const lockKey = `lock:${key}`, token = randomUUID();
  // SET ... NX = faqat qulf bo'sh bo'lsa egalla (atomik)
  const locked = await this.redis.set(lockKey, token, 'PX', 5000, 'NX');
  if (!locked) {                                            // boshqa so'rov tiklamoqda
    await sleep(50);
    return this.getWithLock(key, ttlMs, loader);            // kutib qayta urin
  }
  try {
    const value = await loader();                           // FAQAT shu so'rov DB'ga
    await this.redis.set(key, JSON.stringify(value), 'PX', ttlMs);
    return value;
  } finally {
    await this.redis.eval(RELEASE, 1, lockKey, token);      // qulfni atomik bo'shat
  }
}

Yechim 2 · Probabilistic early expiration (eng silliq)

Lock’siz yondashuv: qiymat muddat tugashidan oldin, ehtimollik bilan bitta so‘rov tomonidan fonda yangilanadi. Shunday qilib hech qachon "hamma birdan miss" holati bo‘lmaydi:

// PROBABILISTIC EARLY EXPIRATION (XFetch): muddat tugashidan OLDIN, ehtimollik
// bilan bitta so'rov fonda qayta hisoblaydi -> hammasi bir vaqtda miss bo'lmaydi.
function shouldRecompute(delta: number, expiry: number, beta = 1.0): boolean {
  // delta = qiymatni hisoblash qancha vaqt olgani (ms)
  return Date.now() - delta * beta * Math.log(Math.random()) >= expiry;
}
// HIT bo'lsa ham shouldRecompute() true qaytarsa -> fonda yangilaymiz (stale-while-revalidate)

Yechim 3 · Stale-while-revalidate

Muddat tugasa ham, eski qiymatni darhol qaytaramiz (foydalanuvchi kutmaydi), fonda esa yangisini hisoblab keshni yangilaymiz. Tezlik birinchi, izchillik biroz keyin.

so‘rovso‘rovso‘rov qulf: 1 ta Baza (1 ta so‘rov) qolganlari kutadi / eski qiymat oladi
Faqat bitta so‘rov bazaga boradi — baza himoyalanadi
02
Deep · Consistency

Invalidatsiya race va versiyalash

cache-aside race · CAS
🏁

Core’da invalidatsiyaning nimaligini ko‘rdik. Senior darajada esa uning eng nozik tomoni — race condition: ikki amal bir vaqtda ishlaganda kesh "abadiy eski" qolib qolishi mumkin.

Mashhur cache-aside race’i

O‘qish (miss) bazadan eski qiymatni oldi, lekin keshga yozishdan oldin yozish amali bazani yangilab, keshni o‘chirdi. Keyin o‘qish eski qiymatni keshga yozadi — va u TTL tugaguncha (yoki abadiy) o‘sha eskiligicha qoladi.

// CACHE-ASIDE NING MASHHUR RACE'i (Facebook "scaling memcache" muammosi)
//
// T1 (o'qish): miss -> bazadan ESKI qiymat o'qidi (X=1), hali keshga yozmadi
// T2 (yozish): bazaga YANGI yozdi (X=2) -> cache.del(key) qildi
// T1: endi cache.set(key, X=1) qiladi -> KESH ESKI QIYMATDA QOLDI (stale, abadiy!)
//
// Yechimlar (kuchayuvchi tartibda):
// 1) Qisqa TTL  -> stale faqat TTL davomida (eng oddiy "xavfsizlik to'ri")
// 2) Write-through -> yozuvda keshni del emas, YANGISINI yoz
// 3) Versiya/CAS -> keshga versiya bilan yoz; eski versiyani yozishni RAD et
// 4) Lease (memcache) -> miss'da "lease token" beriladi; faqat shu token to'ldira oladi

Versiya/CAS bilan to‘g‘ri yechim

Eng ishonchli — har yozuvga versiya berib, keshga faqat yangiroq versiyani yozish (eski kelib qolsa — rad etish). Atomiklik uchun Redis Lua:

// VERSIYA bilan himoya: har yozuvda version oshadi; eski versiyani yozma
async setIfNewer(key: string, value: any, version: number) {
  const LUA = `
    local cur = redis.call("HGET", KEYS[1], "v")
    if cur and tonumber(cur) >= tonumber(ARGV[2]) then return 0 end
    redis.call("HSET", KEYS[1], "v", ARGV[2], "data", ARGV[1])
    return 1`;
  return this.redis.eval(LUA, 1, key, JSON.stringify(value), version);
}
"Source of truth" qarori

Kesh — baza haqiqat manbai bo‘lgan yordamchi qatlammi (odatiy), yoki kesh o‘zi haqiqat manbaimi (write-behind, masalan ko‘rishlar soni)? Bu qaror invalidatsiya strategiyangizni belgilaydi. Aksariyat hollarda: baza — haqiqat, kesh — nusxa, shuning uchun qisqa TTL har doim "xavfsizlik to‘ri" sifatida qoldiriladi.

03
Deep · Multi-level

L1 + L2 kesh va L1 invalidatsiya

near cache · pub/sub
🪜

Redis ~1ms — tez, lekin baribir tarmoq. Eng issiq ma’lumotni ilova xotirasida (L1, ~nanosekund) saqlasa, Redis’ga ham bormaymiz. Ikki qatlamli kesh — senior masshtab quroli.

L1 (lokal) + L2 (Redis)

L1 — server jarayoni ichida (eng tez, lekin kichik va har nusxada alohida). L2 — Redis (umumiy, izchil). So‘rov avval L1, keyin L2, keyin manbaga:

// L1 = lokal (in-memory LRU, ~ns) + L2 = Redis (umumiy, ~1ms)
async get<T>(key: string, loader: () => Promise<T>): Promise<T> {
  const l1 = this.local.get(key);            // L1
  if (l1 !== undefined) return l1;           // eng tez

  const l2 = await this.redis.get(key);      // L2
  if (l2) { const v = JSON.parse(l2); this.local.set(key, v); return v; }

  const v = await loader();                  // MISS -> manba
  await this.redis.set(key, JSON.stringify(v), 'PX', 60_000);
  this.local.set(key, v);
  return v;
}
So‘rov L1 lokal~ns L2 Redis~1ms Manba (DB)
L1 → L2 → manba: har qatlam keyingisiga yetishni kamaytiradi
L1 ning izchillik muammosi va yechimi

Har server nusxasida L1 alohida — biri yangilansa, boshqalarda eski qoladi. Yechim: Redis pub/sub bilan barcha nusxalarga "bu kalitni L1’dan o‘chir" signalini tarqatish:

// L1 muammosi: har server nusxasida ALOHIDA L1. Biri yangilansa, boshqalarda eski.
// Yechim: Redis pub/sub orqali HAMMA nusxaga "bu kalitni L1'dan o'chir" signali.
async invalidate(key: string) {
  await this.redis.del(key);                       // L2
  await this.redis.publish('cache:evict', key);    // hamma nusxaga e'lon
}
// har nusxa ishga tushganda:
this.sub.subscribe('cache:evict');
this.sub.on('message', (_ch, key) => this.local.del(key));   // lokal L1'ni tozala
Redis 6 client-side caching

Redis 6+ buni o‘zi qo‘llab-quvvatlaydi: client-side caching (tracking) — Redis kalit o‘zgarganda klientga invalidatsiya push yuboradi (RESP3). Bu pub/sub’ni qo‘lda yozishdan ko‘ra yetukroq variant.

04
Deep · Redis

Redis ichki tuzilishi va to‘g‘ri ishlatish

single-thread · SCAN · structures
🧠

Redis’ni "qora quti" deb ishlatish — middle daraja. Senior uning ichki ishlashini biladi — chunki bitta noto‘g‘ri buyruq butun tizimni to‘xtatishi mumkin.

1 · Single-threaded — eng muhim fakt

Redis buyruqlarni bitta oqimda, ketma-ket bajaradi. Demak O(N) buyruq (masalan KEYS *, katta SMEMBERS) butun Redis’ni bloklaydi — boshqa hamma so‘rov kutadi. Shuning uchun productionda hech qachon KEYS ishlatmaysan, SCAN ishlatasan:

// ❌ YOMON: KEYS butun Redis'ni BLOKLAYDI (single-thread!) - productionda halokat
// const keys = await redis.keys('product:*');

// ✅ YAXSHI: SCAN kursor bilan, bloklamasdan bo'lib-bo'lib
let cursor = '0';
do {
  const [next, batch] = await redis.scan(cursor, 'MATCH', 'product:*', 'COUNT', 100);
  cursor = next;
  for (const k of batch) { /* ... */ }
} while (cursor !== '0');

2 · To‘g‘ri ma’lumot strukturasini tanlash

Redis — shunchaki "key-value" emas. To‘g‘ri struktura xotira va tezlikni keskin o‘zgartiradi:

StrukturaQachon
StringOddiy qiymat, JSON, hisoblagich (INCR)
HashObyekt maydonlari (user:1 → name, email) — qisman yangilash
Sorted SetReyting, leaderboard, vaqt bo‘yicha navbat
SetNoyob to‘plam, a’zolik tekshiruvi
HyperLogLogNoyob sanash (unique visitors) — juda kam xotira

3 · Persistence va eviction — kesh rejimi

Toza kesh uchun Redis’da persistence (RDB/AOF) ko‘pincha o‘chiriladi (tezroq), eviction esa yoqiladi:

# redis.conf — Redis'ni KESH sifatida ishlatish (durable store emas)
maxmemory 2gb
maxmemory-policy allkeys-lru   # to'lganda eng kam ishlatilganni chiqar
# eviction siyosatlari: noeviction | allkeys-lru | allkeys-lfu
#                       volatile-lru | volatile-ttl | allkeys-random
save ""          # RDB snapshot OFF (kesh uchun kerak emas)
appendonly no    # AOF OFF -> tezroq, lekin restartda kesh bo'sh (normal)
Pipelining va atomiklik

Ko‘p buyruqni bittada yuborish uchun pipelining (tarmoq aylanishlarini kamaytiradi). Bir nechta amalni atomik qilish uchun MULTI/EXEC yoki Lua skript (yuqoridagi qulf/versiya misollaridagidek).

05
Deep · Distributed

Sharding, consistent hashing, hot key

Redis Cluster · 16384 slot
🔗

Bitta Redis yetmay qolsa yoki ishonchlilik kerak bo‘lsa — kesh bir necha tugunga taqsimlanadi. Bu yerda consistent hashing va hot key muammosi paydo bo‘ladi.

Sharding va consistent hashing

Kalitlarni tugunlarga taqsimlashning sodda yo‘li — hash(key) % N. Lekin tugun qo‘shilsa/o‘chsa, N o‘zgaradi va deyarli hamma kalit boshqa tugunga ko‘chadi (ommaviy miss). Consistent hashing buni hal qiladi: tugun o‘zgarganda faqat kichik qismi ko‘chadi.

Node A Node B Node C Node D key→ Tugun qo‘shilsa — faqat qo‘shni segmentdagi kalitlar ko‘chadi, hammasi emas (ommaviy miss yo‘q)
Consistent hashing: tugun o‘zgarsa, kalitlarning faqat kichik qismi ko‘chadi

Redis Cluster buni 16384 ta hash slot orqali qiladi: har kalit CRC16(key) % 16384 bo‘yicha slotga, slotlar esa tugunlarga taqsimlanadi. Ishonchlilik uchun har tugunning replikasi + avtomatik failover bo‘ladi.

Hot key muammosi

Bitta juda mashhur kalit (masalan, "Black Friday" bosh banneri) bitta slotga/tugunga tushadi va o‘sha tugunni yuklab tashlaydi — qolgan tugunlar bo‘sh tursa ham. Yechimlar: o‘sha hot key’ni L1 lokal keshga ham qo‘yish (Redis’ga umuman bormaslik), yoki kalitni bir necha nusxaga bo‘lish (banner:1, banner:2 … tasodifiy o‘qish).

06
Deep · Patterns

Warming, Bloom filter, TTL strategiyasi

cache warming · bloom

Senior darajada ishlatiladigan qo‘shimcha naqshlar:

1 · Cache warming (sovuq keshni isitish)

Deploy yoki Redis restartdan keyin kesh bo‘sh bo‘ladi — birinchi foydalanuvchilar to‘lqini bazani uradi (stampede’ning bir ko‘rinishi). Yechim: eng muhim kalitlarni oldindan to‘ldirish:

// CACHE WARMING: deploy yoki restart'dan keyin "sovuq kesh" muammosi.
// Eng muhim kalitlarni oldindan to'ldiramiz (foydalanuvchidan oldin).
@Cron('0 */6 * * *')               // har 6 soatda
async warmTopProducts() {
  const top = await this.repo.findTop(100);    // eng mashhur 100 mahsulot
  await Promise.all(top.map((p) =>
    this.redis.set(`product:${p.id}`, JSON.stringify(p), 'PX', 3_600_000),
  ));
}

2 · Bloom filter — penetration’ning yetuk yechimi

Core’da "negative caching" ko‘rdik. Undan yetukroq — Bloom filter: mavjud bo‘lmagan kalitni Redis’ga ham bormasdan rad etish (juda kam xotira):

// PENETRATION'ga qarshi Bloom filter: "bu ID umuman bormi?" ni Redis'ga
// bormasdan, juda tez va kam xotira bilan tekshiradi (RedisBloom moduli)
await redis.call('BF.ADD', 'products:exist', productId);   // mavjudlarni qo'shamiz
const maybe = await redis.call('BF.EXISTS', 'products:exist', wantedId);
if (maybe === 0) return null;   // ANIQ yo'q -> bazaga ham, keshga ham bormaymiz
// (Bloom filter "yo'q" desa 100% yo'q; "bor" desa ehtimol bor -> keyin tekshiriladi)

3 · TTL strategiyasi — sliding vs absolute

Absolute TTL: yozilgandan N vaqt o‘tib o‘chadi (har doim yangilanadi). Sliding TTL: har o‘qishda muddat uzayadi (mashhur kalitlar uzoq yashaydi, sessiyalar uchun ideal). Ma’lumot turiga qarab har xil TTL tanlash — senior odat.

07
Deep · Operations

Metrikalar, diagnostika va alert

hit rate · slowlog
📊

"Keshni qo‘ydim, demak tez" — middle. Senior o‘lchaydi: kesh chinakam foyda berayaptimi, xotira yetadimi, hot key bormi?

Kuzatiladigan metrikalar

MetrikaNega muhim
Hit rate (hit / (hit+miss))Keshning asosiy foydasi. Past bo‘lsa (<80%) — kesh ishlamayapti, TTL/kalit noto‘g‘ri
Eviction rateYuqori bo‘lsa — xotira yetmayapti, kalitlar erta chiqib ketyapti
Memory usagemaxmemory’ga yaqinlashsa — sig‘im rejalashtirish kerak
Latency p99Sekin buyruqlar (O(N), katta key) bormi
Hot keys / big keysBitta kalit yukni egallayaptimi (redis-cli --bigkeys)

Diagnostika vositalari

INFO stats — hit/miss, eviction; SLOWLOG — sekin buyruqlar; --bigkeys — katta kalitlar; --hotkeys — issiq kalitlar; MEMORY USAGE key — kalit hajmi. MONITOR esa productionda ehtiyotkorlik bilan (har buyruqni ko‘rsatadi, sekinlashtiradi).

Alert qo‘yish

Avtomatik ogohlantirish: hit rate keskin tushsa, eviction ko‘tarilsa (xotira bosimi), latency p99 oshsa, yoki Redis memory maxmemory’ning 80%idan oshsa. Bular muammoni foydalanuvchidan oldin ushlaydi.

08
Deep · Lab

Lab, concurrency test, senior cheklist

testcontainers

Lab’ning yadrosi va senior tekshiruvi.

1 · Infratuzilma

# docker-compose.yml — lab uchun Redis
services:
  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    ports: ["6379:6379"]

2 · Eng muhim test — stampede himoyasi concurrency ostida

Senior belgisi: keshni nafaqat "hit/miss" balki parallel yuk ostida testlash. Quyidagi test lock chinakam ishlashini isbotlaydi:

// Testlar: hit/miss, invalidatsiya VA stampede himoyasini concurrency bilan
it('100 ta parallel miss -> bazaga FAQAT 1 marta boradi', async () => {
  const loader = jest.fn().mockResolvedValue({ id: '1', price: 100 });
  // 100 ta so'rovni BIR VAQTDA yuboramiz (sovuq kesh)
  await Promise.all(
    Array.from({ length: 100 }, () => cache.getWithLock('product:1', 60_000, loader)),
  );
  expect(loader).toHaveBeenCalledTimes(1);   // lock ishladi -> bazaga 1 marta!
});

3 · Senior tayyorlik cheklisti

Hammasiga "ha" desang — keshlash bo‘yicha senior darajadasan:

Savol / amaliyotBo‘lim
Stampede’ni lock, probabilistic expiration yoki stale-while-revalidate bilan hal qilaman1
Cache-aside race’ini bilaman; versiya/CAS yoki write-through bilan oldini olaman2
L1+L2 multi-level kesh quraman; L1’ni pub/sub bilan izchil saqlayman3
Redis single-threaded ekanini bilaman; KEYS o‘rniga SCAN; to‘g‘ri struktura tanlayman4
Consistent hashing/Redis Cluster va hot key muammosini hal qilaman5
Cache warming, Bloom filter, sliding/absolute TTL’ni o‘rinli ishlataman6
Hit rate, eviction, hot key’larni o‘lchayman va alert qo‘yaman7
Keshni concurrency ostida testlayman; keshga majburiy tayanmayman8

Bu 4-mavzuning Deep/Senior qatlami edi — Core bilan birga, keshlash endi junior’dan senior gacha to‘liq.

Keyingi mavzu: “5-mavzu: Load Balancing” — masshtablashning ikkinchi quroli. Core’dan boshlaymiz.