Caching — Redis bilan MongoDB natijalarini keshlash

~20 daq · MongoDB · Amaliy va Professional

Muammodan boshlaylik

Bosh sahifa har ochilganda:

const kategoriyalar = await Kategoriya.find().lean()                    // 15 ms
const ommabop = await Mahsulot.aggregate([...])                         // 420 ms
const aksiyalar = await Aksiya.find({ faol: true }).populate(...)       // 60 ms

Daqiqasiga 3000 ta tashrif — MongoDB 3000 marta bir xil og'ir aggregation ni bajaradi. Natija esa soatlab deyarli o'zgarmaydi. CPU 90%, boshqa so'rovlar ham sekinlashadi.


Cache qayerda

Ilova ──▶ Redis (1 ms) ──yo'q──▶ MongoDB (420 ms) ──▶ Redis ga saqlash
            │
            └──bor──▶ darhol javob

Redis — xotirada ishlaydigan kalit-qiymat ombori: o'qish ~1 ms, barcha ilova nusxalari uchun umumiy (Express kursi, 57-dars).


Hayotiy o'xshatma

Ofisdagi kotib va arxiv. Har kim "o'tgan oylik hisobot"ni so'raydi. Kotib birinchi marta arxivga tushib (MongoDB), hisobotni topadi va bir nusxasini stoliga qo'yadi (Redis). Keyingi safar — stoldan beradi. Hisobot yangilanganda — stoldagi eski nusxani tashlab yuboradi.


O'rnatish

npm install redis
// src/cache.js
import { createClient } from 'redis'

export const redis = createClient({ url: process.env.REDIS_URL })
redis.on('error', (err) => console.error('Redis xatosi', err))
await redis.connect()

Cache-aside — universal yordamchi

// src/cache.js
export async function keshlangan(kalit, soniya, olish) {
  try {
    const bor = await redis.get(kalit)
    if (bor) return JSON.parse(bor)
  } catch (err) {
    console.error('Redis o\'qish xatosi', err)    // Redis ishlamasa — DB dan davom etamiz
  }

  const yangi = await olish()

  redis.set(kalit, JSON.stringify(yangi), { EX: soniya }).catch(() => {})
  return yangi
}

export async function keshniTozala(...kalitlar) {
  await redis.del(kalitlar).catch(() => {})
}
router.get('/bosh-sahifa', async (req, res) => {
  const [kategoriyalar, ommabop] = await Promise.all([
    keshlangan('kategoriyalar', 3600, () => Kategoriya.find().lean()),
    keshlangan('mahsulotlar:ommabop', 600, () => Mahsulot.ommabop()),
  ])
  res.json({ kategoriyalar, ommabop })
})
  • .lean() — Mongoose document emas, oddiy obyekt keshlansin
  • Redis xatosi — sayt to'xtamasin, to'g'ridan-to'g'ri DB ga murojaat qilinsin
  • TTL (EX) — har doim

[!WARNING] JSON ga aylantirganda MongoDB turlari o'zgaradi: ObjectId → string, Date → ISO string. Keshdan olingan m.createdAt.getTime() — xato, m._id.equals() — xato. Keshlangan ma'lumot bilan ishlashda bu turlarni string deb hisoblang yoki kerak joyda qayta o'giring.


Bitta document keshi

export const mahsulotKaliti = (id) => `mahsulot:${id}`

router.get('/mahsulotlar/:id', async (req, res) => {
  const m = await keshlangan(mahsulotKaliti(req.params.id), 300, () =>
    Mahsulot.findById(req.params.id).populate('kategoriya', 'nomi').lean()
  )
  if (!m) return res.status(404).json({ xato: 'Topilmadi' })
  res.json(m)
})

[!INFO] null natija ham keshlanadi — bu yaxshi: mavjud bo'lmagan ID larni qayta-qayta so'rash (bot yoki hujum) bazani yuklamaydi. Faqat TTL qisqaroq bo'lsin.


Invalidation — eskirgan keshni tozalash

Yozish joyida

router.patch('/mahsulotlar/:id', async (req, res) => {
  const m = await Mahsulot.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true })
  await keshniTozala(mahsulotKaliti(req.params.id), 'mahsulotlar:ommabop')
  res.json(m)
})

Mongoose middleware da — avtomatik

mahsulotSchema.post('save', async function (doc) {
  await keshniTozala(mahsulotKaliti(doc._id), 'mahsulotlar:ommabop')
})

mahsulotSchema.post('findOneAndUpdate', async function (doc) {
  if (doc) await keshniTozala(mahsulotKaliti(doc._id), 'mahsulotlar:ommabop')
})

Mahsulot qayerdan o'zgartirilmasin (API, admin panel, skript) — kesh tozalanadi. Lekin updateMany kabi so'rovlar uchun ham hook yozishni unutmang.

Barcha manbalardan — Change Streams

Boshqa xizmatlar ham bazani o'zgartirsa — Change Stream (54-dars) orqali:

Mahsulot.watch().on('change', (o) => {
  if (o.documentKey) keshniTozala(mahsulotKaliti(o.documentKey._id))
})

Kesh strategiyalari

Strategiya Qanday Qachon
Cache-aside O'qishda: yo'q bo'lsa DB → keshga Ko'pchilik holatlar
Write-through Yozishda: DB + keshni birga yangilash O'qish juda ko'p, kesh doim yangi bo'lishi kerak
TTL faqat Invalidation siz, muddat tugashini kutish Biroz eskirish muammo emas (statistika)
Oldindan isitish Scheduled job keshni to'ldiradi Juda og'ir hisobotlar

Cache stampede

Mashhur kalitning TTL i tugadi — shu soniyada 500 ta so'rov keshni bo'sh topadi va hammasi bir vaqtda og'ir aggregation ni ishga tushiradi.

Oddiy himoya — bir jarayon ichida bitta so'rov:

const jarayonda = new Map()

export async function keshlangan(kalit, soniya, olish) {
  const bor = await redis.get(kalit).catch(() => null)
  if (bor) return JSON.parse(bor)

  if (jarayonda.has(kalit)) return jarayonda.get(kalit)

  const vada = olish()
    .then(async (yangi) => {
      await redis.set(kalit, JSON.stringify(yangi), { EX: soniya }).catch(() => {})
      return yangi
    })
    .finally(() => jarayonda.delete(kalit))

  jarayonda.set(kalit, vada)
  return vada
}

Bir nechta server uchun — Redis qulfi (SET kalit:lock 1 NX EX 10) yoki TTL ga tasodifiy qo'shimcha (jitter) — kalitlar bir vaqtda eskirmasligi uchun.

[!TIP] Keshlashdan oldin so'rovni tezlashtirishga harakat qiling: to'g'ri index (26–29-darslar) ko'pincha 420 ms ni 5 ms ga tushiradi va kesh umuman kerak bo'lmaydi. Kesh — murakkablik va eskirgan ma'lumot xavfini qo'shadi.


Tez-tez uchraydigan xatolar

Foydalanuvchiga xos ma'lumotni umumiy kalit bilan

// ❌ — hamma birinchi foydalanuvchining savatini ko'radi
keshlangan('savat', ...)

// ✅
keshlangan(`savat:${req.user.id}`, ...)

Mongoose document ni keshlash

JSON.stringify(doc) — ishlaydi, lekin keshdan olingani document emas, oddiy obyekt. .lean() bilan boshidanoq oddiy obyekt oling.

TTL siz kalitlar

Invalidation unutilsa — kesh abadiy eskirgan qoladi va Redis xotirasi to'ladi.


Xulosa

  • Redis — og'ir va tez-tez so'raladigan natijalar uchun umumiy kesh
  • Cache-aside: Redis → yo'q bo'lsa MongoDB → saqlash (TTL bilan)
  • .lean() natijalarni keshlang; JSON da ObjectId va Date string ga aylanadi
  • Invalidation: yozish joyida, Mongoose post hook lari yoki Change Streams
  • Redis xatosi saytni to'xtatmasin
  • Stampede — jarayon ichida bitta so'rov, qulf, jitter
  • Avval index — keyin kesh

Keyingi dars: 59-dars — Migration — migrate-mongo bilan schema o'zgarishlarini boshqarish