Pull to refresh

Cacheops

Reading time 3 min
Views 7.9K
Некоторое время назад я писал о системе кеширования. Помнится, я обещал продолжение, но сейчас решил, что строка кода лучше сотни комментариев, теорию оставим на потом. Поэтому сегодня у нас своего рода анонс с парой советов по использованию в одном флаконе. Встречайте, cacheops — система кеширования и автоматической инвалидации кеша для Django ORM.

Краткие разъяснения для тех, кто не читал или просто подзабыл предыдущую статью. Система призвана инвалидировать результаты запросов к базе через ORM по событию изменения объекта модели, автоматически определяя для каждого события результаты каких запросов могли устареть. Так как для автоматической инвалидации требуется хранить дополнительную структурированную информацию кроме, собственно, содержимого кеша, то в качестве бэкенда был выбран Redis. А так как это практический анонс, то я затыкаюсь и перехожу к делу.

Положим вы уже установили сам Redis, Django и у вас есть что-то, что можно кешировать (модели и запросы с их использованием). Установим cacheops:

pip install django-cacheops

или если вы всё же решите покопаться в коде:

git clone git://github.com/Suor/django-cacheops.git
ln -s `pwd`/django-cacheops/cacheops/ /somewhere/on/your/python/import/path/

Далее нам следует его настроить, добавим cacheops в список установленных приложений. Cacheops должен быть инициализирован до загрузки моделей Django, поэтому ставим его первым:

INSTALLED_APPS = (
    'cacheops',
    ...
)

Необходимо также настроить соединение с редисом и профили кеширования:

CACHEOPS_REDIS = {
    'host': 'localhost',      # сервер redis доступен локально
    'port': 6379,             # порт по умолчанию
    #'db': 1,                 # можно выбрать номер БД
    'socket_timeout': 3,
}

CACHEOPS = {
    # Автоматически кешировать все User.objects.get() на 15 минут
    # В том числе доступ через request.user и  post.author,
    # где Post.author - foreign key к auth.User
    'auth.user': ('get', 60*15),

    # Автоматически кешировать все запросы 
    # к остальным моделям django.contrib.auth на час
    'auth.*': ('all', 60*60),

    # Включить ручное кеширование для всех моделей приложения news на час.
    # Инвалидация по-прежнему автоматическая.
    'news.*': ('just_enable', 60*60),

    # Автоматически кешировать все запросы .count() 
    # для оставшихся моделей на 15 минут    
    '*.*': ('count', 60*15),
}

Здесь и далее часто фигурирует время хранения кеша, важно понимать, что это только максимальное ограничение на время жизни, в реальной работе каждая конкретная запись может быть стёрта значительно раньше по событию.

Настройка готова, можно приступать. Собственно, можно уже пользоваться, все запросы указанных типов будут кешироваться и автоматически инвалидироваться при изменении, добавлении или удалении соответствующих моделей. Однако, возможно и более тонкое использование, на уровне отдельных queryset-ов.

В качестве минимальной настройки, которая позволит использовать ручное кеширование, можно использовать:

CACHEOPS = {
    '*.*': ('just_enable', <таймаут по умолчанию>),
}

Это позволит нам писать что-то вроде:

articles = Article.objects.filter(tag=2).cache()

и получить запрос к базе, который с одной стороны будет закеширован, а с другой, кеш которого будет стёрт, при изменении, добавлении или удалении статьи с тегом 2.

В метод .cache() можно передать набор операций, который нужно кешировать и таймаут:

qs = Article.objects.filter(tag=2).cache(ops=['count'], timeout=60*5)
paginator = Paginator(objects, ipp)   # count запрос внутри будет кеширован на 5 минут
artiсles = list(pager.page(page_num)) # а эта выборка кеширована не будет

Набор операций может быть любым подмножеством ['get', 'fetch', 'count'], в том числе пустым — чтобы отключить кеширование для текущего queryset-а. Впрочем, для последнего случая есть шорткат:

qs = Article.objects.filter(visible=True).nocache()

Здесь доступ к содержимому qs пойдёт в базу.

Кроме queryset-ов cacheops умеет работать с функциями, причём может инвалидировать их как некоторый queryset:

from cacheops import cacheoped_as

@cacheoped_as(Article.objects.all())
def article_stats():
    return {
        'tags': list( Article.objects.values('tag').annotate(count=Count('id')).nocache() )
        'categories': list( Article.objects.values('category').annotate(count=Count('id')).nocache() )
    }

Обратите внимание на оборачивание queryset-ов в list() — мы же не хотим положить в кеш объект запроса, который потом будет выполнятся каждый раз при попытке доступа. Также мы используем .nocache(), чтобы не делать лишней работы и не засорять кеш промежуточными результатами.

Пожалуй, я дал достаточно, чтобы желающие могли почувствовать вкус, так что остановлюсь, пока это не стало скучным.

P.S. Для тех, кто хочет интимных подробностей — на гитхабе есть ветка с русскими комментариями.
Tags:
Hubs:
+47
Comments 32
Comments Comments 32

Articles