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.
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:
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:
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:
# 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.
# 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:
# 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")
uv run python demo.py
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:
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:
# 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))
uv run python eskirish.py
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:
docker stop kesh-redis
uv run python demo.py
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:
docker start kesh-redis
Tekshirib bo‘lgach, konteynerni butunlay olib tashlash mumkin:
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.