Rate limiting va caching: Redis'siz boshla, kerak bo'lganda o'sir
Loyiha o'sa boshlaganda ikkita muammo deyarli bir vaqtda keladi: kimdir API'ingizni so'rovlar bilan bosib tashlaydi (ataylab yoki bexosdan), database esa har so'rovda bir xil og'ir query'ni qayta-qayta bajaradi. Birinchisiga rate limiting, ikkinchisiga caching javob beradi. Ko'pchilik bu ikki so'zni eshitgan zahoti "demak, Redis kerak" deb o'ylaydi. Men esa teskarisini aytaman: ikkalasini ham bitta Node.js instansiyasida, hech qanday tashqi infra'siz boshlash mumkin — va ko'p loyihalar uchun shu bosqichning o'zi yetarli.
Nega bu ikkisi birga yuradi
Bir qarashda rate limiting (himoya) va caching (tezlik) boshqa-boshqa mavzular. Lekin mohiyatda ikkalasi bitta savolga javob beradi: har kelgan so'rovni to'liq ishlashimiz shartmi? Rate limiting "bu so'rovni umuman qabul qilamizmi?" deb so'raydi. Caching esa "qabul qildik, lekin javobni qaytadan hisoblaymizmi yoki tayyorini beramizmi?" deydi. Ikkalasi ham serverning eng qimmat resurslarini — CPU va database'ni — behuda ishdan qutqaradi.
Shuning uchun arxitektura savollari ham bir xil: holatni qayerda saqlaymiz (jarayon ichida yoki umumiy store'da), u qachon eskiradi va instansiya soni oshsa nima bo'ladi. Bittasini to'g'ri tushunsangiz, ikkinchisi ham o'z-o'zidan tushunarli bo'lib qoladi.
Rate limiting algoritmlari sodda tilda
Fixed window — eng soddasi: "har daqiqada 60 ta so'rov". Har daqiqa uchun hisoblagich ochiladi, 60 ga yetsa — 429. Kamchiligi chegarada: foydalanuvchi 11:00:59 da 60 ta, 11:01:00 da yana 60 ta so'rov yuborsa, ikki sekundda 120 so'rov o'tadi — limit amalda ikki baravar buziladi.
Sliding window — shu muammoni tuzatadi: "oxirgi 60 sekund" oynasini siljitib hisoblaydi. Aniqroq, lekin har so'rov vaqtini saqlash yoki taxminiy formula bilan ishlash kerak — biroz qimmatroq.
Token bucket — mening sevimlim. Tasavvur qiling: har foydalanuvchida savat bor, unga sekundiga 5 tadan token tomib turadi, savat sig'imi — 10 ta. Har so'rov bitta token yeydi. Token bo'lsa — marhamat, bo'lmasa — kut. Bu model real trafikka eng mos keladi: foydalanuvchi qisqa burst qila oladi (sahifa ochilganda 8 ta parallel so'rov — normal holat), lekin o'rtacha tezligi baribir cheklangan. Amalda API himoyasi uchun ko'p holatda token bucket yetadi — qolgan ikkitasini bilish esa tayyor kutubxonalar sozlamalarini tushunishda asqotadi.
In-memory'dan boshlang: Map bilan token bucket
Bitta Node.js instansiyasi uchun oddiy Map yetarli. Mana to'liq ishlaydigan Express middleware:
// rate-limit.js
const buckets = new Map();
const CAPACITY = 10; // savat sig'imi (burst chegarasi)
const REFILL_RATE = 5; // sekundiga qo'shiladigan token
function isAllowed(key) {
const now = Date.now();
let bucket = buckets.get(key);
if (!bucket) {
bucket = { tokens: CAPACITY, updatedAt: now };
buckets.set(key, bucket);
}
// o'tgan vaqtga proporsional token qo'shamiz
const elapsedSec = (now - bucket.updatedAt) / 1000;
bucket.tokens = Math.min(CAPACITY, bucket.tokens + elapsedSec * REFILL_RATE);
bucket.updatedAt = now;
if (bucket.tokens >= 1) {
bucket.tokens -= 1;
return true;
}
return false;
}
// eski bucket'larni tozalab turamiz — aks holda Map cheksiz o'sadi
setInterval(() => {
const now = Date.now();
for (const [key, bucket] of buckets) {
if (now - bucket.updatedAt > 60_000) buckets.delete(key);
}
}, 30_000);
module.exports = function rateLimit(req, res, next) {
if (!isAllowed(req.ip)) {
res.set('Retry-After', '1');
return res.status(429).json({ error: 'Too many requests' });
}
next();
};E'tibor bering: har bucket uchun alohida timer yo'q — tokenlar so'rov kelganda, o'tgan vaqtga qarab "dangasa" usulda to'ldiriladi. Bu 40 qator kod production'da bemalol ishlaydi; mashhur express-rate-limit kutubxonasi ham default holatda xuddi shunday in-memory store ishlatadi.
Qachon in-memory yetmaydi
Bitta instansiya bo'lsa — hammasi joyida. Endi PM2 cluster mode'da 4 ta worker ishga tushirdingiz deylik: har worker'ning o'z Map'i bor, so'rovlar ular orasida taqsimlanadi va real limit 60 emas, taxminan 240 bo'lib qoladi. Load balancer ortidagi ikki server — yana o'sha manzara.
Bu yerda muhim savol: limitning aniqligi qanchalik muhim? Agar maqsad shunchaki himoya bo'lsa ("bitta IP serverni yiqitmasin"), taxminiy limit ham vazifani bajaradi — har worker o'z ulushini nazorat qiladi, yashayverasiz. Lekin limit mahsulot qoidasi bo'lsa — "Free plan'da kuniga 1000 so'rov" — endi aniq umumiy hisoblagich kerak, va aynan shu nuqtada Redis sahnaga chiqadi. E'tibor bering: Redis bu yerda kesh sifatida emas, instansiyalar orasidagi umumiy hisoblagich sifatida kerak bo'lyapti. rate-limiter-flexible kutubxonasi ikkala rejimni ham qo'llaydi: memory store bilan boshlaysiz, kerak bo'lganda bitta konstruktor parametrini almashtirib Redis'ga o'tasiz — qolgan kod o'zgarmaydi.
HTTP caching: kod yozishdan oldin header yozing
Eng arzon so'rov — serveringizga umuman yetib kelmaydigan so'rov. Buning uchun kod emas, header kerak:
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');max-age=60 — brauzer va CDN javobni 60 sekund saqlaydi va bu vaqt ichida serverga umuman murojaat qilmaydi. stale-while-revalidate=300 — muddat o'tgach ham keyingi 5 daqiqada eski javob darhol beriladi, yangisi esa fonda olinadi: foydalanuvchi hech qachon kutib qolmaydi.
ETag boshqacha ishlaydi: server javob bilan birga kontent hash'ini yuboradi, brauzer keyingi safar If-None-Match bilan so'raydi. Kontent o'zgarmagan bo'lsa server 304 qaytaradi — body yuborilmaydi. Bu trafikni tejaydi, lekin server so'rovni baribir ishlaydi — ya'ni ETag CPU emas, kanalni tejaydi. Cloudflare kabi CDN'lar shu headerlarni o'qiydi: bitta qator bilan minglab so'rov serverga yetmasdan javob oladi. Faqat shaxsiy ma'lumotda ehtiyot bo'ling — public o'rniga private qo'ying, aks holda bir foydalanuvchining javobi CDN orqali boshqasiga tegishi mumkin.
Application cache: LRU + TTL
Database query natijasi, tashqi API javobi, og'ir hisob-kitob — bularni HTTP header bilan keshlashtirib bo'lmaydi, application ichida kesh kerak. Mana minimal LRU + TTL kesh:
class LruCache {
constructor(maxSize = 500, ttlMs = 60_000) {
this.maxSize = maxSize;
this.ttlMs = ttlMs;
this.map = new Map();
}
get(key) {
const entry = this.map.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
this.map.delete(key);
return undefined;
}
// LRU: ishlatilgan yozuvni oxiriga ko'chiramiz
this.map.delete(key);
this.map.set(key, entry);
return entry.value;
}
set(key, value) {
if (this.map.size >= this.maxSize && !this.map.has(key)) {
// Map kiritish tartibini saqlaydi — birinchisi eng eskisi
this.map.delete(this.map.keys().next().value);
}
this.map.set(key, { value, expiresAt: Date.now() + this.ttlMs });
}
delete(key) {
this.map.delete(key);
}
}
const cache = new LruCache(1000, 30_000);
async function getProduct(id) {
const cached = cache.get(`product:${id}`);
if (cached) return cached;
const product = await db.products.findById(id);
cache.set(`product:${id}`, product);
return product;
}
async function updateProduct(id, data) {
const product = await db.products.update(id, data);
cache.delete(`product:${id}`); // event'da invalidatsiya
return product;
}Bu yerda ikki himoya bor: TTL eskirgan ma'lumot chegarasini beradi, maxSize esa xotirani — chegaraga yetganda eng uzoq ishlatilmagan yozuv chiqarib yuboriladi.
Endi qiyin qismi. Cache invalidatsiya — dasturlashdagi mashhur ikki qiyin muammodan biri, va men unga uchta amaliy strategiya bilan yondashaman. Birinchisi — TTL bilan yashash: ma'lumot 30–60 sekund eskirishga chidasa (mahsulot ro'yxati, statistika, blog postlar), hech narsa qilmang — TTL o'zi hal qiladi. Ma'lumotlarning katta qismi aslida shunday. Ikkinchisi — event'da o'chirish: yuqoridagi updateProduct kabi, yozuv o'zgarganda keshdan o'chirasiz. Aniq ishlaydi, lekin har bir yozish joyini unutmaslik kerak — shu sabab yozuvlarni bitta service qatlamiga jamlang. Uchinchisi — ikkalasini birga: event'da o'chirish, lekin baribir TTL qo'yish. O'chirishni unutgan joyingizda TTL sug'urta bo'lib turadi. Men production'da deyarli doim uchinchisini tanlayman.
Next.js ISR — shu oilaning a'zosi
Agar Next.js ishlatsangiz, bularni allaqachon ko'rgansiz. revalidate: 60 — bu sahifa darajasidagi stale-while-revalidate: eski HTML darhol beriladi, yangisi fonda quriladi. revalidatePath esa event'da invalidatsiyaning o'zi — kontent o'zgarganda keshni qo'lda tozalaysiz. Mening saytim (Notion CMS + Next.js) ham aynan shu printsipda ishlaydi: Notion'da post o'zgaradi, sayt keshdan tez javob beraveradi, fonda esa yangilanadi. ISR'ni tushunish uchun yangi nazariya kerak emas — bu maqoladagi strategiyalar freymvork darajasiga ko'tarilgani xolos.
Xulosa: avval o'lchang, keyin Redis
Redis ajoyib vosita, lekin u javob bo'lishi kerak, default emas. Mening tartibim shunday: avval monitoring — qaysi endpoint sekin, database'ga qancha bir xil query ketyapti, cache hit rate qancha. O'lchamasangiz, Redis qo'shib nima yutganingizni ham bila olmaysiz. Keyin in-memory yechim: Map bilan token bucket, LRU + TTL kesh, to'g'ri Cache-Control headerlar. Va faqat aniq belgi paydo bo'lganda — bir nechta instansiyada aniq kvota kerak, kesh hajmi bitta jarayon xotirasiga sig'may qoldi, har deploy'da kesh yo'qolishi og'ritadigan bo'ldi — Redis qo'shasiz. O'shanda ham kod deyarli o'zgarmaydi: store almashadi, mantiq esa siz allaqachon tushunib olgan o'sha token bucket va o'sha TTL bo'lib qolaveradi.