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.
Thundering herd — lock, XFetch, SWR
lock · probabilisticSenior 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.
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.
Invalidatsiya race va versiyalash
cache-aside race · CASCore’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.
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);
}
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.
L1 + L2 kesh va L1 invalidatsiya
near cache · pub/subRedis ~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;
}
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+ 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.
Redis ichki tuzilishi va to‘g‘ri ishlatish
single-thread · SCAN · structuresRedis’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:
| Struktura | Qachon |
|---|---|
| String | Oddiy qiymat, JSON, hisoblagich (INCR) |
| Hash | Obyekt maydonlari (user:1 → name, email) — qisman yangilash |
| Sorted Set | Reyting, leaderboard, vaqt bo‘yicha navbat |
| Set | Noyob to‘plam, a’zolik tekshiruvi |
| HyperLogLog | Noyob 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)
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).
Sharding, consistent hashing, hot key
Redis Cluster · 16384 slotBitta 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.
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.
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).
Warming, Bloom filter, TTL strategiyasi
cache warming · bloomSenior 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.
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
| Metrika | Nega muhim |
|---|---|
| Hit rate (hit / (hit+miss)) | Keshning asosiy foydasi. Past bo‘lsa (<80%) — kesh ishlamayapti, TTL/kalit noto‘g‘ri |
| Eviction rate | Yuqori bo‘lsa — xotira yetmayapti, kalitlar erta chiqib ketyapti |
| Memory usage | maxmemory’ga yaqinlashsa — sig‘im rejalashtirish kerak |
| Latency p99 | Sekin buyruqlar (O(N), katta key) bormi |
| Hot keys / big keys | Bitta 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).
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.
Lab, concurrency test, senior cheklist
testcontainersLab’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 / amaliyot | Bo‘lim |
|---|---|
| Stampede’ni lock, probabilistic expiration yoki stale-while-revalidate bilan hal qilaman | 1 |
| Cache-aside race’ini bilaman; versiya/CAS yoki write-through bilan oldini olaman | 2 |
| L1+L2 multi-level kesh quraman; L1’ni pub/sub bilan izchil saqlayman | 3 |
| Redis single-threaded ekanini bilaman; KEYS o‘rniga SCAN; to‘g‘ri struktura tanlayman | 4 |
| Consistent hashing/Redis Cluster va hot key muammosini hal qilaman | 5 |
| Cache warming, Bloom filter, sliding/absolute TTL’ni o‘rinli ishlataman | 6 |
| Hit rate, eviction, hot key’larni o‘lchayman va alert qo‘yaman | 7 |
| Keshni concurrency ostida testlayman; keshga majburiy tayanmayman | 8 |
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.