Dilshod.dev

Keshni bekor qilish — amaliyotda

Hazil bu ikkita qiyin muammodan biri deydi. Haqiqat esa shuki, ko'p jamoalar bekor qilish strategiyasini umuman yozmaydi — ular TTL yozib, umid qiladi. Muqobillar aslida nimaga tushishini ko'ramiz.

Muallif: Dilshod Abdullayev8 daqiqa o'qish

Kompyuter fanidagi ikkita qiyin narsa — keshni bekor qilish va nomlar tanlash, degan eskirgan hazil bor. Muhandislik hazillarining ko'pi kabi u yashaydi, chunki yarim rost va yo'l-yo'riq sifatida mutlaqo befoyda.

Men ko'rgan narsa shuki, keshni bekor qilish hazil aytgan ma'noda qiyin emas. U qiyin, chunki jamoalar loyihalash bosqichini butunlay o'tkazib yuboradi. Kimdir sekin endpointni tuzatish uchun kesh qo'shadi, mos ko'ringan TTL tanlaydi va deploy qiladi. Qanchalik eskirgan ma'lumot maqbul ekani hech qayerda yozilmaydi, shuning uchun u maqbul bo'lishdan to'xtaganini hech kim sezmaydi. Olti oydan keyin support tiketida narx kecha o'zgargani, ilova esa hamon eskisini ko'rsatayotgani yoziladi.

Kesh xato qilmagan. U aynan aytilgan ishni bajargan. Unga nima deyishni hech kim hal qilmagan edi.

Hech kim so'ramaydigan savoldan boshlang

Redis, TTL yoki bekor qilish hodisalarini tanlashdan oldin shu savolga javob bering: bu ma'lumot kimgadir zarar yetkazguncha qancha eskirishi mumkin?

"Kimdir sezguncha" emas. Zarar yetguncha. Bu ikkisi juda boshqacha raqamlar.

  • Marketing sahifasidagi mahsulot katalogi: soatlar normal. Bir kunlik tavsifdan hech kim zarar ko'rmaydi.
  • Checkout'da ko'rsatilgan narx: soniyalar, aslida esa hech qachon — yechayotgan summangiz ko'rsatgan narxingizga teng bo'lishi shart.
  • Foydalanuvchining o'z profili, u tahrirlagandan keyin: nol. Odamga o'zi kiritgan o'zgarishning eski holatini ko'rsatish — ilovani buzuq his qildirishning eng tez usuli.
  • Analitika paneli: daqiqalar, va foydalanuvchi baribir kechikish bor deb o'ylaydi.

Bu to'rt javob to'rtta butunlay boshqa dizaynga olib boradi. Shu savolni o'tkazib yuborsangiz, bitta Redis mijozi, bitta CACHE_TTL konstantasi va to'rttasiga ham qo'llangan o'sha 300 soniya bilan qolasiz.

TTL — bu strategiya, va ko'pincha to'g'ri strategiya

Faqat TTL'ga asoslangan keshni dangasalik varianti deb hisoblash odati bor. Unday emas. Bu — eng kam harakatlanuvchi qismga ega variant, va ma'lumotlarning katta sinfi uchun u to'g'ri.

async function getCountryCatalogue(countryCode: string) {
  const key = `catalogue:v3:${countryCode}`;
 
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  const fresh = await db.plan.findMany({ where: { countryCode, active: true } });
  await redis.set(key, JSON.stringify(fresh), 'EX', 600);
  return fresh;
}

Haftada ikki marta o'zgaradigan katalogda o'n daqiqalik eskirish — bu xato emas. Bu — siz ongli ravishda qilgan savdo, va u sizga bitta qator kod hamda servislar orasida nol muvofiqlashtirish turadi.

Shu parchadagi ikki tafsilot ko'ringanidan muhimroq. Kalitdagi v3 — versiya prefiksi: keshlanadigan qiymatning shakli o'zgarsa, uni oshirasiz va barcha eski yozuvlar bir zumda yetib bo'lmas holga keladi. Migratsiya yo'q, flush yo'q, deploydan keyin noto'g'ri tipga deserializatsiya bo'ladigan eski obyektlar yo'q. Bu — keshdagi eng arzon xavfsizlik mexanizmi va u muntazam ravishda tushirib qoldiriladi.

Ikkinchisi — kalit javobni o'zgartiradigan har bir kirishni o'z ichiga oladi. Parametrni tushirib qoldirgan kesh kaliti — kesh emas, bir mamlakat tariflarini boshqasiga xizmat qiladigan xato.

TTL keltirib chiqaradigan bosqin

TTL'ning bitta mashhur nosozligi bor, va biror ommabop narsani keshlasangiz, u bilan albatta uchrashasiz.

Kalit muddati tugaydi. Ikki yuzta parallel so'rov keshni topolmaydi. Barcha ikki yuztasi bazaga bir xil qimmat so'rov yuboradi. Bir zum oldin bemalol ishlab turgan baza endi sizning eng sekin so'rovingizning ikki yuzta nusxasini bir vaqtda bajaryapti, kechikish sakrashi qayta urinishlarni keltiradi, ular yana ko'proq nusxa hosil qiladi.

Yechim — aynan bitta so'rov qayta qursin, qolganlari kutsin yoki eski nusxani olsin:

async function getWithLock<T>(key: string, ttl: number, load: () => Promise<T>): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  // Qulfni faqat bitta chaqiruvchi yutadi; NX buni atomar qiladi. Tasodifiy
  // token aynan SHU egani belgilaydi — pastdagi bo'shatish skriptiga qarang.
  const lockKey = `${key}:lock`;
  const token = crypto.randomUUID();
  const won = await redis.set(lockKey, token, 'EX', 10, 'NX');
 
  if (!won) {
    // Boshqasi qayta qurmoqda. Qisqa kutamiz, keyin uning natijasini o'qiymiz.
    await sleep(50);
    const retry = await redis.get(key);
    if (retry) return JSON.parse(retry);
    return load();            // so'rovni ushlab turgandan ko'ra o'zimiz olamiz
  }
 
  try {
    const fresh = await load();
    await redis.set(key, JSON.stringify(fresh), 'EX', ttl);
    return fresh;
  } finally {
    // Bitta atomar qadamda solishtirib-o'chirish. Oddiy DEL o'sha yerdagi
    // istalgan qulfni o'chiradi — jumladan BOSHQA chaqiruvchining qulfini
    // ham, agar load() 10 soniyalik muddatdan uzoq ketgan bo'lsa.
    await redis.eval(
      `if redis.call("get", KEYS[1]) == ARGV[1] then
         return redis.call("del", KEYS[1])
       else return 0 end`,
      1, lockKey, token,
    );
  }
}

finally ixtiyoriy emas. Xato yo'lida bo'shatilmaydigan qulf o'tkinchi baza nosozligini har bir so'rov keshni chetlab o'tadigan o'n soniyaga aylantiradi — ya'ni siz oldini olmoqchi bo'lgan bosqin, endi xatoni qayta ishlashning o'zi tomonidan qo'zg'atilgan.

Token ham ixtiyoriy emas, va odatda aynan shu qism tushirib qoldiriladi. Agar load() qulfning o'n soniyalik muddatidan uzoq ketsa, qulf allaqachon yo'q va uni boshqa so'rov qonuniy ravishda egallagan bo'ladi. Shu paytdagi oddiy DEL o'shaning qulfini o'chiradi, va endi ikkita chaqiruvchi bir vaqtda qayta qurmoqda — qulf oldini olish uchun mavjud bo'lgan aynan o'sha holat, tozalashning o'zi tomonidan qaytarib keltirilgan. O'chirishdan oldin tokenni tekshirish bitta Lua skript turadi va butun bir xatolar sinfini yo'q qiladi.

Aniq bekor qilish va u sotib oladigan bog'liqlik

Eskirish nolga yaqin bo'lishi shart bo'lganda TTL yetarli bo'lmay qoladi. Yozuv paytida bekor qilasiz.

async function updatePlanPrice(planId: string, price: number) {
  const plan = await db.plan.update({ where: { id: planId }, data: { price } });
 
  await Promise.all([
    redis.del(`plan:v3:${planId}`),
    redis.del(`catalogue:v3:${plan.countryCode}`),
  ]);
 
  return plan;
}

Bu ishlaydi, va nomlashga arziydigan haqiqiy narx keltiradi: endi yozuv yo'li bu ma'lumotni o'z ichiga olgan har bir kesh kalitini bilishi kerak. Keyingi chorakda "tanlangan tariflar" keshini qo'shsangiz, bu funksiya yangilanishi shart — yangilanmasa, hech qayerda xato bermaydigan eskirgan ma'lumot olasiz. Bilim har bir yozuvchi bo'ylab tarqalgan va uning to'liqligini hech nima majburlamaydi.

Buni jilovlashning ikki yo'li bor.

Teglarga asoslangan bekor qilish. Teskari moslikni saqlang, shunda yozuvchilar kalitlarni sanab chiqmasdan tushunchani nomlaydi.

// Keshlayotganda bu kalit qaysi teglarga tegishli ekanini yozib qo'yamiz.
// (Bu postdagi barcha buyruq nomlari ioredis uslubida — node-redis v4 da
// ular sAdd/sMembers, set esa options obyektini oladi.)
await redis.sadd(`tag:plan:${planId}`, key);
await redis.sadd(`tag:country:${countryCode}`, key);
 
// Yozuv paytida teg ostidagi hamma narsani tashlaymiz.
async function invalidateTag(tag: string) {
  const keys = await redis.smembers(`tag:${tag}`);
  if (keys.length) await redis.del(keys);
  await redis.del(`tag:${tag}`);
}

Endi updatePlanPrice invalidateTag ni tarif tegi bilan chaqiradi va qanday keshlar borligini bilishi shart emas. Yangi keshlar o'zini o'zi ro'yxatdan o'tkazadi.

To'g'ridan-to'g'ri chaqiruv o'rniga hodisalar. plan.updated e'lon qiling va har bir kesh egasi obuna bo'lsin. Bu to'g'ri ajratadi, lekin evaziga pirovard izchillik (eventual consistency) beradi: endi yozuv bilan bekor qilish orasida oyna paydo bo'ladi. Odatda millisekundlar, ba'zan iste'molchi orqada qolganda ancha uzoq. Talabingiz "nol eskirish" bo'lgan bo'lsa, hodisalar shinasi buni bermaydi — u eskirishni ko'zga kamroq tashlanadigan joyga ko'chiradi, xolos.

O'z yozuvingni o'zing ko'rish qoidasi

Men ko'rgan barcha kesh xatolari ichida bu bittasi har qator kodga eng ko'p foydalanuvchi g'azabini keltiradi.

Foydalanuvchi profilini tahrirlaydi. Yozuv muvaffaqiyatli. Redirect profilni keshdan yuklaydi. U eski ismini ko'radi. Yana tahrirlaydi. Natija o'sha. U ilova buzuq degan xulosaga keladi, va u haq.

Umumiy eskirish chidamliligi va shaxsiy eskirish chidamliligi — boshqa raqamlar. Boshqa foydalanuvchining profili o'ttiz soniya eskirganidan hech kim norozi emas. O'zinikidan esa hamma norozi.

async function getProfile(userId: string, viewerId: string) {
  // Ko'ruvchilar keshni ko'radi. Egasi doim haqiqatni ko'radi.
  if (userId === viewerId) return db.user.findUnique({ where: { id: userId } });
 
  return getWithLock(`profile:v2:${userId}`, 60, () =>
    db.user.findUnique({ where: { id: userId } }),
  );
}

Uch qator. U "ilova o'zgarishlarimni saqlamadi" turkumidagi butun bir shikoyatlar sinfini yo'q qiladi — ularning ko'pchiligida ilova o'zgarishlarni mukammal saqlagan, keyin esa keshdagi nusxani ko'rsatgan edi.

Aslida nima buziladi

Bosqin va o'z yozuvini ko'rishdan tashqari, men duch kelgan kesh hodisalarining ko'pini uchta narsa tashkil qiladi.

Nosozlikni keshlash. Yuqori oqimdagi chaqiruv vaqti tugaydi, handler null qaytaradi va null o'n daqiqaga keshlanadi. Endi o'tkinchi uzilish o'n daqiqalik ishdan chiqishga aylandi. Faqat muvaffaqiyatli natijalarni keshlang, va agar qidiruv toshqinidan himoya uchun salbiy natijalarni keshlash shart bo'lsa, ularga o'zining ancha qisqa TTL sini bering.

Cheklanmagan kalitlar o'sishi. Foydalanuvchi kiritgan ma'lumotdan qurilgan kalit — qidiruv so'rovi, filtrlar kombinatsiyasi — hujumchi yoki qidiruv roboti keshingizni bir martalik yozuvlar bilan to'ldirib, qimmatli hamma narsani siqib chiqarishi mumkinligini bildiradi. Kirish maydonini cheklang yoki uni belgilangan miqdordagi savatlarga heshlang.

Deploy paytidagi deserializatsiya xatolari. Keshlangan obyekt tipiga maydon qo'shdingiz. Eski yozuvlarda u yo'q. Kod value.newField.something o'qiydi va eski TTL tugaguncha xato beradi. Versiya prefiksi aynan shuning uchun mavjud, va shuning uchun u birinchi hodisadan keyin qo'shiladigan narsa emas, birinchi kundan kalitda bo'lishi kerak.

Umumlashadigan qism

Kesh — oxirida qo'shiladigan tezlik hiylasi emas. Bu — tezlik va yuk kamayishi evaziga noto'g'ri bo'lishi mumkinligini bilgan ma'lumotni xizmat qilish haqidagi ongli qaror. Bu qonuniy savdo — internetning ko'p qismi shunga tayanadi — lekin bu savdo, va savdolar oshkora qilinishi kerak.

Shuning uchun har bir keshning yoniga uni ko'rib chiqiladigan qiladigan ikki narsani yozib qo'ying: bu qancha eskirsa muhim bo'lib qoladi va uni nima yangilaydi. Ikkalasiga ham bittadan jumlada javob bera olmasangiz, kesh tugallanmagan. U hozircha tez, xolos.

O'xshash maqolalar

system-designbackend

Ikki marta yechilgan to'lov

Qayta urinish takroriy so'rov emas — to'lov ikki marta o'tib ketguncha. Idempotentlik kalitlari aslida qanday ishlaydi, sodda variant nega baribir ikki marta yechadi va qaysi Postgres cheklovi kafolatni haqiqiy qiladi.

7 daqiqa o'qish
postgresqlperformance

Postgres 18: asinxron I/O va UUIDv7 menda nimani o'zgartirdi

Asinxron I/O va ichki `uuidv7()` — asosiy yangiliklar shular edi. Ulardan biri men ikki yil chetlab o'tib kelgan muammoni jimgina hal qildi; ikkinchisi esa benchmarklar va'da qilganidan kamroq ish qildi.

5 daqiqa o'qish
postgresqldatabases

Hech nima qilmaydigan indekslar

Index qo'shdingiz, query'ni ishlatdingiz — u hamon sekin. Planner sizni mensimayotgani yo'q — u sizga ma'lumotingiz, ustunlar tartibi yoki tiplaringiz haqida nimadir aytyapti. Uni qanday o'qishni ko'ramiz.

10 daqiqa o'qish