Pull to refresh

И снова о кешировании в Django

Reading time 3 min
Views 7.4K
Для django уже есть множество библиотек для кеширования и они уже обсуждалось на хабре, но, к сожалению, проблемы с производительностью не решить добавлением строчки в INSTALLED_APPS. В библиотеках патчащих queryset кеш инвалидируется либо слишком часто, либо слишком редко и самое главное у программиста мало контроля за этим процессом. Можно написать инвалидацию вручную, но потребуется много кода, в котором легко допустить ошибку.

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

В качестве зависимости можно указать:

  1. Класс модели. При изменении/удалении любого объекта модели, вызова bulk_create, update у queryset'а этой модели запись в кеше будет инвалидирована.
  2. Инстанс модели. При изменении/удалении этого инстанса, запись в кеше будет инвалидирована.
  3. Related manger. При изменении любого дочернего объекта имеющего внешний ключ на указанный объект, запись в кеше будет инвалидирована.

Рассмотрим это на примере простого блога, у которого есть список всех постов и просмотр конкретного поста.

Для начала установим clever_cache.

$ pip instal django-clever-cache

Добавим ‘clever_cache’ в INSTALLED_APPS и укажем ‘clever_cache.backend.RedisCache’ в качестве бэкенда для кеша.

INSTALLED_APPS += ['clever_cache']

CACHES = {
    "default": {
        "BACKEND": 'clever_cache.backend.RedisCache',
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            'DB': 1,
        }
    }
}

Модели в нашем приложении-блоге выглядят следующим образом:

class Post(models.Model):
    author = models.ForeignKey('auth.User')
    title = models.CharField(max_length=128)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = 'post'
        verbose_name_plural = 'posts'
        ordering = ['-created_at']


class Comment(models.Model):
    author = models.ForeignKey('auth.User')
    post = models.ForeignKey(Post, related_name='comments')
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = 'comment'
        verbose_name_plural = 'comments'
        ordering = ['-created_at']

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

class PostListView(ListView):
    context_object_name = 'post_list'

    def get_queryset(self):
        post_list_qs = cache.get('post_list_qs')
        if not post_list_qs:
            post_list_qs = Post.objects.all().select_related(
                'author'
            ).annotate(comments_count=Count('comments'))
            cache.set(
                'post_list_qs',
                post_list_qs,
                depends_on=[Post, Comment, User]
                # Запись в кеше зависит от моделей Post, Comment и User
            )
        return post_list_qs

Реализуем просмотр отдельного поста. Тут мы из базы данных получаем пост, его автора и отдельно комментарии к посту. Соответственно при изменении перечисленных объектов следует инвалидировать кеш.

class PostDetailView(DetailView):
    model = Post

    def get_context_data(self, **ctx):
        post = self.get_post()
        comments = self.get_comments(post)
        ctx['post'] = post
        ctx['comments'] = comments
        return ctx

    def get_post(self, *args, **kwargs):
        pk = self.kwargs.get(self.pk_url_kwarg)
        cache_key = "post_detail_%s" % pk
        post = cache.get(cache_key)
        if not post:
            post = Post.objects.select_related('author').get(pk=pk)
            cache.set(
                cache_key, post,
                depends_on=[post, post.author]
                # при изменении поста или автора удалять запись из кеша
            )
        return post

    def get_comments(self, post):
        cache_key = "post_detail_%s_comments" % post.pk
        comments = cache.get(cache_key)
        if not comments:
            comments = post.comments.all()
            cache.set(
                cache_key, comments,
                depends_on=[post.comments]
                # post.comments - это RelatedManager,
                # при изменении любого комментария поста, кеш будет инвалидирован
            )
        return comments

Надеюсь, эта библиотека избавит вас от проблем с инвалидацией кеша и позволит сосредоточиться на выборе имен переменных.

Код


Доступен на github
Tags:
Hubs:
+13
Comments 6
Comments Comments 6

Articles