20 β Keshlash va performance¶
β¬ οΈ Oldingi: 19 β Signallar va custom mantiq Β· π README Β· Keyingi: 21 β Async va fon vazifalari β‘οΈ
Bu bobda: ilovangizni tezlashtirishni o'rganamiz. Avval Django'ning cache frameworki bilan tanishamiz: bitta
CACHESsozlamasi orqali butun loyihaga kesh ulanadi. Backendlarni ko'rib chiqamiz β ishlab chiqish uchunlocmem(xotirada, hech narsa o'rnatish shart emas), production uchunRedisvaMemcached(bu bobda Redis serveri yo'q, shuning uchun uni illustrativ ko'rsatamiz, lekin RUN paytidalocmemishlatamiz). Keyin keshlashning to'rt darajasini bosib o'tamiz: low-level cache API (cache.get/cache.set/get_or_set/incr/set_manyβ eng aniq nazorat), per-view kesh (@cache_pagedekoratori), template fragment kesh ({% cache %}tegi), va per-site kesh (middleware bilan butun saytni keshlash). Eng muhim savolga javob beramiz: qachon keshlash kerak va qachon kerak emas β kesh "stale" (eskirgan) ma'lumot xavfini keltiradi, shuning uchun invalidatsiya (keshni bekor qilish) strategiyalarini ko'ramiz. Oxirida keshlashdan oldin qilinishi shart bo'lgan ishni β query optimizatsiyani (select_related,prefetch_related,only,values,count)assertNumQueriesbilan aniq raqam orqali isbotlaymiz. Bu bob Python'ni bilishni nazarda tutadi (Python qo'llanma); ORM va SQL asoslari foydali (SQL qo'llanma); production deploy keyingi qadamlarda (CI/deploy qo'llanma). Hamma kod Django 6.0.6 da haqiqatan ishga tushirib tekshirilgan (kesh uchunlocmem, baza uchun SQLite; Redis/Memcached serverlari illustrativ).
Nega keshlash kerak?¶
Har bir HTTP so'rov kelganda Django bir xil ishni qaytadan bajaradi: bazaga so'rovlar yuboradi, shablonlarni render qiladi, hisob-kitoblar qiladi. Agar ma'lumot tez-tez o'zgarmasa β masalan, bosh sahifadagi "eng mashhur 10 maqola" ro'yxati β bu ishni har safar qaytarish isrofdir.
Keshlash β qimmat hisobning natijasini tez joyga (xotira yoki Redis) saqlab qo'yish, keyingi so'rovda esa qaytadan hisoblamasdan to'g'ridan-to'g'ri o'sha tayyor natijani berish.
Asosiy tushunchalar:
- Cache hit β kerakli qiymat keshda topildi, tez qaytariladi (bazaga tegilmaydi).
- Cache miss β keshda yo'q, qimmat hisob bajariladi, natija keshga yoziladi.
- Timeout (TTL) β qiymat necha soniya keshda yashaydi. Vaqt tugagach "eskiradi" (expire).
- Invalidatsiya β manba ma'lumot o'zgarganda eski keshni qo'lda o'chirish.
Django'da keshlashni to'rt darajada qilish mumkin β keng qamrovdan (butun sayt) aniq nazoratgacha (bitta qiymat):
Pastga tushganingiz sari kodingiz ko'payadi, lekin nazoratingiz aniqlashadi. Quyida pastdan yuqoriga emas, balki eng moslashuvchanidan (low-level) boshlaymiz, chunki qolganlarini tushunish osonlashadi.
Muhim qoida: Keshlash sekin kodni tuzatmaydi, faqat yashiradi. Keshlashdan oldin so'rovlaringizni optimizatsiya qiling (bobning oxiriga qarang). Aks holda kesh "miss" bo'lgan har safar foydalanuvchi yana sekin javob oladi.
Cache framework va backendlar¶
Django keshni markazlashgan tarzda sozlaydi: settings.py da bitta CACHES lug'ati. Undan keyin butun loyiha bo'ylab bir xil cache obyektidan foydalanasiz β kod backend'ga bog'liq emas. Ertaga locmem'dan Redis'ga o'tsangiz, kodni o'zgartirmaysiz, faqat sozlamani almashtirasiz.
locmem β ishlab chiqish uchun (RUN qilinadi)¶
Eng oddiy backend β local memory (jarayon xotirasida). Hech narsa o'rnatish shart emas, default ham aynan shu. Bu bobda hamma kod aynan shu backend bilan tekshirilgan.
# config/settings.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "ch20-cache", # bir nechta locmem keshni ajratish uchun
}
}
locmem'ning cheklovi: kesh har bir jarayon xotirasida alohida. Agar production'da bir nechta worker (gunicorn) ishlasa, har biri o'z keshiga ega bo'ladi va ular bir-birini ko'rmaydi. Shuning uchun locmem faqat ishlab chiqish va testlar uchun.
Redis β production uchun (illustrativ)¶
Production'da hammasi bitta umumiy keshni ko'rishi kerak. Django 6.0 da Redis backend o'rnatilgan holda keladi (RedisCache klassi). Quyidagi konfiguratsiya to'g'ri, lekin bu muhitda Redis serveri yo'q β shuning uchun u illustrativ (ishga tushirilmagan):
# config/settings.py β ILLUSTRATIV: Redis serveri ishlab turishi kerak
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
RedisCache klassi Django bilan birga keladi (uni import qilib bo'ladi), lekin ishlash uchun redis Python paketi (pip install redis) va ishlab turgan Redis serveri kerak. Redis disk emas, xotirada ishlaydi, shuning uchun juda tez; bundan tashqari incr, expire kabi atomik operatsiyalarni qo'llab-quvvatlaydi.
Memcached β yana bir production varianti (illustrativ)¶
Memcached ham mashhur. Django 6.0 da PyMemcacheCache backend bor:
# config/settings.py β ILLUSTRATIV: Memcached serveri + pymemcache paketi kerak
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211",
}
}
Boshqa backendlar (qisqacha)¶
db.DatabaseCacheβ keshni baza jadvalida saqlaydi (createcachetablekerak). Redis yo'q bo'lsa ham worker'lar o'rtasida umumiy bo'ladi, lekin bazaga yuk tushiradi.filebased.FileBasedCacheβ keshni fayllarda saqlaydi.dummy.DummyCacheβ hech narsa keshlamaydi (kodingizni testlashda keshni "o'chirib qo'yish" uchun qulay).
Bash: Quyidagi buyruq Redis backend klassi Django bilan kelishini ko'rsatadi (server kerakmas, faqat import):
Natija:
RedisCache mavjud(bizning muhitda ham chiqdi). Lekin uni amalda ishlatish uchun server kerak.
Low-level cache API: cache.get / cache.set¶
Eng moslashuvchan usul β istalgan Python obyektini (son, satr, ro'yxat, lug'at, hatto QuerySet natijasi) qo'lda keshlash. Buning uchun django.core.cache dan cache obyektini import qilamiz.
from django.core.cache import cache
cache.set("salom", "Assalomu alaykum", timeout=300) # 300 soniya (5 daqiqa)
qiymat = cache.get("salom") # "Assalomu alaykum"
yoq = cache.get("yoq", "standart") # kalit yo'q bo'lsa -> "standart"
Mantiq oddiy: avval keshdan so'raymiz; bor bo'lsa (hit) β qaytaramiz; yo'q bo'lsa (miss) β hisoblaymiz va keshga yozamiz.
Quyidagi to'liq misol haqiqatan ishga tushirilgan (python test_lowlevel.py):
from django.core.cache import cache
# set / get
cache.set("salom", "Assalomu alaykum", timeout=300)
print(cache.get("salom")) # Assalomu alaykum
print(cache.get("yoq", "standart-qiymat")) # standart-qiymat (kalit yo'q)
# add β FAQAT kalit yo'q bo'lsa yozadi, bor bo'lsa tegmaydi
print(cache.add("hisob", 1)) # True (yangi yozildi)
print(cache.add("hisob", 999)) # False (allaqachon bor, tegmadi)
print(cache.get("hisob")) # 1 (999 yozilmadi)
# incr / decr β atomik son oshirish/kamaytirish
cache.set("hits", 10)
print(cache.incr("hits")) # 11
print(cache.incr("hits", 5)) # 16
print(cache.decr("hits")) # 15
# set_many / get_many β bir nechta kalit birvarakayiga
cache.set_many({"a": 1, "b": 2, "c": 3})
print(cache.get_many(["a", "b", "c"])) # {'a': 1, 'b': 2, 'c': 3}
# delete β keshdan o'chirish (invalidatsiya)
cache.delete("salom")
print(cache.get("salom", "OCHIRILDI")) # OCHIRILDI
Timeout qoidalari (bu ham tekshirilgan):
cache.set("abadiy", "x", timeout=None) # None = hech qachon eskirmaydi
cache.set("darhol", "y", timeout=0) # 0 = darhol eskiradi (deyarli yozilmaydi)
print(cache.get("abadiy")) # x
print(cache.get("darhol", "ESKIRDI")) # ESKIRDI
Diqqat: Yuqoridagi
cache.add("hisob", 999)qatori ataylab "ishlamaydigan" misol βaddmavjud kalitni o'zgartirmaydi. Agar majburan ustiga yozmoqchi bo'lsangiz,cache.setishlating,cache.addemas.
get_or_set: eng ko'p ishlatiladigan naqsh¶
"Keshda bo'lsa qaytar, bo'lmasa hisobla-yoz-qaytar" mantig'ini bitta chaqiruvga jamlaydi:
from django.core.cache import cache
def qimmat_hisob():
print(" >> qimmat funksiya ishladi")
return 42
# 1-chaqiruv: keshda yo'q -> funksiya ishlaydi, natija keshga yoziladi
print(cache.get_or_set("javob", qimmat_hisob, 300)) # funksiya ishladi -> 42
# 2-chaqiruv: keshda bor -> funksiya UMUMAN ishlamaydi
print(cache.get_or_set("javob", qimmat_hisob, 300)) # to'g'ridan keshdan -> 42
Haqiqiy chiqishda >> qimmat funksiya ishladi faqat bir marta chiqdi β ikkinchi chaqiruvda funksiya umuman ishga tushmadi. Bu aynan biz xohlagan tejamkorlik.
touch: eskirish vaqtini yangilash¶
Qiymatni qayta yozmasdan, faqat TTL'ni uzaytirish:
cache.set("sessiya", "ma'lumot", timeout=10)
print(cache.touch("sessiya", 300)) # True β endi yana 300 soniya yashaydi
print(cache.touch("yoq-kalit", 300)) # False β kalit yo'q, hech narsa qilmadi
Low-level keshni view ichida ishlatish¶
Endi haqiqiy stsenariy β bosh sahifada og'ir hisobni keshlash. Quyidagi naqsh python test_invalidate.py da tekshirilgan:
from django.core.cache import cache
from blog.models import Post
def postlar_soni():
"""Keshlangan hisob. Faqat kesh bo'sh bo'lsa bazaga boradi."""
kesh = cache.get("postlar_soni")
if kesh is None: # cache miss
kesh = Post.objects.count() # qimmat hisob (DB so'rovi)
cache.set("postlar_soni", kesh, 300)
return kesh # cache hit yo'lida DB tegilmaydi
Tekshirishda birinchi chaqiruv bazaga bordi (DB hisob: 1), ikkinchi chaqiruv keshdan keldi (DB hisob hamon 1). Yangi post qo'shilganda esa eski kesh noto'g'ri bo'lib qoladi β uni qo'lda bekor qilamiz:
Post.objects.create(sarlavha="Yangi", matn="...", category=c)
cache.delete("postlar_soni") # INVALIDATSIYA β endi keyingi chaqiruv qayta hisoblaydi
Bu stale ma'lumot muammosining yechimi: ma'lumot o'zgarganda keshni o'chirib tashlaysiz. Buni qo'lda emas, signal orqali avtomatik qilish ham mumkin β pastda ko'ramiz.
Bir nechta nomli kesh¶
CACHES da bir nechta kesh sozlasangiz, caches lug'ati orqali nomi bilan tanlanadi:
from django.core.cache import cache, caches
# `cache` β bu "default" keshning qulay yorlig'i (thread-local proxy)
caches["default"].set("kalit", "qiymat")
print(cache.get("kalit")) # qiymat β ikkalasi bir xil keshni ko'rsatadi
Nozik nuqta:
caches["default"] is cacheaslidaFalseqaytaradi βcachebu nomli keshga ishora qiluvchi alohida proxy obyekt, lekin xuddi shu kesh ma'lumotlariga ulanadi. Ya'nicaches["default"]ga yozganingiznicacheorqali o'qiy olasiz.
Per-view kesh: @cache_page¶
Agar butun view'ning HTTP javobini keshlamoqchi bo'lsangiz, qiymatlarni qo'lda boshqarish shart emas β @cache_page dekoratori javobni butunligicha keshlaydi.
# blog/views.py
from django.http import HttpResponse
from django.views.decorators.cache import cache_page
VIEW_HISOB = {"n": 0}
@cache_page(60) # javob 60 soniya keshlanadi
def vaqt_view(request):
VIEW_HISOB["n"] += 1
return HttpResponse(f"Hisoblandi: {VIEW_HISOB['n']} marta")
assertNumQueries'ga o'xshab, biz view necha marta haqiqatan ishlaganini sanab isbotladik (python manage.py test):
# blog/tests.py
from django.test import TestCase, override_settings
from django.core.cache import cache
from blog import views
@override_settings(CACHES={"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
}})
class CachePageTest(TestCase):
def setUp(self):
cache.clear()
views.VIEW_HISOB["n"] = 0
def test_cache_page(self):
r1 = self.client.get("/vaqt/")
r2 = self.client.get("/vaqt/")
self.assertEqual(r1.content, r2.content) # ikkala javob bir xil
self.assertEqual(views.VIEW_HISOB["n"], 1) # view FAQAT 1 marta ishladi
# query string boshqa bo'lsa -> ALOHIDA kesh
self.client.get("/vaqt/?p=2")
self.assertEqual(views.VIEW_HISOB["n"], 2) # boshqa URL uchun yana ishladi
Natija: OK β birinchi va ikkinchi /vaqt/ so'rovida view atigi bir marta ishladi (ikkinchisi keshdan keldi), lekin ?p=2 boshqa URL bo'lgani uchun alohida keshlandi (view yana ishladi).
URL'ga dekoratorni urls.py da ham qo'yish mumkin (view'ni o'zgartirmasdan):
# config/urls.py
from django.urls import path
from django.views.decorators.cache import cache_page
from blog import views
urlpatterns = [
path("vaqt/", cache_page(60)(views.vaqt_view), name="vaqt"),
]
Diqqat:
@cache_pageni autentifikatsiyaga bog'liq (har foydalanuvchiga har xil) yoki POST view'larda ishlatmang β barcha foydalanuvchilar bir xil keshlangan javobni ko'rib qoladi. U faqat anonim, GET, hammaga bir xil sahifalar uchun. Foydalanuvchiga bog'liq javoblar uchunvary_on_headers/vary_on_cookieyoki low-level kesh ishlating.
Template fragment kesh:¶
Ko'pincha butun sahifa emas, faqat bir qismi qimmat bo'ladi β masalan, sidebar'dagi "eng mashhur teglar" yoki murakkab jadval. {% cache %} tegi shablonning faqat shu bo'lagini keshlaydi, qolgani har safar yangi render bo'ladi.
{% load cache %}
{% cache 300 panel %}
<!-- bu blok 300 soniya keshlanadi -->
OG'IR HISOB: {{ qiymat }}
{% endcache %}
python test_fragment.py da tekshirildi: birinchi render qiymat=AAA bilan keshlandi; ikkinchi render qiymat=BBB bilan chaqirilsa ham, fragment keshlangani uchun hali ham AAA chiqdi (OG'IR HISOB: AAA). Demak fragment ichidagi hisob qaytadan bajarilmadi.
Vary-by argumentlar¶
Bitta fragment turli foydalanuvchilarga turlicha bo'lishi mumkin. {% cache %} ga qo'shimcha argumentlar bersangiz, har bir kombinatsiya alohida keshlanadi:
Tekshirishda user_id=1 uchun Panel #1, user_id=2 uchun Panel #2 chiqdi β ya'ni har bir foydalanuvchi o'z keshini oldi, aralashib ketmadi.
Fragmentni dasturdan o'chirish: make_template_fragment_key¶
Manba o'zgarganda fragment keshini bekor qilish uchun uning kalitini make_template_fragment_key bilan qayta hosil qilamiz:
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
# {% cache 300 user_panel user.id %} ga mos kalit (user.id=7 uchun)
kalit = make_template_fragment_key("user_panel", [7])
print(kalit) # template.cache.user_panel.03a149245ce7dd99...
cache.delete(kalit) # fragment keshi o'chdi -> keyingi render yangilanadi
Birinchi argument β fragment nomi ({% cache ... NOM %} dagi), ikkinchisi β vary-by argumentlar ro'yxati. Bu tekshirildi: o'chirgandan keyin cache.get(kalit) None qaytardi.
Per-site kesh: butun saytni keshlash¶
Eng yuqori daraja β middleware orqali butun saytning har bir GET javobini keshlash. Ikki middleware kerak va ularning tartibi muhim: UpdateCacheMiddleware ro'yxatda eng tepada (javobni yozish uchun), FetchFromCacheMiddleware eng pastda (kelgan so'rovga kesh bormi tekshirish uchun).
# config/settings.py (per-site uchun)
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware", # ENG TEPADA
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware", # ENG PASTDA
]
CACHE_MIDDLEWARE_SECONDS = 60 # har javob 60 soniya keshlanadi
CACHE_MIDDLEWARE_KEY_PREFIX = "" # bir nechta sayt bitta keshni bo'lishsa
python -m pytest test_persite.py bilan tekshirildi: per-site kesh yoqilganida, dekoratori yo'q oddiy view ham ikkinchi so'rovga umuman ishlamadi (view hisobi 1 da qoldi). Ya'ni javob to'liq middleware darajasida keshdan qaytdi.
Diqqat: Per-site kesh β eng kuchli, lekin eng xavfli variant. U autentifikatsiya qilingan/shaxsiy sahifalar bo'lgan saytda noto'g'ri ishlaydi (bir foydalanuvchining sahifasi boshqasiga ko'rinib qolishi mumkin). U faqat deyarli to'liq anonim, statik saytlar (masalan, blog, hujjatlar sayti) uchun. Aksariyat real loyihalarda
@cache_pageyoki low-level kesh aniqroq va xavfsizroq.
Kesh versiyalash va kalit prefiksi¶
Bir nechta loyiha bitta Redis serverini bo'lishsa, kalitlar to'qnashmasligi uchun Django har kalitga prefiks va versiya qo'shadi. Versiyalashdan deploy paytida butun keshni "yangilash" uchun foydalansa bo'ladi.
from django.core.cache import cache
# version bilan izolyatsiya
cache.set("kalit", "v1-qiymat", version=1)
cache.set("kalit", "v2-qiymat", version=2)
print(cache.get("kalit", version=1)) # v1-qiymat
print(cache.get("kalit", version=2)) # v2-qiymat
# incr_version β kalit versiyasini oshirib, eski versiyani "ko'rinmas" qiladi
cache.set("hujjat", "matn", version=1)
cache.incr_version("hujjat", version=1) # endi qiymat version=2 da
print(cache.get("hujjat", version=1, default="YO'Q")) # YO'Q (eski versiya bo'sh)
print(cache.get("hujjat", version=2)) # matn (yangi versiyada)
Bu tekshirildi: incr_version qiymatni eski versiyadan yangisiga "ko'chirdi". settings.py da KEY_PREFIX va VERSION ni global belgilash ham mumkin:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"KEY_PREFIX": "myapp", # barcha kalitlar oldiga "myapp:" qo'shiladi
"VERSION": 1, # default versiya
"TIMEOUT": 300, # default TTL (soniyalarda)
}
}
Qachon keshlash kerak (va qachon kerak emas)¶
Keshlash bepul emas β u stale ma'lumot xavfini va invalidatsiya murakkabligini olib keladi. Mashhur ibora: kompyuter fanidagi ikki qiyin masala β kesh invalidatsiyasi va narsalarga nom qo'yish.
Keshlash YAXSHI bo'lgan joylar:
- O'qish ko'p, yozish kam ma'lumot (blog postlari, kategoriyalar, sozlamalar).
- Hisoblash qimmat (murakkab agregat, tashqi API javobi, og'ir shablon render).
- Natija deyarli barcha foydalanuvchilarga bir xil (anonim bosh sahifa).
- Bir necha soniya/daqiqa eskirgan ma'lumot maqbul (yangiliklar lentasi).
Keshlash YOMON bo'lgan joylar:
- Har foydalanuvchiga shaxsiy va doim yangi bo'lishi shart ma'lumot (bank balansi, savatcha).
- Tez-tez o'zgaradigan, lekin har doim aniq bo'lishi kerak qiymat.
- So'rov allaqachon tez (keshlash murakkablik qo'shadi, foyda bermaydi).
Invalidatsiya strategiyalari:
- TTL (vaqt bilan) β eng oddiy.
timeoutqo'yasiz, vaqt tugagach kesh o'zi yangilanadi. Biroz "stale" ma'lumotga toqat qilsangiz, eng kam og'riqli yo'l. - Hodisa bilan (signal) β manba o'zgarganda keshni darhol o'chirish.
post_save/post_deletesignaliga ulanasiz (19-bobga qarang):
# blog/signals.py β ILLUSTRATIV naqsh (signal 19-bobda batafsil)
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from blog.models import Post
@receiver([post_save, post_delete], sender=Post)
def postlar_keshini_bekor_qil(sender, **kwargs):
cache.delete("postlar_soni") # manba o'zgardi -> kesh eskirdi, o'chiramiz
Bu naqshning mantig'i (signalsiz, qo'lda cache.delete) test_invalidate.py da tekshirildi: yangi post qo'shilib, kesh o'chirilgandan keyin hisob qayta hisoblandi.
Maslahat: Avval TTL bilan boshlang. Faqat "stale" ma'lumot haqiqatan muammo bo'lganda hodisaga asoslangan invalidatsiyaga o'ting β u ancha murakkab va xatoga moyil.
Keshlashdan oldin: query optimizatsiya¶
Eng katta xato β sekin so'rovni keshlash bilan "yashirish". Kesh "miss" bo'lgan har safar foydalanuvchi yana sekin javob oladi. Shuning uchun avval so'rovlar sonini kamaytiring.
Eng keng tarqalgan muammo β N+1: bitta so'rov bilan ro'yxat olasiz, keyin har bir element uchun yana bittadan so'rov yuborasiz.
assertNumQueries Django testlarida har bir blok aniq necha SQL so'rov yuborganini isbotlaydi. Quyidagilar python manage.py test blog.tests da o'tdi (Ran 7 tests ... OK):
# blog/tests.py
from blog.models import Category, Post
class QueryOptimizationTest(TestCase):
@classmethod
def setUpTestData(cls):
c1 = Category.objects.create(nom="Texnologiya")
c2 = Category.objects.create(nom="Sport")
for i in range(10):
Post.objects.create(
sarlavha=f"Post {i}", matn="...",
category=c1 if i % 2 else c2,
)
def test_n_plus_1(self):
# β select_related YO'Q -> 1 (postlar) + 10 (har biriga category) = 11 so'rov
with self.assertNumQueries(11):
for p in Post.objects.all():
_ = p.category.nom
def test_select_related(self):
# β
select_related BOR -> JOIN bilan atigi 1 so'rov
with self.assertNumQueries(1):
for p in Post.objects.select_related("category"):
_ = p.category.nom
def test_count_vs_len(self):
# β
.count() -> SELECT COUNT(*), 1 so'rov, qatorlarni xotiraga yuklamaydi
with self.assertNumQueries(1):
n = Post.objects.count()
self.assertEqual(n, 10)
11 vs 1 β bu farq 10 postda sezilmaydi, lekin 10 000 postda sayt "qotib" qoladi. Boshqa muhim vositalar (bular ham tekshirildi):
class MoreOptimizationTest(TestCase):
# ... setUpTestData: 1 category, 5 post ...
def test_prefetch_related(self):
# teskari FK (Category -> postlar): prefetch bilan atigi 2 so'rov
with self.assertNumQueries(2):
for cat in Category.objects.prefetch_related("postlar"):
_ = list(cat.postlar.all())
def test_only(self):
# .only("sarlavha") faqat kerakli ustunni oladi (kam ma'lumot)
with self.assertNumQueries(1):
list(Post.objects.only("sarlavha"))
def test_values(self):
# .values() dict qaytaradi, model obyekti yaratmaydi -> tezroq, yengilroq
with self.assertNumQueries(1):
data = list(Post.objects.values("sarlavha"))
self.assertIsInstance(data[0], dict)
Qoidalar:
select_relatedβForeignKey/OneToOne(ya'ni "bitta" tomon) uchun: SQLJOINbilan bir so'rovda oladi.prefetch_relatedβManyToManyva teskariForeignKey("ko'p" tomon) uchun: alohida so'rov yuboradi, Python'da bog'laydi (yuqorida 2 so'rov).only/deferβ faqat kerakli ustunlarni oling (only) yoki og'irlarini keyinga qoldiring (defer).values/values_listβ model obyekti kerak bo'lmasa, dict/tuple bilan tezroq.countβ ro'yxat uzunligi kerak bo'lsalen(qs)o'rnigaqs.count()(qatorlarni yuklamaydi).existsβ "biror qator bormi" uchunif qs:o'rnigaif qs.exists():.
N+1 va munosabatlar 9-bobda chuqurroq ko'rilgan. Esda tuting: optimizatsiya birinchi, keshlash ikkinchi.
Performance o'lchash: DEBUG va so'rovlarni ko'rish¶
DEBUG=True bo'lsa, Django har bir so'rovni connection.queries da yozib boradi. Buni ishlab chiqishda tekshirish uchun ishlatasiz:
from django.db import connection, reset_queries
from blog.models import Post
reset_queries()
list(Post.objects.select_related("category"))
print("So'rovlar soni:", len(connection.queries))
# DEBUG=True da har bir so'rovning SQL matnini ham ko'rish mumkin
for q in connection.queries:
print(q["sql"][:80])
Eslatma:
connection.queriesfaqatDEBUG=Trueda to'ldiriladi. Production'da (DEBUG=False) u har doim bo'sh β bu xotirani tejash uchun. So'rovlarni testda aniq sanash uchunassertNumQueriesishonchliroq. Real loyihadadjango-debug-toolbaryokidjango-silkpaketlari so'rovlarni vizual ko'rsatadi (ular bu bobda o'rnatilmagan β illustrativ).
Xulosa¶
- Cache framework markazlashgan: bitta
CACHESsozlamasi, kod backend'ga bog'liq emas. - Backendlar:
locmem(ishlab chiqish, RUN qilindi),Redis/Memcached(production, illustrativ β server kerak),db/filebased/dummy. - Low-level API:
cache.set/get/add/delete/incr/set_many/get_or_set/touchβ eng aniq nazorat. - Per-view:
@cache_page(60)butun javobni keshlaydi; URL (query bilan) kalit bo'ladi. - Fragment:
{% cache 300 nom %}...{% endcache %}sahifaning bir qismini;make_template_fragment_keybilan o'chiriladi. - Per-site: ikki middleware butun saytni keshlaydi β kuchli, lekin faqat anonim/statik saytlar uchun.
- Qachon keshlash: o'qish ko'p / yozish kam, qimmat hisob, biroz "stale"ga toqat. Invalidatsiya: TTL (oddiy) yoki signal (aniq).
- Avval optimizatsiya:
select_related/prefetch_related/only/values/countbilan so'rovlar sonini kamaytiring;assertNumQueriesbilan isbotlang.
Keyingi bobda fon vazifalari va asinxron ishlashni ko'ramiz β uzoq davom etadigan ishlarni (email yuborish, hisobot tayyorlash) so'rov tsiklidan tashqariga chiqaramiz.
Mashqlar¶
Oson¶
settings.pydalocmemkeshini sozlang (LOCATIONbilan).python manage.py shelldacache.set("test", 123, 60)vacache.get("test")ni chaqirib,123qaytishini tekshiring.cache.addvacache.setfarqini ko'rsating: bitta kalitgaaddni ikki marta turli qiymat bilan chaqirib, ikkinchisi e'tiborga olinmasligini isbotlang.cache.set_many({"x": 1, "y": 2})bilan ikkita kalit yozing, keyincache.get_many(["x", "y", "z"])natijasidazyo'qligini ko'ring.cache.get("yoq-kalit", "default")chaqirib, mavjud bo'lmagan kalit uchundefaultqaytishini tasdiqlang.@cache_page(30)dekoratorini bitta oddiy view'ga qo'shing va URL'ga ulang.- Shablonda
{% load cache %}qilib,{% cache 60 salom %}Salom {{ ism }}{% endcache %}bloki yozing.
O'rta¶
get_or_setdan foydalanib,Post.objects.count()natijasini keshlovchi funksiya yozing. Funksiya bazaga faqat birinchi marta borishini print bilan ko'rsating.incr/decrbilan oddiy "sahifa ko'rishlar hisoblagichi" yozing: har bir view chaqiruvidacache.incr("hits").make_template_fragment_key("user_panel", [user_id])bilan ma'lum bir foydalanuvchining fragment keshini topib o'chiring.cache.set(..., version=1)vaversion=2bilan bir xil kalitga ikki qiymat yozib, ular bir-biriga aralashmasligini ko'rsating.- Testda
assertNumQueriesbilanPost.objects.all()(N+1) vaPost.objects.select_related("category")so'rovlar sonini taqqoslang. @override_settingsbilan testdaDummyCacheishlatib,cache.set/cache.getdan keyinNoneqaytishini (kesh o'chirilgan) tekshiring.
Qiyin¶
- Signal (
post_save) orqaliPostsaqlangandacache.delete("postlar_soni")ni avtomatik chaqiruvchi invalidatsiya tizimi yozing va uni test bilan isbotlang (post yaratilgandan keyin keshlangan hisob yangilanadi). - Per-site kesh middleware'ini sozlab (
UpdateCacheMiddleware+FetchFromCacheMiddleware),pytesttestida ikkinchi so'rovga view umuman ishlamasliginiassertNumQueries(0)yoki view hisoblagichi orqali isbotlang. Category.objects.prefetch_related("postlar")va prefetch'siz versiyaniassertNumQueriesbilan taqqoslab, prefetch'ning so'rovlar sonini kamaytirishini ko'rsating.
Yechimlar
1.
# settings.py
CACHES = {"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "mashq-cache",
}}
# shell
from django.core.cache import cache
cache.set("test", 123, 60)
print(cache.get("test")) # 123
2.
from django.core.cache import cache
cache.delete("k")
print(cache.add("k", "birinchi")) # True
print(cache.add("k", "ikkinchi")) # False β tegmadi
print(cache.get("k")) # birinchi
3.
cache.set_many({"x": 1, "y": 2})
print(cache.get_many(["x", "y", "z"])) # {'x': 1, 'y': 2} β z yo'q
4.
5.
# views.py
from django.http import HttpResponse
from django.views.decorators.cache import cache_page
@cache_page(30)
def oddiy(request):
return HttpResponse("Keshlangan javob")
# urls.py
from django.urls import path
from blog import views
urlpatterns = [path("oddiy/", views.oddiy, name="oddiy")]
6.
7.
from django.core.cache import cache
from blog.models import Post
def keshlangan_soni():
def hisobla():
print(">> bazaga bordi")
return Post.objects.count()
return cache.get_or_set("post_soni", hisobla, 300)
keshlangan_soni() # >> bazaga bordi (birinchi marta)
keshlangan_soni() # (print chiqmaydi β keshdan)
8.
from django.core.cache import cache
from django.http import HttpResponse
def hisoblagich(request):
cache.add("hits", 0) # kalit yo'q bo'lsa 0 dan boshla
soni = cache.incr("hits") # har chaqiruvda +1
return HttpResponse(f"Ko'rishlar: {soni}")
9.
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
def fragmentni_ochir(user_id):
kalit = make_template_fragment_key("user_panel", [user_id])
cache.delete(kalit)
10.
cache.set("k", "v1", version=1)
cache.set("k", "v2", version=2)
print(cache.get("k", version=1)) # v1
print(cache.get("k", version=2)) # v2 β aralashmaydi
11.
from django.test import TestCase
from blog.models import Category, Post
class N1Test(TestCase):
@classmethod
def setUpTestData(cls):
c = Category.objects.create(nom="A")
for i in range(10):
Post.objects.create(sarlavha=f"P{i}", matn="...", category=c)
def test_taqqos(self):
with self.assertNumQueries(11): # N+1
[p.category.nom for p in Post.objects.all()]
with self.assertNumQueries(1): # select_related
[p.category.nom for p in Post.objects.select_related("category")]
12.
from django.test import TestCase, override_settings
from django.core.cache import cache
@override_settings(CACHES={"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
}})
class DummyTest(TestCase):
def test_dummy(self):
cache.set("k", "v")
self.assertIsNone(cache.get("k")) # DummyCache hech narsa saqlamaydi
13.
# blog/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from blog.models import Post
@receiver([post_save, post_delete], sender=Post)
def post_keshini_bekor_qil(sender, **kwargs):
cache.delete("postlar_soni")
# blog/apps.py β ready() da signalni ulang
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
from blog import signals # noqa
# blog/tests.py
from django.test import TestCase
from django.core.cache import cache
from blog.models import Category, Post
class InvalidateTest(TestCase):
def test_signal_keshni_bekor(self):
c = Category.objects.create(nom="A")
cache.set("postlar_soni", Post.objects.count(), 300) # 0
Post.objects.create(sarlavha="X", matn="...", category=c) # signal -> delete
self.assertIsNone(cache.get("postlar_soni")) # kesh bekor qilindi
14.
# test_persite.py
from django.test import override_settings
MW = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
@override_settings(
MIDDLEWARE=MW, CACHE_MIDDLEWARE_SECONDS=60,
CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}},
)
def test_per_site(client):
from blog import views
views.PLAIN_HISOB["n"] = 0
client.get("/oddiy/")
client.get("/oddiy/")
assert views.PLAIN_HISOB["n"] == 1 # 2-so'rov view'ga bormadi
15.
from django.test import TestCase
from blog.models import Category, Post
class PrefetchTest(TestCase):
@classmethod
def setUpTestData(cls):
for j in range(3):
c = Category.objects.create(nom=f"C{j}")
for i in range(5):
Post.objects.create(sarlavha=f"P{i}", matn="...", category=c)
def test_prefetch(self):
# prefetch'siz: 1 (category) + 3 (har biriga postlar) = 4
with self.assertNumQueries(4):
for cat in Category.objects.all():
list(cat.postlar.all())
# prefetch bilan: atigi 2
with self.assertNumQueries(2):
for cat in Category.objects.prefetch_related("postlar"):
list(cat.postlar.all())
β¬ οΈ Oldingi: 19 β Signallar va custom mantiq Β· π README Β· Keyingi: 21 β Async va fon vazifalari β‘οΈ