Asosiy kontentga o‘tish

Maqolalar 10 daqiqa 50 ko‘rish

Redis bilan keshlash: tezlik yasash va eskirgan ma'lumot tuzog‘i

Bir xil ma'lumot qayta-qayta bazadan so‘ralsa, uni Redis'da saqlab, bazani tinch qoldirsa bo‘ladi. Cache-aside naqshini yozamiz, TTL bilan tanishamiz va keshlashning eng qiyin qismi — invalidatsiyani o‘z misolimizda ko‘ramiz.

database performance backend

Mundarija

Ma'lumotlar bazasiga har bir murojaat bir oz vaqt oladi. Bir-ikkitasi bilinmaydi, lekin bir xil ma'lumot sahifa har ochilganda qayta-qayta so‘ralsa, o‘sha takroriy so‘rovlar server yukining katta qismiga aylanadi. Keshlashning g‘oyasi oddiy: bir marta olingan ma'lumotni tezkor joyda saqlab qo‘yish va keyingi so‘rovlarga bazani bezovta qilmasdan javob berish. Bu tezkor joy odatda Redis bo‘ladi.

G‘oyani tushunish oson, lekin amalda keshlashning o‘z tuzoqlari bor va ular haqida oldindan bilib qo‘ygan yaxshi. Keling, hammasini kichik misolda — o‘zingiz ishga tushirib ko‘radigan misolda — ko‘rib chiqamiz.

Tayyorgarlik: Redis'ni ko‘tarib olamiz#

Redis — alohida server, shuning uchun uni avval ishga tushirish kerak. Eng tez yo‘li — Docker:

Terminal
docker run -d --name kesh-redis -p 6379:6379 redis:7
docker exec kesh-redis redis-cli ping

Ikkinchi buyruq PONG desa, Redis tayyor. Endi kichik loyiha yasaymiz:

Terminal
uv init --bare keshdemo
cd keshdemo
uv add redis

Kesh nimani tezlashtirayotganini ko‘rish uchun bizga "baza" ham kerak. Uni sun'iy qilib o‘tirmaymiz — oddiy SQLite yetarli. Muhimi shundaki, bazaga necha marta borganimizni sanab boramiz:

Python
# db.py
import sqlite3

BAZA = "kesh.db"
murojaat = 0  # bazaga necha marta borganimizni sanab boramiz


def tayyorla():
    with sqlite3.connect(BAZA) as conn:
        conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
        conn.execute("INSERT OR REPLACE INTO users (id, name) VALUES (1, 'Asadbek')")


def get_user(user_id):
    global murojaat
    murojaat += 1
    with sqlite3.connect(BAZA) as conn:
        row = conn.execute("SELECT id, name FROM users WHERE id = ?", (user_id,)).fetchone()
    return {"id": row[0], "name": row[1]}


def update_user(user_id, name):
    with sqlite3.connect(BAZA) as conn:
        conn.execute("UPDATE users SET name = ? WHERE id = ?", (name, user_id))

Haqiqiy loyihada bu yerda Django ORM yoki SQLAlchemy turadi — kesh tomonidan qaralganda farqi yo‘q.

Cache-aside: eng ko‘p ishlatiladigan naqsh#

Bu naqshning mantiqi shunday: ilova avval keshdan qaraydi, topsa — o‘shani qaytaradi, topmasa — bazadan oladi va keyingi safar uchun keshga yozib qo‘yadi.

Python
# cache.py
import json

import redis

import db

r = redis.Redis(host="localhost", port=6379, decode_responses=True)


def get_user(user_id):
    key = f"user:{user_id}"

    cached = r.get(key)
    if cached:  # keshda bor — bazaga bormaymiz
        return json.loads(cached)

    user = db.get_user(user_id)  # keshda yo'q — bazadan olamiz
    r.setex(key, 300, json.dumps(user))  # va 5 daqiqaga keshlab qo'yamiz
    return user

decode_responses=True — kichik, lekin hayotni osonlashtiradigan sozlama: usiz Redis javoblari b'{"id": 1...' ko‘rinishida, ya'ni bayt bo‘lib keladi va loglarga qaraganingizda ko‘zni charchatadi.

Birinchi chaqiruvda kesh bo‘sh bo‘ladi, shuning uchun ma'lumot bazadan keladi va keshga yoziladi. Keyingi besh daqiqa davomida esa har qanday chaqiruv to‘g‘ridan-to‘g‘ri keshdan javob oladi, baza umuman ishtirok etmaydi. Buni tekshirib ko‘ramiz — funksiyani yuz marta chaqiramiz:

Python
# demo.py
import cache
import db

db.tayyorla()
cache.r.flushdb()

for _ in range(100):
    cache.get_user(1)

print("100 ta chaqiruv qilindi")
print("bazaga murojaat:", db.murojaat)
print("kalitning qolgan muddati:", cache.r.ttl("user:1"), "soniya")
Terminal
uv run python demo.py
Natija
100 ta chaqiruv qilindi
bazaga murojaat: 1
kalitning qolgan muddati: 300 soniya

Mana shu — keshlashning butun mohiyati bitta qatorda: yuz ta so‘rov keldi, bazaga esa bir marta borildi. Qolgan to‘qson to‘qqiztasiga Redis javob berdi. Bu naqsh sodda, ishonchli va ko‘pchilik holat uchun aynan shu yetarli.

flushdb — faqat demo uchun

flushdb — shu Redis bazasidagi hamma kalitni o‘chiradi. Bu yerda u faqat har safar toza holatdan boshlash uchun turibdi. Ishlayotgan serverning Redis'ida uni hech qachon ishlatmang.

TTL: kesh o‘zini o‘zi tozalaydi#

Demoning oxirgi qatorida ko‘ringan 300 bejiz emas — u setex ga bergan muddatimiz, soniyada. Redis o‘sha kalitni besh daqiqadan keyin o‘zi o‘chiradi va navbatdagi so‘rov yana bazadan yangi ma'lumot olib keladi. Bu — TTL, ya'ni "yashash muddati". U keshlashning eng katta xavfini, ya'ni eskirgan ma'lumotni ko‘rsatib qolishni avtomatik cheklaydi: ma'lumot ko‘pi bilan besh daqiqa eskiradi, keyin o‘zi yangilanadi.

Muddatni tanlashning oddiy qoidasi bor: ma'lumot qanchalik tez-tez o‘zgarsa, TTL shunchalik qisqa bo‘ladi. Kamdan-kam o‘zgaradigan narsalarni — masalan kategoriyalar ro‘yxatini — bir necha soatga bemalol keshlash mumkin. Tez-tez o‘zgaradiganlarga esa qisqa muddat qo‘yiladi yoki umuman keshlanmaydi.

Eng qiyin qism: invalidatsiya#

Dasturchilar orasida mashhur bir hazil bor: kompyuter fanida ikkita chindan qiyin narsa bor, biri kesh invalidatsiyasi, ikkinchisi esa nomlarni to‘g‘ri tanlash. Bu bejiz aytilmagan.

Muammo shunday tug‘iladi. Foydalanuvchi ma'lumotini keshladingiz, u besh daqiqaga saqlanib turibdi. Shu orada foydalanuvchi o‘z profilini tahrirlaydi. Baza yangilandi, lekin keshda hali eski ma'lumot yotibdi — va keyingi besh daqiqa davomida hamma o‘sha eskisini ko‘radi. Foydalanuvchi ismini o‘zgartirib, sahifani yangilaydi-yu, eski ism turaveradi. Bu keshlashning klassik boshog‘rig‘i.

Yechim — ma'lumot o‘zgargan zahoti keshni tozalash. Buni cache.py ga qo‘shamiz:

Python
def update_user(user_id, name):
    db.update_user(user_id, name)
    r.delete(f"user:{user_id}")  # eski keshni o'chiramiz

Endi bitta faylda ikkala yo‘lni yonma-yon qo‘yamiz: biri to‘g‘ridan-to‘g‘ri bazani o‘zgartiradi (ya'ni keshni tozalashni "unutgan" yo‘l), ikkinchisi esa yangi update_user orqali boradi:

Python
# eskirish.py
import cache
import db

db.tayyorla()
cache.r.flushdb()

print("keshga tushdi:  ", cache.get_user(1))

db.update_user(1, "Asadbek Solijonov")  # keshni tozalashni unutgan yo'l
print("bazada endi:    ", db.get_user(1))
print("ilova ko'radi:  ", cache.get_user(1))

cache.update_user(1, "Asadbek Solijonov")  # to'g'ri yo'l — delete bor
print("delete'dan keyin:", cache.get_user(1))
Terminal
uv run python eskirish.py
Natija
keshga tushdi:   {'id': 1, 'name': 'Asadbek'}
bazada endi:     {'id': 1, 'name': 'Asadbek Solijonov'}
ilova ko'radi:   {'id': 1, 'name': 'Asadbek'}
delete'dan keyin: {'id': 1, 'name': 'Asadbek Solijonov'}

Uchinchi qatorga qarang: bazada ism allaqachon o‘zgargan, ilova esa hamon eskisini qaytaryapti. Hech qanday xato, hech qanday ogohlantirish yo‘q — shuning uchun ham bu tuzoq shunchalik yoqimsiz. Va u o‘zi ketmaydi, TTL tugaguncha, ya'ni to‘la besh daqiqa shunday turadi. To‘rtinchi qator esa delete borligida hammasi joyida ekanini ko‘rsatadi.

Diqqat qiladigan joy ham shu yerda: ma'lumotni o‘zgartiradigan har bir yo‘lda keshni tozalashni unutmaslik kerak. Agar loyihada foydalanuvchi bir necha joydan yangilanadigan bo‘lsa, o‘sha joylarning birortasida delete tushib qolsa, o‘sha yerdan kelgan o‘zgarish keshda ko‘rinmay qoladi.

Ba'zan invalidatsiyaga umuman aralashmaslik yaxshiroq

Har o‘zgarishda keshni to‘g‘ri tozalash murakkab bo‘lsa, ba'zan eng sodda yechim — shunchaki qisqa TTL qo‘yish va invalidatsiya bilan umuman shug‘ullanmaslik. Ma'lumot bir daqiqa eskirsa zarar bo‘lmaydigan joylarda (masalan bosh sahifadagi statistika) bu ancha kam xato manbasi bo‘ladi. Aniqlik muhim joyda esa aksincha — keshni qo‘lda tozalash shart.

Nimani keshlamaslik kerak#

Hamma narsani keshlash vasvasasi bor, lekin ba'zi narsalar keshga umuman tushmasligi kerak. Tez-tez o‘zgaradigan va aniqligi muhim ma'lumot — masalan hisob balansi — keshlanmaydi, chunki eskirgan balans jiddiy muammo. Har foydalanuvchi uchun noyob va bir marta ishlatiladigan ma'lumotni keshlashning ma'nosi yo‘q, u ikkinchi marta so‘ralmaydi ham. Ruxsat tekshiruvlarini keshlash esa ochig‘i xavfli: foydalanuvchining huquqi bekor qilingan bo‘lsa ham, keshda eski ruxsat turib, unga kirish berib qo‘yishi mumkin.

Kesh — bu yana bitta server#

Keshlashning ko‘pincha e'tibordan chetda qoladigan narxi bor: siz ilovaga yangi bog‘liqlik qo‘shdingiz. Redis'ni o‘chirib ko‘ring va o‘sha demoni qaytadan ishga tushiring:

Terminal
docker stop kesh-redis
uv run python demo.py
Natija
redis.exceptions.ConnectionError: Error 61 connecting to localhost:6379. Connection refused.

Baza esa joyida, sog‘-salomat turibdi. Ya'ni kesh qo‘shilgunga qadar ishlayotgan sahifa endi Redis tufayli yiqildi. Aynan shuning uchun jiddiy loyihalarda kesh o‘qishini try/except ichiga olib, Redis javob bermasa to‘g‘ridan-to‘g‘ri bazaga tushib ketadigan qilib yozishadi — sayt sekinroq ishlaydi, lekin ishlaydi. Redis'ni qaytarib yoqib qo‘ying:

Terminal
docker start kesh-redis

Tekshirib bo‘lgach, konteynerni butunlay olib tashlash mumkin:

Terminal
docker rm -f kesh-redis

Asosiy qoida#

Keshlash kuchli vosita, lekin u bepul kelmaydi — har kesh loyihaga murakkablik va eskirgan ma'lumot xavfini qo‘shadi. Shuning uchun uni "har ehtimolga qarshi" hamma joyga sepmang. Avval o‘lchang, haqiqatan sekin va tez-tez takrorlanadigan joyni toping, keyin faqat o‘shani keshlang. Sekin bo‘lmagan joyni keshlash tezlik bermaydi, faqat yangi xato manbai yaratadi.

10 daqiqa 50 marta o‘qildi