Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. Cache-aside
- 2.1 Naqsh
- 2.2 Boshqa naqshlar
- 3. TTL va jitter
- 4. Invalidatsiya: eskirgan kesh
- 4.1 Muammo
- 4.2 Tartib muhim
- 4.3 Avlod (versiya) kaliti
- 5. Cache stampede
- 5.1 Muammo
- 5.2 Qulf bilan
- 5.3 Stale-while-revalidate va erta yangilash
- 5.4 Kesh muammolarining nomlari
- 6. Negative caching va nimani keshlamaslik
- 6.1 "Yo'q" ham javob
- 6.2 Xotira
- 6.3 Nimani keshlamaslik
- 7. Loyiha qadami: vazifalar-api ga Redis kesh (holat 65, 1-qism)
- 7.1 O'rnatish va config
- 7.2 KeshliOmbor — decorator
- 7.3 Ulash: omborYarat
- 7.4 Testlar
- 7.5 O'lchov: foyda bormi?
- 7.6 Ishga tushirish va ogohlantirish
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): naqshni toping
- 2-mashq (o'rta): kesh kaliti
- 3-mashq (qiyin): stampede'ni takrorlang
- 10. Real ishda
- Xulosa
- Manbalar
Redis kesh naqshlari: cache-aside, TTL, invalidatsiya va cache stampede
Qisqacha: Kesh — qimmat natijaning tez nusxasi. Eng ko'p ishlatiladigan naqsh — cache-aside: avval Redis'dan so'raymiz, yo'q bo'lsa bazadan olib, Redis'ga TTL bilan qo'yamiz. Kanonda qimmat statistika so'rovi Postgres'da 9.5 ms, keshdan 1.5 ms bo'ldi. Lekin kesh bepul emas: yozuvdan keyin eskirgan javob qaytishi mumkin (invalidatsiya), kalit o'lgan paytda 100 so'rov birdan bazaga yugurishi mumkin (stampede: himoyasiz 100 so'rov, qulf bilan 1 ta). Va kesh har doim tezlashtirmaydi:
vazifalar-apiro'yxatida o'lchangan foyda yo'q (12.9 → 13.7 ms).
Bu darsda
- Cache-aside, write-through va write-behind naqshlarini farqlaysiz.
- TTL va jitter'ni to'g'ri tanlaysiz.
- Yozuvdan keyin keshni qanday yangilashni (invalidatsiya) va avlod kalitini tushuntirasiz.
- Cache stampede'ga qarshi qulf va stale-while-revalidate ishlatasiz.
vazifalar-apigaKeshliOmborqo'shasiz va uning foydasini halol o'lchaysiz.
Oldin bilishingiz kerak: String, TTL va kalit nomlash, Atomiklik: MULTI, Lua va pipelining, HTTP caching va shartli so'rovlar.
Oldingi darsda bir nechta buyruqni xavfsiz birga bajarishni o'rgandik. Endi Redis'ning eng mashhur vazifasiga o'tamiz — kesh. Hamma raqamlar — kanon o'lchovi (Postgres 18.6 katta bazasi: 500 000 vazifa; Redis 8.10.2, node-redis 6.3.0).
1. Nega bu kerak?
Bosh sahifada egasining statistikasi: jami vazifalar, bajarilganlar, teglar soni. Bu count(*) va JOIN — butun jadval bo'ylab. Kanon egasi 77 uchun o'lchadi:
Postgres to'g'ridan: 9.47 ms; kesh miss (DB + SET): 13.67 ms; kesh hit: 1.49 ms (mediana 20)
{"jami":401,"bajarilgan":234,"teglar":316}
TTL (jitter bilan): 62- Postgres to'g'ridan9,47 ms
- Kesh miss (DB + SET)13,67 ms
- Kesh hit1,49 ms
Manba: IlmHamroh kanon o'lchovi, 2026-10-25: Postgres 18.6 katta (500 000 vazifa), Redis 8.10.2, node-redis 6.3.0, mediana 20
Grafikka qarang: kesh hit (topildi) 6 baravar tez. Lekin miss (topilmadi) bazadan ham sekin — bazaga ham, Redis'ga ham borildi. Kesh foydasi shuncha: hit'lar ulushi qancha katta bo'lsa, shuncha yaxshi.
O'xshatish: talaba tez-tez so'raladigan formulani daftar chetiga yozib qo'yadi. Kitobni har safar ochish shart emas. Lekin o'qituvchi formulani tuzatsa va talaba chetdagini o'zgartirishni unutsa — xato javob beradi. Kesh shunday: tez, lekin eskirishi mumkin.
2. Cache-aside
2.1 Naqsh
Cache-aside (yonidagi kesh, "lazy loading") — ilova keshni o'zi boshqaradi:
- Redis'dan so'raydi.
- Bor bo'lsa (hit) — qaytaradi.
- Yo'q bo'lsa (miss) — bazadan oladi, Redis'ga TTL bilan yozadi, qaytaradi.
sequenceDiagram
participant A as API
participant R as Redis
participant P as Postgres
A->>R: GET kalit
alt hit
R-->>A: qiymat
else miss
R-->>A: null
A->>P: SELECT …
P-->>A: natija
A->>R: SET kalit natija EX 60
endKanon kodi:
async function keshliStatistika(egasi) {
const kalit = `t58:stat:${egasi}`;
const bor = await r.get(kalit);
if (bor !== null) return JSON.parse(bor);
const s = await statistika(egasi);
await r.set(kalit, JSON.stringify(s), {
EX: 60 + Math.floor(Math.random() * 10),
});
return s;
}Kesh faqat so'ralgan ma'lumotni saqlaydi — hech kim so'ramagan narsa xotirani egallamaydi. Redis o'chsa ham ilova ishlaydi (sekinroq). Shuning uchun cache-aside — sukut tanlov.
2.2 Boshqa naqshlar
| Naqsh | Qanday | Qachon |
|---|---|---|
| Cache-aside | o'qishda: miss → DB → kesh | ko'p hollarda |
| Write-through | yozishda: DB va keshga birga | o'qish yozuvdan ancha ko'p, yangilik muhim |
| Write-behind | avval keshga, DB'ga keyin fonda | juda ko'p yozuv (hisoblagichlar); yo'qotish xavfi bor |
| Read-through | kesh o'zi DB'dan oladi (kutubxona) | tayyor kesh qatlamlari |
Write-behind xavfli: Redis keskin o'chsa, hali bazaga yozilmagan o'zgarishlar yo'qoladi (Redis persistence da o'lchaymiz). Kanon bu naqshni sinamagan — u faqat yo'qotish qabul qilinadigan joyda (masalan ko'rishlar hisoblagichi) ishlatiladi.
3. TTL va jitter
TTL — keshning "yaroqlilik muddati". Ikki xil xato bor:
- TTL juda katta — ma'lumot uzoq eskirgan ko'rinadi.
- TTL juda kichik — hit kam, foyda yo'q.
Kanon 60 + tasodifiy 0…9 soniya berdi (TTL javobi — 62). Bu jitter (tasodifiy siljish). Nega kerak? Server ishga tushganda minglab kalit bir paytda keshlanadi. TTL hammasida aynan 60 bo'lsa, 60 soniyadan keyin hammasi bir paytda o'ladi — va hamma so'rov birdan bazaga ketadi. Jitter o'limlarni vaqt bo'ylab yoyadi.
Qoida: TTL'ni "foydalanuvchi qancha eskirishga chidaydi" savoliga qarab tanlang. Valyuta kursi — 1 soat, statistika — 1 daqiqa, foydalanuvchining o'z vazifalari — avlod kaliti bilan darhol yangilanadi (pastda).
4. Invalidatsiya: eskirgan kesh
4.1 Muammo
Kanon tranzaksiya ichida bitta vazifani o'chirdi va keshdan statistikani so'radi:
DB o'zgardi (tranzaksiyada): keshdan 401, DB'da 400 (oldin 401)Bazada 400, kesh esa 401 deydi. Foydalanuvchi vazifani o'chirdi, lekin sahifada eski son. Kesh invalidatsiyasi (cache invalidation) — ma'lumot o'zgarganda eski kesh nusxasini yaroqsiz qilish. Dasturlashdagi eng mashhur qiyin masalalardan biri.
4.2 Tartib muhim
Kanon xulosasi:
To'g'ri tartib: (1) DB COMMIT, (2) DEL kalit. Teskari tartib (avval DEL) — orada boshqa so'rov eski qiymatni qayta keshlaydi.Nega teskari tartib xato? A so'rov avval DEL qildi, keyin tranzaksiya davom etmoqda. Shu orada B so'rov keldi va keshni eski qiymat bilan qayta to'ldirdi. Vaqt bo'yicha:
| Qadam | A (vazifani o'chiradi) | B (statistikani o'qiydi) | Keshda |
|---|---|---|---|
| 1 | BEGIN, DELETE |
— | 401 |
| 2 | DEL kalit |
— | bo'sh |
| 3 | — | GET → miss |
bo'sh |
| 4 | — | bazadan o'qidi → 401 (A hali COMMIT qilmagan) | bo'sh |
| 5 | — | SET kalit 401 |
401 |
| 6 | COMMIT (bazada 400) |
— | 401 — eskirgan |
A COMMIT qildi, lekin keshda yana eski 401 — TTL tugaguncha. To'g'ri tartibda DEL 6-qadamdan keyin keladi va B qo'ygan eski qiymatni ham o'chiradi.
To'g'ri tartibda ham kichik oyna qoladi (COMMIT va DEL orasida). Lekin u millisekundlar, va oxirida kesh tozalanadi.
4.3 Avlod (versiya) kaliti
Kanonda yana bir muqobil taklif qilingan:
Versiyali kalit muqobili: kalit = t58:royxat:<egasi>:v<vazifalar_versiyasi> — yozuv versiyani oshiradi, eski kalit TTL bilan o'zi o'ladi (DEL kerak emas).G'oya: kalitni o'chirmaymiz — kalit nomini o'zgartiramiz. Kalitda "avlod" (generation) raqami bor. Har yozuv avlodni bittaga oshiradi (INCR). O'qish joriy avlod bilan kalit yasaydi — eski avlod kalitlari endi hech kim tomonidan o'qilmaydi va TTL bilan o'zi o'ladi.
Bitta foydalanuvchi (egasi 1) misolida so'rovlar oqimi:
| So'rov | Redis'da nima bo'ladi | Natija |
|---|---|---|
| ro'yxat | GET vz:1:avlod → yo'q (0); GET vz:1:a0:royxat:… → miss |
bazadan, keshga a0 kaliti |
| ro'yxat | avlod 0; a0 kaliti bor → hit |
keshdan |
| yangi vazifa | bazaga yozildi, keyin INCR vz:1:avlod → 1 |
eski a0 endi o'qilmaydi |
| ro'yxat | avlod 1; a1 kaliti yo'q → miss |
bazadan, yangi ro'yxat |
Avlod yozuvdan keyin (COMMIT'dan keyin) oshadi. Yuqoridagi jadvaldagi poyga bu yerda zarar qilmaydi: B eski qiymatni eski avlod kalitiga yozsa ham, avlod oshgach o'sha kalitni hech kim o'qimaydi.
Afzalliklari: bitta yozuvdan keyin ko'p kalitni (ro'yxatning hamma sahifalari, filtrlari) bitta INCR bilan "o'chirish" mumkin. SCAN + DEL kerak emas (String, TTL va kalit nomlash da KEYS xavfini ko'rdik). Narxi — har o'qishda bitta qo'shimcha Redis so'rovi (avlodni olish). vazifalar-api aynan shu naqshni ishlatadi.
Tekshirib ko'ring: Kimdir psql orqali to'g'ridan bazada vazifani o'zgartirdi. Avlod kaliti bilan kesh yangilanadimi?
Javob
Yo'q. Avlod faqat ilova orqali bo'lgan yozuvda oshadi. Tashqi yozuv (psql, boshqa xizmat) keshni bilmaydi — eski javob TTL tugaguncha ko'rinadi. Shuning uchun avlod kaliti bilan ham TTL qo'yiladi: u tashqi o'zgarishlar uchun "eng ko'p eskirish" chegarasi.
5. Cache stampede
5.1 Muammo
Mashhur kalit o'ldi. Shu soniyada 100 ta so'rov keldi. Hammasi miss ko'radi — hammasi bazaga boradi. Bu cache stampede ("podaning dupuri"). Kanon o'lchadi:
himoyasiz: 100 ta DB so'rovi, 187 ms
SET NX qulf bilan: 1 ta DB so'rovi, 38 ms
stale-while-revalidate (eskirgan qiymat bor): javoblar 8 ms ichida, fonda 1 ta DB so'roviHimoyasiz — bitta natija uchun 100 ta bir xil qimmat so'rov. Katta saytda bu bazani yiqitishi mumkin: kesh aynan eng og'ir paytda yordam bermaydi.
5.2 Qulf bilan
Faqat bitta so'rov hisoblaydi, qolganlari kutib, keshdan oladi. Qulf — String, TTL va kalit nomlash dagi SET … NX PX:
async function qulfliStatistika(egasi) {
const kalit = `t58:stat:${egasi}`;
for (let i = 0; i < 50; i++) {
const bor = await r.get(kalit);
if (bor !== null) return JSON.parse(bor);
const oldim = await r.set(`${kalit}:qulf`, "1", {
NX: true,
PX: 5000,
});
if (oldim) {
try {
const s = await statistika(egasi);
await r.set(kalit, JSON.stringify(s), { EX: 60 });
return s;
} finally {
await r.del(`${kalit}:qulf`);
}
}
await new Promise((res) => setTimeout(res, 20));
}
return statistika(egasi); // qulf egasi yiqilgan — oxirgi chora
}PX: 5000— qulf 5 soniyada o'zi ochiladi. Hisoblayotgan jarayon yiqilsa ham boshqalar abadiy kutmaydi.- Qolganlar 20 ms dan kutib, qayta tekshiradi. 50 urinishdan keyin (1 s) — bazaga o'zi boradi.
- Natija: 100 so'rovdan 1 tasi bazada, umumiy vaqt 187 → 38 ms.
Bu qulfdagi DEL ning xavfli tomoni bor (muddat tugab, boshqasi olgan qulfni o'chirish) — Distributed lock va rate limit darsida token bilan to'g'irlaymiz. Kesh uchun bu xavf kichik: eng yomon holda bazaga ikkita so'rov ketadi.
5.3 Stale-while-revalidate va erta yangilash
Stale-while-revalidate (eskisini ber, fonda yangila): keshda qiymat bilan birga "qachongacha yangi" vaqti saqlanadi. Muddat o'tgan bo'lsa ham eski qiymat darhol qaytadi, fonda esa bitta so'rov uni yangilaydi (yangilash ham SET NX qulfi bilan). Kanonda 100 javob 8 ms ichida, bazaga — 1 so'rov. Frontend'dagi xuddi shu nomli strategiya bilan bir g'oya (HTTP caching va shartli so'rovlar).
Uchinchi usul — erta yangilash (early recompute, "XFetch"): TTL tugashiga oz qolganda, so'rovlardan biri tasodifiy ehtimol bilan qiymatni oldindan yangilaydi. Kanonda sinalmagan — hujjat va maqolalarda uchraydi.
| Usul | Foydalanuvchi | Baza | Murakkablik |
|---|---|---|---|
| Himoyasiz | kutadi | N so'rov | yo'q |
| Qulf | kutadi (1 hisob) | 1 so'rov | o'rta |
| Stale-while-revalidate | darhol (eski) | 1 so'rov fonda | o'rta |
Tekshirib ko'ring: Qulf
PX: 5000bilan olindi, lekin statistika so'rovi 7 soniya ishladi. Nima bo'ladi?
Javob
5 soniyada qulf o'zi ochiladi. Kutayotgan so'rovlardan biri qulfni oladi va bazaga ikkinchi so'rovni yuboradi. Kesh uchun bu zararsiz — eng yomon holda bazaga 2 ta so'rov ketadi, 100 ta emas. Qulf muddati odatdagi hisob vaqtidan katta bo'lishi kerak. Muddat tugashining xavfli tomonlarini Distributed lock va rate limit darsida ko'ramiz.
5.4 Kesh muammolarining nomlari
Intervyu va maqolalarda uchta nom uchraydi. Uchalasi ham "baza kutilmaganda yuklanib qoldi" degani, faqat sababi har xil:
| Nomi | Nima bo'ladi | Davosi (shu darsda) |
|---|---|---|
| Stampede (breakdown) | bitta mashhur kalit o'ldi, hamma bazaga | qulf, stale-while-revalidate |
| Avalanche (ko'chki) | ko'p kalit bir paytda o'ldi yoki Redis o'chdi | jitter; Redis'siz ham ishlash |
| Penetration (teshib o'tish) | yo'q kalitlar so'raladi, kesh hech qachon to'lmaydi | negative caching, Bloom filtr |
Bloom filtrni Maxsus tuzilmalar darsida ko'rgansiz: 0 javobida bazaga umuman bormaymiz.
Keshning foydasini bitta son o'lchaydi — hit ratio (topilish ulushi): hit'lar soni / (hit + miss). Redis buni o'zi sanaydi: INFO stats dagi keyspace_hits va keyspace_misses. KeshliOmbor ham o'z hisob obyektida (hit, miss) sanaydi. Hit ratio past bo'lsa (masalan 20 %), kesh asosan miss narxini qo'shadi va foyda bermaydi.
6. Negative caching va nimani keshlamaslik
6.1 "Yo'q" ham javob
50 so'rov yo'q id bilan: DB'ga 1 marta bordi; TTL 30 sYo'q foydalanuvchi (id 999999) haqida so'rov keshlanmasa — har safar bazaga boradi. Hujumchi tasodifiy id'lar bilan bazani "bombardimon" qilishi mumkin. Negative caching — "topilmadi" javobini ham qisqa TTL bilan keshlash. Kanon "yo'q" ni maxsus satr bilan saqladi, TTL 30 s (bor qiymatlar 300 s). Qisqa TTL — keyin shu id bilan foydalanuvchi yaratilsa, kesh uzoq yolg'on gapirmasin.
6.2 Xotira
used_memory_human:4.23M | maxmemory_human:0B | maxmemory_policy:noeviction | bitta kalit: 85 bayt5 000 statistik kalit — 4.23 MB (Redis'ning o'z yuklamasi bilan), bitta kalit 85 bayt. maxmemory 0 — chegara yo'q, noeviction — xotira to'lsa yangi yozuvni rad etadi. Kesh uchun boshqa siyosat kerak (allkeys-lru) — Redis persistence va operatsiya da.
6.3 Nimani keshlamaslik
- Arzon so'rovni. Indeksli so'rov 1–2 ms — kesh qo'shimcha Redis aylanishi bilan yutuq bermaydi (pastdagi o'lchov).
- Shaxsiy va maxfiy ma'lumotni umumiy kalitda. Kalitga egasini qo'ymasangiz, Aziz Malikaning vazifalarini ko'radi.
- Pul, qoldiq, huquqlar — har doim bazadan; eskirgan qiymat bu yerda xato emas, zarar.
- Juda tez o'zgaradigan narsa — hit bo'lmaydi.
7. Loyiha qadami: vazifalar-api ga Redis kesh (holat 65, 1-qism)
Holat 65 — 27-qismning yakuniy holati. U ikki mavzuni birlashtiradi: Redis kesh (shu dars) va test bazasi bilan ishlash (Test DB strategiyalari dan Repository va SQL'ni testlash gacha). Bugun kesh qismini qo'shamiz. Hali commit qilmang — commit Repository va SQL'ni testlash darsida, hamma test tayyor bo'lgach.
7.1 O'rnatish va config
npm i --save-exact redis@6.3.0Prod bog'liqliklar 9 → 10 ta. src/config.ts dagi Zod sxemasiga ikki maydon:
/** Redis kesh (ixtiyoriy): berilsa postgres/prisma ombori
* ustiga KeshliOmbor. Lokal: redis://127.0.0.1:6380 */
REDIS_URL: z.url({
protocol: /^rediss?$/,
error: "REDIS_URL — redis://host:port",
}).optional(),
/** kesh yashash vaqti (s) — yozuvlar avlod bilan darhol
* ko'rinadi; TTL faqat eski kalitlarni tozalaydi va tashqi
* yozuvlar uchun chegara */
KESH_TTL: Butun(60).pipe(z.number().min(1).max(86_400)),REDIS_URL ixtiyoriy: berilmasa — kesh o'chiq, API avvalgidek ishlaydi. rediss? — redis:// yoki TLS bilan rediss://. Butun — Env validatsiyasi va tipli config da yozgan yordamchimiz (sukut 60, bo'sh qiymat — xato). Kanon izohlarini telefon uchun qatorlarga bo'ldik, ma'nosi o'zgarmadi.
test/config.test.ts ga:
test("config: REDIS_URL (ixtiyoriy, faqat redis://), "
+ "KESH_TTL (sukut 60, bo'sh — xato)", () => {
assert.equal(configYukla({}).REDIS_URL, undefined);
assert.equal(configYukla({}).KESH_TTL, 60);
assert.equal(
configYukla({ REDIS_URL: "redis://127.0.0.1:6380" }).REDIS_URL,
"redis://127.0.0.1:6380",
);
assert.throws(
() => configYukla({ REDIS_URL: "http://127.0.0.1:6380" }),
/at REDIS_URL/,
);
assert.throws(() => configYukla({ KESH_TTL: "" }), /at KESH_TTL/);
});7.2 KeshliOmbor — decorator
Asosiy g'oya: mavjud omborlarga (PgOmbori, PrismaOmbori) tegmaymiz. Yangi klass ham VazifaOmbori interfeysini amalga oshiradi va ichida boshqa omborni o'raydi. Bu decorator (o'rovchi) naqshi — dizayn naqshlarini kursning arxitektura qismida to'liq o'rganamiz. O'qishlar avval Redis'dan o'tadi, yozuvlar ichki omborga boradi va avlodni oshiradi. Shartnoma testlari (Backend test strategiyasi) uni xuddi boshqa omborlardek tekshiradi.
Yangi fayl src/vazifalar/kesh-ombor.ts (kanon kodi; uzun qatorlar telefon uchun bo'lingan, izohlar qisqartirilgan):
// kesh-ombor.ts — VazifaOmbori ustidan Redis keshi (decorator):
// ichki ombor (pg/prisma) o'zgarmaydi. Naqsh: cache-aside + AVLOD
// kaliti. O'qish kaliti: vz:<egasi>:a<avlod>:<amal>:<so'rov hash'i>.
// Har muvaffaqiyatli yozuv avlodni INCR qiladi — eski kalitlar
// o'qilmay qoladi va TTL bilan o'ladi (KEYS/SCAN kerak emas).
// Narxi: har o'qishda +1 Redis so'rovi (avlodni olish).
// Redis ishlamasa — kesh chetlab o'tiladi, xato log'ga.
import { createHash } from "node:crypto";
import type { RedisClientType } from "redis";
import type {
Vazifa, VazifaOzgarishi, YangiVazifa,
} from "./vazifa.ts";
import type {
AlmashtirishVazifasi, Filtr, RoyxatNatija, RoyxatSorovi, Shart,
TegSoni, VazifaOmbori, YozishNatijasi,
} from "./ombor.ts";
export type KeshSozlamasi = {
ttlS: number;
log?: { warn: (obj: object, xabar: string) => void };
};
type Redis = Pick<RedisClientType, "get" | "set" | "incr">;
/** JSON.stringify(undefined) → undefined — "topilmadi" ham
* keshlansin: "null" */
function json(x: unknown): string {
const s: unknown = JSON.stringify(x);
return typeof s === "string" ? s : "null";
}
export class KeshliOmbor implements VazifaOmbori {
readonly #ichki: VazifaOmbori;
readonly #redis: Redis;
readonly #egasi: number;
readonly #s: KeshSozlamasi;
/** statistika (test va o'lchov uchun) */
readonly hisob = { hit: 0, miss: 0, xato: 0 };
constructor(
ichki: VazifaOmbori,
redis: Redis,
egasiId: number,
sozlama: KeshSozlamasi,
) {
this.#ichki = ichki;
this.#redis = redis;
this.#egasi = egasiId;
this.#s = sozlama;
}
async #keshdan<T>(
amal: string,
args: unknown,
hisobla: () => Promise<T>,
): Promise<T> {
let kalit: string;
try {
const avlod =
(await this.#redis.get(`vz:${String(this.#egasi)}:avlod`))
?? "0";
kalit = `vz:${String(this.#egasi)}:a${avlod}:${amal}:${
createHash("sha1").update(json(args)).digest("base64url")}`;
const bor = await this.#redis.get(kalit);
if (bor !== null) {
this.hisob.hit++;
return JSON.parse(bor) as T;
}
} catch (x) {
this.hisob.xato++;
this.#s.log?.warn({ err: x },
"kesh: Redis xatosi — to'g'ridan ombordan");
return hisobla();
}
this.hisob.miss++;
const qiymat = await hisobla();
// undefined (topilmadi) ham keshlanadi: negative caching
const ttl = this.#s.ttlS
+ Math.floor(Math.random() * Math.max(1, this.#s.ttlS / 10));
// jitter — hamma kalit bir paytda o'lmasin
await this.#redis.set(kalit, json(qiymat), { EX: ttl })
.catch((x: unknown) => {
this.hisob.xato++;
this.#s.log?.warn({ err: x }, "kesh: yozib bo'lmadi");
});
return qiymat;
}
async #yozdi<T>(natija: T): Promise<T> {
await this.#redis.incr(`vz:${String(this.#egasi)}:avlod`)
.catch((x: unknown) => {
// avlod oshmasa — eski kesh TTL tugaguncha ko'rinadi
this.hisob.xato++;
this.#s.log?.warn({ err: x },
"kesh: avlodni oshirib bo'lmadi — "
+ "TTL gacha eskirgan javob mumkin");
});
return natija;
}
royxat(sorov: RoyxatSorovi): Promise<RoyxatNatija> {
return this.#keshdan("royxat", sorov,
() => this.#ichki.royxat(sorov));
}
soni(filtr: Filtr): Promise<number> {
return this.#keshdan("soni", filtr,
() => this.#ichki.soni(filtr));
}
async ol(id: number): Promise<Vazifa | undefined> {
return (await this.#keshdan("ol", id, () => this.#ichki.ol(id)))
?? undefined;
}
teglar(): Promise<TegSoni[]> {
return this.#keshdan("teglar", null, () => this.#ichki.teglar());
}
async yarat(yangi: YangiVazifa): Promise<Vazifa> {
return this.#yozdi(await this.#ichki.yarat(yangi));
}
async yangila(
id: number, o: VazifaOzgarishi, shart?: Shart,
): Promise<YozishNatijasi<Vazifa>> {
const n = await this.#ichki.yangila(id, o, shart);
return n.tur === "ok" ? this.#yozdi(n) : n;
}
async ochir(
id: number, shart?: Shart,
): Promise<YozishNatijasi<null>> {
const n = await this.#ichki.ochir(id, shart);
return n.tur === "ok" ? this.#yozdi(n) : n;
}
async almashtirHammasi(
v: readonly AlmashtirishVazifasi[],
): Promise<{ vazifalar: Vazifa[]; versiya: number }> {
return this.#yozdi(await this.#ichki.almashtirHammasi(v));
}
}Muhim qarorlar:
- Kalit —
vz:<egasi>:a<avlod>:<amal>:<hash>. So'rov parametrlari (filtr, saralash, kursor) JSON qilinib, SHA-1 bilan qisqa satrga aylanadi. Har egasining o'z kalitlari — boshqa odamning keshi ko'rinmaydi. - Faqat muvaffaqiyatli yozuv avlodni oshiradi.
konflikt(412) yokitopilmadi(404) — hech narsa o'zgarmadi, kesh behuda tozalanmaydi. - Redis xatosi — o'qishda ombordan to'g'ridan; yozishda — faqat log. Kesh ixtiyoriy qatlam: u ishlamasa API to'xtamasligi kerak.
- Negative caching —
ol(42)ningundefinedjavobi"null"bo'lib keshlanadi.
7.3 Ulash: omborYarat
src/ishga-tushir.ts da importlar:
import { createClient } from "redis";
import { KeshliOmbor } from "./vazifalar/kesh-ombor.ts";omborYarat endi ikki qadamli: avval asosiy ombor (avvalgi switch — asosiyOmbor nomli funksiyaga ko'chdi, ichi o'zgarmadi), keyin ixtiyoriy kesh:
export async function omborYarat(
config: Pick<Config, "SAQLASH" | "FAYL_YOLI" | "DATABASE_URL"
| "EGASI_ID" | "PG_POOL_MAX" | "PG_STATEMENT_TIMEOUT">
& Partial<Pick<Config, "NODE_ENV" | "REDIS_URL" | "KESH_TTL">>,
log?: OmborLogi,
): Promise<Saqlash> {
const s = await asosiyOmbor(config, log);
if (config.REDIS_URL === undefined || config.SAQLASH === "xotira"
|| config.SAQLASH === "fayl") return s;
// disableOfflineQueue: ulanish yo'q paytda buyruq navbatda
// kutmasin — darhol xato, KeshliOmbor ombordan oladi
const redis = createClient({
url: config.REDIS_URL,
disableOfflineQueue: true,
socket: {
connectTimeout: 2_000,
reconnectStrategy: (n) => Math.min(n * 200, 5_000),
},
});
redis.on("error", (err) => {
log?.error({ err }, "redis: ulanish xatosi");
});
try {
await redis.connect();
} catch (x) {
await s.yop();
throw x;
}
const ombor = new KeshliOmbor(s.ombor, redis, config.EGASI_ID, {
ttlS: config.KESH_TTL ?? 60,
...(log?.warn === undefined ? {} : { log: { warn: log.warn } }),
});
return {
ombor,
yop: async () => { redis.destroy(); await s.yop(); },
};
}
async function asosiyOmbor(
config: Pick<Config, "SAQLASH" | "FAYL_YOLI" | "DATABASE_URL"
| "EGASI_ID" | "PG_POOL_MAX" | "PG_STATEMENT_TIMEOUT">
& Partial<Pick<Config, "NODE_ENV">>,
log?: OmborLogi,
): Promise<Saqlash> {
// … avvalgi omborYarat'ning switch qismi, o'zgarishsiz
}Xotira va fayl omborlari keshlanmaydi — ular o'zi xotirada. Ishga tushishda Redis'ga ulanib bo'lmasa — server ishga tushmaydi (fail-fast, pool ham yopiladi). Ishlab turgan paytda uzilsa — reconnectStrategy qayta ulanadi, shu orada so'rovlar keshsiz o'tadi. Server to'xtaganda yop() ham Redis, ham pool'ni yopadi.
7.4 Testlar
test/kesh-ombor.test.ts — Redis bilan (TEST_REDIS_URL, sukut redis://127.0.0.1:6380). Redis yo'q kompyuterda — SKIP, yiqilmaydi. Kanon kodi; telefon uchun qatorlar bo'lingan, izohlar va test nomlari qisqartirilgan:
// kesh-ombor.test.ts — KeshliOmbor: SHARTNOMA testlari, hit/miss,
// yozuvdan keyin eskirgan javob yo'qligi, Redis o'chsa ham ishlashi.
// Redis: TEST_REDIS_URL; har test o'z egasi id'sida.
// Redis yo'q — SKIP.
import { after, describe, test } from "node:test";
import assert from "node:assert/strict";
import { createClient } from "redis";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { KeshliOmbor } from "../src/vazifalar/kesh-ombor.ts";
import { omborShartnomasi } from "./ombor-shartnoma.ts";
import type { RoyxatSorovi } from "../src/vazifalar/ombor.ts";
/** royxat() necha marta ichki omborga yetib kelganini sanaydi */
class SanovchiOmbor extends XotiraOmbori {
chaqiruv = 0;
override royxat(
s: RoyxatSorovi,
): ReturnType<XotiraOmbori["royxat"]> {
this.chaqiruv++;
return super.royxat(s);
}
}
const URL = process.env["TEST_REDIS_URL"] ?? "redis://127.0.0.1:6380";
const redis = createClient({
url: URL,
disableOfflineQueue: true,
socket: { connectTimeout: 1000, reconnectStrategy: false },
});
redis.on("error", () => {});
const bor = await redis.connect().then(() => true, () => false);
if (!bor) {
test("KeshliOmbor", { skip: `Redis yo'q (${URL})` }, () => {});
} else {
// test egalari 9e6 dan — ilova kalitlariga aralashmaydi
let egasi = 9_000_000 + Math.floor(Math.random() * 100_000);
after(async () => {
for await (const k of redis.scanIterator({
MATCH: "vz:9*", COUNT: 500,
})) if (k.length > 0) await redis.del(k);
redis.destroy();
});
const hammasi: RoyxatSorovi = {
filtr: {}, saralash: [], limit: 10, sahifa: { tur: "kursor" },
};
omborShartnomasi("xotira + Redis kesh", () => Promise.resolve(
new KeshliOmbor(new XotiraOmbori(), redis, ++egasi,
{ ttlS: 60 })));
describe("KeshliOmbor: kesh xulqi", () => {
test("ikkinchi o'qish keshdan; yozuvdan keyin yangi natija",
async () => {
const sanovchi = new SanovchiOmbor();
const o = new KeshliOmbor(sanovchi, redis, ++egasi,
{ ttlS: 60 });
await o.yarat({ matn: "Non" });
assert.equal((await o.royxat(hammasi)).vazifalar.length, 1);
assert.equal((await o.royxat(hammasi)).vazifalar.length, 1);
assert.equal(sanovchi.chaqiruv, 1);
assert.deepEqual(o.hisob, { hit: 1, miss: 1, xato: 0 });
await o.yarat({ matn: "Sut" });
// avlod oshdi — eski kesh ishlatilmadi
assert.equal((await o.royxat(hammasi)).vazifalar.length, 2);
assert.equal(sanovchi.chaqiruv, 2);
});
test("konflikt/topilmadi — avlod OSHMAYDI; undefined keshda",
async () => {
const o = new KeshliOmbor(new XotiraOmbori(), redis, ++egasi,
{ ttlS: 60 });
await o.yarat({ matn: "a" });
const avlod = await redis.get(`vz:${String(egasi)}:avlod`);
assert.equal(
(await o.yangila(1, { matn: "b" }, { versiya: 99 })).tur,
"konflikt");
assert.equal((await o.ochir(42)).tur, "topilmadi");
assert.equal(await redis.get(`vz:${String(egasi)}:avlod`),
avlod);
assert.equal(await o.ol(42), undefined);
assert.equal(await o.ol(42), undefined);
assert.equal(o.hisob.hit, 1);
});
test("TTL + jitter: kalit muddati 60–65 s", async () => {
const o = new KeshliOmbor(new XotiraOmbori(), redis, ++egasi,
{ ttlS: 60 });
await o.teglar();
const kalitlar: string[] = [];
for await (const k of redis.scanIterator({
MATCH: `vz:${String(egasi)}:a*`, COUNT: 100,
})) kalitlar.push(...(Array.isArray(k) ? k : [k]));
assert.equal(kalitlar.length, 1);
const ttl = await redis.ttl(kalitlar[0] ?? "");
assert.ok(ttl >= 59 && ttl <= 66, `ttl ${String(ttl)}`);
});
test("Redis ishlamasa — ombor to'g'ridan, so'rov yiqilmaydi",
async () => {
const ulanmagan = createClient({
url: "redis://127.0.0.1:6399",
disableOfflineQueue: true,
socket: { reconnectStrategy: false },
});
ulanmagan.on("error", () => {});
const ogoh: string[] = [];
const o = new KeshliOmbor(new XotiraOmbori(), ulanmagan,
++egasi, {
ttlS: 60,
log: { warn: (_o, x) => { ogoh.push(x); } },
});
await o.yarat({ matn: "Redis'siz" });
assert.equal((await o.royxat(hammasi)).vazifalar[0]?.matn,
"Redis'siz");
assert.ok(o.hisob.xato >= 2);
assert.ok(ogoh.some(
(x) => x.startsWith("kesh: Redis xatosi")));
});
});
}Shartnoma testlari (omborShartnomasi) keshli omborni xotira ombori bilan bir xil natija berishga majburlaydi: agar invalidatsiyada xato bo'lsa (masalan yozuvdan keyin eski ro'yxat), shartnomaning "yarat → ro'yxat" testi yiqiladi. Kanon yakuniy tekshiruvida bu fayl +9 test qo'shdi (shartnoma 5 + kesh xulqi 4).
7.5 O'lchov: foyda bormi?
Kanon jonli API'ni katta bazasida (egasi 77) 300 ketma-ket so'rov bilan o'lchadi:
--- SAQLASH=postgres (keshsiz)
/v1/vazifalar?limit=50 mediana 12.92 ms, p95 41.81 ms
/v1/teglar mediana 9.16 ms, p95 20.36 ms
--- SAQLASH=postgres (Redis kesh)redis://127.0.0.1:6380
/v1/vazifalar?limit=50 mediana 13.67 ms, p95 35.35 ms
/v1/teglar mediana 7.97 ms, p95 23.43 ms
--- SAQLASH=prisma (keshsiz)
/v1/vazifalar?limit=50 mediana 14.07 ms, p95 33.68 ms
/v1/teglar mediana 10.93 ms, p95 25.96 ms
--- SAQLASH=prisma (Redis kesh)redis://127.0.0.1:6380
/v1/vazifalar?limit=50 mediana 12.88 ms, p95 22.79 ms
/v1/teglar mediana 12.29 ms, p95 23.49 msHalol xulosa: bu so'rovlarda kesh foyda bermadi. Postgres bilan ro'yxat hatto biroz sekinlashdi (12.9 → 13.7 ms). Sabab: ro'yxat so'rovi indeksli, bazada 1–5 ms. Javob vaqtining asosiy qismi — HTTP va Express (~8–10 ms). Kesh esa avlodni olish uchun +1 Redis aylanishini qo'shadi. Prisma bilan p95 33.7 → 22.8 ms ga yaxshilandi, lekin mediana deyarli bir xil.
Kesh qimmat so'rovda foyda beradi — shu darsning boshidagi statistika (9.5 → 1.5 ms). Kanon kodni baribir qoldirdi: u naqsh va invalidatsiyani to'g'ri o'rgatadi, baza va ma'lumot o'sganda esa foydasi paydo bo'ladi. Real loyihada qoida: avval o'lchang, keyin keshlang.
Invalidatsiya jonli sinovda ishladi — yozuvdan keyin ro'yxat darhol yangilandi (versiya 0 → 1, avlod 1).
7.6 Ishga tushirish va ogohlantirish
REDIS_URL=redis://127.0.0.1:6379 SAQLASH=postgres \
DATABASE_URL=postgres://postgres@127.0.0.1:5499/vazifalar \
EGASI_ID=1 npm startKanon topilmasi: bazani qayta qursangiz (prisma migrate reset yoki sxemani noldan), Redis'dagi avlod raqami va eski kesh qoladi — eski ma'lumot ko'rinishi mumkin. Bazani qayta qurganda Redis'dagi vz:* kalitlarini ham o'chiring (redis-cli --scan --pattern 'vz:*' + UNLINK). Kanon README'ga shu ogohlantirishni qo'shdi.
Tekshiring: npm run tip, npm run lint, npm test o'tishi kerak. Commit — keyinroq.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| O'zgarish ekranda ko'rinmaydi | yozuvdan keyin kesh tozalanmagan | DEL (COMMIT dan keyin) yoki avlod |
| Kesh tozalansa ham eski qiymat qaytadi | avval DEL, keyin COMMIT |
tartib: COMMIT → DEL |
| Har daqiqada baza yuklamasi keskin oshadi | hamma TTL bir xil | jitter |
| Mashhur kalit o'lganda baza yiqiladi | stampede | qulf yoki stale-while-revalidate |
| Yo'q id'lar bazani band qiladi | "topilmadi" keshlanmaydi | negative caching, qisqa TTL |
| Redis o'chsa API to'xtaydi | xato ushlanmagan, offline queue | catch → ombordan; disableOfflineQueue |
| Kesh qo'shildi, lekin tezlashmadi | so'rov o'zi arzon | avval o'lchang |
| Aziz Malikaning ma'lumotini ko'rdi | kalitda egasi yo'q | kalitga egasini qo'shing |
9. Mashqlar
Mashqlar kurs/mashqlar/27/58-kesh/ da.
1-mashq (oson): naqshni toping
- O'qishda: kesh → yo'q bo'lsa baza → keshga yozish —
- Yozuv avval keshga, bazaga keyin fonda —
- Bir paytda o'ladigan kalitlarni vaqt bo'ylab yoyish —
- "Topilmadi" javobini ham keshlash —
- Muddati o'tgan qiymatni darhol berib, fonda yangilash —
Yechim
- cache-aside — sukut tanlov.
- write-behind — tez, lekin Redis o'chsa yozuvlar yo'qoladi.
- jitter —
60 + Math.random() * 10. - negative caching — qisqa TTL bilan.
- stale-while-revalidate — kanonda 100 javob 8 ms, fonda 1 so'rov.
2-mashq (o'rta): kesh kaliti
keshKaliti(egasi, avlod, amal, args) funksiyasini yozing: kalit vz:<egasi>:a<avlod>:<amal>:<hash>; hash — args ning JSON'idan SHA-1, base64url (Web Crypto: crypto.subtle.digest, natijani base64url qiling). Bir xil argumentlar — bir xil kalit, avlod oshsa — boshqa kalit ekanini ko'rsating. Keyin muammoni toping: { limit: 10, teg: "uy" } va { teg: "uy", limit: 10 } bir xil kalit beradimi?
Yechim
async function sha1Base64url(matn) {
const bayt = new TextEncoder().encode(matn);
const h = await crypto.subtle.digest("SHA-1", bayt);
const b64 = btoa(String.fromCharCode(...new Uint8Array(h)));
return b64.replaceAll("+", "-").replaceAll("/", "_")
.replace(/=+$/, "");
}
async function keshKaliti(egasi, avlod, amal, args) {
const json = JSON.stringify(args) ?? "null";
return `vz:${egasi}:a${avlod}:${amal}:${await sha1Base64url(json)}`;
}
const a = await keshKaliti(1, 0, "royxat", { limit: 10, teg: "uy" });
const b = await keshKaliti(1, 0, "royxat", { limit: 10, teg: "uy" });
const c = await keshKaliti(1, 1, "royxat", { limit: 10, teg: "uy" });
const d = await keshKaliti(1, 0, "royxat", { teg: "uy", limit: 10 });
console.log(a === b, a === c, a === d);
console.log(a.split(":").slice(0, 4).join(":"));
console.log(a.split(":")[4].length);Konsolda:
true false false
vz:1:a0:royxat
27Bir xil so'rov — bir xil kalit (hit), avlod oshsa — yangi kalit (eski kesh o'qilmaydi). Lekin d: maydonlar tartibi boshqa — JSON.stringify boshqa satr beradi va kalit boshqacha. Natija xato bo'lmaydi, faqat kesh ikki marta to'ldiriladi. vazifalar-api da bu muammo yo'q: so'rov obyekti har doim bitta funksiyada, bir xil tartibda yig'iladi. SHA-1 20 bayt — base64url'da 27 belgi; xavfsizlik uchun emas, faqat qisqa nom uchun ishlatilgani sababli SHA-1 yetarli.
3-mashq (qiyin): stampede'ni takrorlang
Redis'siz, Map bilan: soxtaBaza() — 10 ms kutib, natija qaytaradi va chaqiruvlarni sanaydi. Ikki funksiya yozing: keshliOl(kalit) (himoyasiz cache-aside) va qulfliOl(kalit) (qulflar to'plamida kalit yo'q bo'lsa qulf olib hisoblaydi, aks holda 5 ms kutib qayta urinadi). Ikkalasini 100 parallel so'rov bilan sinab, bazaga nechta so'rov ketganini chiqaring.
Yechim
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
let dbSoni = 0;
async function soxtaBaza() {
dbSoni++;
await kut(10);
return { jami: 401 };
}
const kesh = new Map();
const qulflar = new Set();
async function keshliOl(kalit) {
if (kesh.has(kalit)) return kesh.get(kalit);
const s = await soxtaBaza();
kesh.set(kalit, s);
return s;
}
async function qulfliOl(kalit) {
for (;;) {
if (kesh.has(kalit)) return kesh.get(kalit);
if (!qulflar.has(kalit)) {
qulflar.add(kalit); // SET NX
try {
const s = await soxtaBaza();
kesh.set(kalit, s);
return s;
} finally {
qulflar.delete(kalit);
}
}
await kut(5);
}
}
const yuz = (fn) => Promise.all(Array.from({ length: 100 }, fn));
await yuz(() => keshliOl("stat:77"));
console.log("himoyasiz:", dbSoni);
kesh.clear();
dbSoni = 0;
const javoblar = await yuz(() => qulfliOl("stat:77"));
console.log("qulf bilan:", dbSoni, "| javoblar:", javoblar.length);Konsolda:
himoyasiz: 100
qulf bilan: 1 | javoblar: 100Kanon bilan bir xil manzara: himoyasiz 100 so'rovning hammasi miss ko'rdi (baza javobi 10 ms kechikkani uchun hech biri keshni topa olmadi). Qulf bilan faqat bittasi bazaga bordi, qolgan 99 tasi kutib, keshdan oldi. Bitta Node jarayonida Set yetadi; bir nechta serverda qulf umumiy bo'lishi kerak — shuning uchun Redis'dagi SET NX PX.
10. Real ishda
- Katta saytlar — bosh sahifa, katalog, sozlamalar keshda; foydalanuvchi ma'lumoti avlod yoki tez invalidatsiya bilan.
- Kutubxonalar — NestJS'da
CacheModule(Keyv + Redis, 30-qismda), Express'da qo'lda yozilgan qatlam ko'p uchraydi. - CDN va HTTP kesh — Redis'dan oldingi qatlam: umumiy sahifalar umuman serverga kelmaydi (HTTP caching va shartli so'rovlar).
- Intervyuda: "Cache-aside va write-through farqi?", "Keshni qanday invalidatsiya qilasiz?", "Cache stampede nima va qanday oldini olasiz?", "Kesh qachon zarar keltiradi?"
Xulosa
- Cache-aside: kesh → miss bo'lsa baza → keshga TTL bilan; Redis o'chsa ilova ishlashda davom etadi.
- Hit 1.5 ms vs baza 9.5 ms — lekin miss sekinroq; foyda hit ulushiga bog'liq.
- TTL + jitter; invalidatsiya: COMMIT → DEL yoki avlod kaliti (bitta
INCR). - Stampede: qulf (100 → 1 so'rov) yoki stale-while-revalidate; "yo'q" ham keshlanadi.
vazifalar-api:KeshliOmbordecorator,REDIS_URLixtiyoriy; ro'yxatda foyda o'lchanmadi — avval o'lchang.
Keyingi dars: Pub/Sub va Redis Streams — "yubor va unut" xabarlar va saqlanadigan jurnal: consumer group, ACK va yiqilgan ishchining xabarini boshqasi oladi.
Manbalar
- Redis hujjati — kesh sifatida: https://redis.io/docs/latest/develop/use/client-side-caching/, https://redis.io/solutions/caching/
- Cache stampede, erta yangilash (XFetch): Vattani, Chierichetti, Lowenstein, "Optimal Probabilistic Cache Stampede Prevention", VLDB 2015
- node-redis —
disableOfflineQueue,reconnectStrategy: https://github.com/redis/node-redis/blob/master/docs/client-configuration.md - O'lchovlar va kod — kanon o'lchovi (IlmHamroh, 2026-10): Redis 8.10.2, Node 24.21.0, node-redis 6.3.0; holat 65 —
src/vazifalar/kesh-ombor.ts,test/kesh-ombor.test.ts.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!