Кеширование. Инвалидация сигналами. Трапеза

Так. продолжаем процесс употребления кеширования, основанного на сигналах в django. О теории уже было сказано достаточно, перейдем к практике, а для этого немного пофантазируем.

Итак, у нас есть приложения блога и в нем такие модели:

class Post( models.Model ):
    title = models.CharField(max_length=100)
    date = models.DateTimeField(default=datetime.now)
    text = models.TextField()

class Comment( models.Model ):
    post = models.ForeignKey( Post, related_name = "comments" )
    text = models.TextField()

Теперь как полагается в хорошем блог движке, мы хотим получить последние 5 постов для того чтобы сообщить о них постетителю. Для этого напишем простую функцию

def get_latest_posts():
    return Post.objects.all().order_by( "-date" )[ :5 ]

Отлично, функция готова и мы её можем применять в нужном нам месте. Но зачем каждый раз мучить базу и просить её вернуть последние посты? Ведь пишу то я не часто (ну так получается:)), поэтому можно это дело закешировать и не тратить ресурсы попусту, т.к. они не часто обновляются.

Но классический вариант с таймаутом тут не очень удобен: если сделать маленький таймаут, то падает эффективность кеширования, если же учитывая большие интервалы между добавлением новых постов, то можно и таймаут побольше поставить, но тут сразу появляется проблема. Я добавил новый пост, а кеш обновится только через сутки (ну реально большие интервалы между постингами и таймаут соответствующий:)) - не хорошо, не актуальный контент получается видим на фронте.

Вот тут то и пригодится система кеширования с инвалидацией сигналами. Но перед тем как её применить необходимо произвести небольшой анализ. Анализ того, в каких случаях может измениться последовательность постов? Тут варианта всего 2: либо мы добавили новый пост, либо мы вдруг удалили.

Итак оборачиваем нашу функцию в декоратор cached, который и делает всю магию

@cached( trigger = { "signal" : ( signals.post_save, signals.post_delete ),
                     "sender" : Post  } )
def get_latest_posts():
    return list( Post.objects.all().order_by( "-date" )[:5] )

Вот, мы описали сигналы при которых может произойти изменение данных, передав их описание в качестве параметра trigger декоратора. Теперь при первом выполнение данной функции, возвращенный ею результат будет закеширован, и пока не сработает хоть один из сигналов, больше эта функция выполнятся не будет - всегда будет браться результат из кеша. Ну как?

Вы заметили, что я принудительно обернул результат запроса в list. Это для того чтобы в кеш попали именно уже полученные объекты, а не QuerySet, который ленивый и сам по себе бы запрос не выполнил и объекты не создал:)

Но этот пример достаточно прост, обычно в рабочем коде встречаются более сложные ситуации и функции.

Идем далее. Рассмотрим такой вариант:

@cached( trigger = { "signal" : ( signals.post_save, signals.post_delete ),
                     "sender" : Comment,
                     "suffix" : lambda instance, *args, **kwargs: instance.post.id  },
         suffix = lambda post: post.id )
def get_comments( post ):
    return list( post.comments.all() )

Тут уже функция принимает в качестве параметра объект поста, для которого нужно вернуть комментарии. Понятно, что просто закешироват возвращаемое значение нельзя, т.к. оно уже зависит от параметра. Для такого случая применяет так называемый суффикс. В теории суффикс это некий идентификатор, который добавляется к ключу кеша, для строгого разделения закешированных значений. Понятно, что он строковой. Для того чтобы его создать и применяется функция suffix. Но как вы заметили, их две - одна в параметре декоратора, другая как часть trigger.

Разберем их по-порядку. Итак, функция в декораторе принимает те же аргументы что и главная(та которую декорируем) функция, возвратить она должна либо одно значение, либо последовательность значений, на основе которых и будет составлен суффикс путем преобразования в строку.

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

Поскольку ситуаций, при которых меняются данные, бывает много, а сигналы абсолютна различными, то параметр trigger может принимать как один словарь с описанием сигналов, так и последовательность оных.

Для иллюстрации этого немного изменим предыдущую функцию. Комментарии к посту могут ещё при одной ситуации потерять актуальность - если удалиться сам пост. Так зачем тогда нам забивать ненужными данными кеш? Вот что получается:

@cached( trigger = [ { "signal" : ( signals.post_save, signals.post_delete ),
                       "sender" : Comment,
                       "suffix" : lambda instance, *args, **kwargs: instance.post.id  },

                     { "signal" : signals.post_delete,
                       "sender" : Post,
                       "suffix" : lambda instance, *args, **kwargs: instance.id  }
                   ],
         suffix = lambda post: post.id )
def get_comments( post ):
    return list( post.comments.all() )

Тут мы добавили ещё один триггер, но на этот раз на сигнал удаления поста. (На самом деле, читатель, который хорошо знает устройство джанги, помнит, что джанга пытается поддерживать ссылочную целостность и поэтому к моменту удаления поста, все комментарии к нему уже тоже будут удалены, а значит и данные из кеша будут удалены, и смысла вешать дополнительный обработчик нет. Но будем считать что этого мы не знаем:)А для примера такой код в самый раз.)

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

Но бывают ситуации, когда составление суффикса слишком сложный процесс или невозможный из-за разных контекстов вызова функции и соответствующих сигналов. В таком случае вместо ключа suffix у тригеров можно использовать checker. Это обработчик, который тоже принимает параметры сигнала, но не должен возвращать суффикс. Он должен вернуть True, если требуется обновление кеша или False если не требуется. Суффикс в данном случае игнорируется.

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

Но это всё хорошо, скажите вы, и попросите код для того, чтобы пощупать самим. Пожалуйста. Вся эта функциональность является частью моего пакета django-pantheon. Он доступен а разделе проекты. Именно суб-пакет cache в нем и есть реализация высказанных выше концепций.

Для решивших по-изучать и попробовать код, ещё добавлю, что декоратор 'cached' имеет один дополнительны параметр base_name, где может быть указано базовое имя для ключа в кеше. Если он не указан, то используется название функции и модуля, где она расположена.

Так же, помимо декоратора cached там есть декоратор для завертывания inclusion_tag'ов. Это позволяет вывести на более высокий уровень кеширование - сохранять готовый html.

Вот пример тега, который по аналогии с предыдущей функцией возвращает комментарии:

@cached_inclusion_tag( register,
                       trigger = { "signal" : ( signals.post_save, signals.post_delete ),
                                   "sender" : Comment,
                                   "suffix" : lambda instance, *args, **kwargs: instance.post.id  },

                       suffix = lambda post: post.id,
                       file_name='blog/comments.html',
                       takes_context=True)
def comments_pad( context, post ):
    return { 'comments" : post.comments.all() }

Первый параметр объект Library к которому будет добавлен тег. file_name и take_context это обычные параметры inclusion tags. trigger и suffix выполняют аналогичную роль, что и в декораторе cached.

Ну вот и всё. Я поделился с сами в двух частях своей реализации концепции инвалидации кеша при помощи сигналов в django. Сама идея принадлежит Сергею Кириллову, по крайней мере его мысли на этот счет я увидел первым и от них получил вдохновение. За что ему отдельное спасибо - подбросил мне интересную задачу:)

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

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

Удачи!

PS: подробно про проект django-pantheon тоже в следующих сериях:)

Комментарии 19

  1. zgoda написал:

    Excellent.

    Dare to translate this to English? I understand Russian (well, living nearly 20 years under communist regime at last pays ;)), but I'm a rarity in interweb. ;)

    Оставлен 09 Март 2008 в 23:36
  2. finn написал:

    Честно говоря не очень понял, какой в этом всем смысл.

    Для малонагруженных приложений кеширование не принципиально.

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

    Или я туплю?

    Оставлен 10 Март 2008 в 02:46
  3. Сергей Кириллов написал:

    finn, вы второй человек который об это спотыкается.

    Кеш ОБЩИЙ (memcached) поэтому всё чудно инвалидируется.

    Оставлен 10 Март 2008 в 13:22
  4. Александр Кошелев написал:

    Dare to translate this to English?

    Ok. But don't forget backlinks to original:)

    Оставлен 10 Март 2008 в 13:30
  5. Сергей Кириллов написал:

    Автору: Весьма неплохо. Намного масштабней чем было в моём варианте ;)

    Оставлен 10 Март 2008 в 13:31
  6. Александр Кошелев написал:

    Для малонагруженных приложений кеширование не принципиально.

    У меня для блога оно очень важно:)

    А сильнонагруженные приложения обслуживаются несколькими независимыми процессами, стало быть сигнал в одном процессе никак не инвалидирует кеш в других процессах. Или я туплю?

    Не знаю какой бекэнд вы используете, но стандартные джанговские все мульти-процессорные. Значит проблем быть не должно.

    Я использую memcached.

    Оставлен 10 Март 2008 в 13:39
  7. Александр Кошелев написал:

    Автору: Весьма неплохо. Намного масштабней чем было в моём варианте ;)

    Спасибо:)

    Оставлен 10 Март 2008 в 13:39
  8. finn написал:

    Да, протупил.

    Кстати, стандартный лжанговский locmem-бекэнд живет в пределах процесса. Там, ЕМНИП, обычная хеш-таблица.

    Оставлен 10 Март 2008 в 13:56
  9. Александр Кошелев написал:

    Кстати, стандартный лжанговский locmem-бекэнд живет в пределах процесса. Там, ЕМНИП, обычная хеш-таблица.

    Да, там стандартный dict. Но в документации написано:

    This cache is multi-process and thread-safe.

    Надо разобраться:)

    Оставлен 10 Март 2008 в 14:15
  10. Сябро Максим написал:

    А можно пример функции cached и пример с использованием get_latest_posts из первого примера. Сижу уже час уже вроде понимаю что такое декораторы, но такую кучу аргументов переварить не могу :(

    Оставлен 20 Июль 2008 в 22:26
  11. Александр Кошелев написал:

    Так вроде бы есть пример в посте:

    @cached( trigger = { "signal" : ( signals.post_save, signals.post_delete ),
                         "sender" : Post  } )
    def get_latest_posts():
       return list( Post.objects.all().order_by( "-date" )[:5] )
    

    Напишите поподробней, что конкретно не ясно. Тогда я смогу помочь.

    Оставлен 23 Июль 2008 в 15:29
  12. Big 40wt Svetlyak написал:

    У тебя в заголовке страницы указан неправильный Atom feed:

    Не работает автоопределение. Вот тебе и «webnewage». Добро пожаловать в волшебный мир Веб 2.0 :-)

    Оставлен 10 Сентябрь 2008 в 16:57
  13. Big 40wt Svetlyak написал:

    Sorry, markdown $%бал HTML :)

    Atom feed:

    <link rel="alternate" type="application/atom+xml" title="Кеширование. Инвалидация сигналами. Трапеза | Интернет нового века | webnewage.org" href="/feeds/atom/wna/47/">

    Оставлен 10 Сентябрь 2008 в 16:59
  14. Big 40wt Svetlyak написал:

    А еще, совсем не обязательно, наверное, присылать мне на почту мои же комментарии?

    Оставлен 10 Сентябрь 2008 в 17:01
  15. Алексей написал:

    Огромный респект за статью, попровбовал прикрутить к сайту заработало (под виндой). А когда перенес на сервер unix+mode_python обнаружилось что триггеры не срабатыват. Перепробывал все настройки кеша (оперативная память, база даных и файловая система) - но результат идин и тотже: не очищает данные.

    Есть у кого-нибудь какие-нибудь предположения куда копать?

    Оставлен 18 Январь 2009 в 22:43
  16. Александр Кошелев написал:

    Огромный респект за статью,

    Спасибо:-)

    Есть у кого-нибудь какие-нибудь предположения куда копать?

    Как вы могли заметить статья уже почти годичной давности и с этого времени много воды утекло. Как показала практика, такая реализация системы кеширования не всегда адекватно работает. Она лишь будет справляться со своей задачей при большой интенсивности запросов.

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

    Кстати, почему на разных конфигурациях ведет себя по разному - для меня загадка.

    Спасибо за замечание. обращу и на это внимание.

    Оставлен 19 Январь 2009 в 19:35
  17. OP написал:

    сейчас стоит подобная задача у меня, можно тезисно проблемы этого подхода и куда стоит себе копать?

    Оставлен 01 Май 2009 в 17:08
  18. RomanKRV написал:

    Актуальна ли статья в свете выхода django.views.decorators.http.condition django1.1?

    Оставлен 29 Октябрь 2009 в 17:06
  19. Александр Кошелев написал:

    Актуальна ли статья в свете выхода django.views.decorators.http.condition django1.1?

    Конечно. Я описываю кеш на уровне сервера, а http condition это кеш на уровне http протокола.

    Оставлен 30 Октябрь 2009 в 03:10

Пингбеки 3

  1. От Эффективность кэширования 22 Февраль 2010 в 18:32

    Да, нужно. Для облегчения процесса могу посоветовать — http://webnewage.org/2008/03/09/keshirovanie-invalidatsiya-signalami-trapeza/

  2. От Маниакальный Веблог &raquo; Декларативная инвалидация кеша 24 Январь 2010 в 00:04

    [...]Алексей Кошелев написал подробную статью про инвалидацию кешей. Самое интересное (для меня, по крайней мере) в ней то, что там сделана попытка придумать декларативный синтаксис описания процесса инвалидации. И хотя в общем случае такая задача не решается, было[...]

  3. От links for 2008-05-03 &laquo; Bloggitation 03 Май 2008 в 04:31

    [...]Кеширование. Инвалидация сигналами.[...]

Оставьте комментарий