Так. продолжаем процесс употребления кеширования, основанного на сигналах в 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
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. ;)
Честно говоря не очень понял, какой в этом всем смысл.
Для малонагруженных приложений кеширование не принципиально.
А сильнонагруженные приложения обслуживаются несколькими независимыми процессами, стало быть сигнал в одном процессе никак не инвалидирует кеш в других процессах.
Или я туплю?
finn, вы второй человек который об это спотыкается.
Кеш ОБЩИЙ (memcached) поэтому всё чудно инвалидируется.
Ok. But don't forget backlinks to original:)
Оставлен 10 Март 2008 в 13:30 ¶Автору: Весьма неплохо. Намного масштабней чем было в моём варианте ;)
У меня для блога оно очень важно:)
Не знаю какой бекэнд вы используете, но стандартные джанговские все мульти-процессорные. Значит проблем быть не должно.
Я использую memcached.
Оставлен 10 Март 2008 в 13:39 ¶Спасибо:)
Оставлен 10 Март 2008 в 13:39 ¶Да, протупил.
Кстати, стандартный лжанговский locmem-бекэнд живет в пределах процесса. Там, ЕМНИП, обычная хеш-таблица.
Да, там стандартный dict. Но в документации написано:
Надо разобраться:)
Оставлен 10 Март 2008 в 14:15 ¶А можно пример функции cached и пример с использованием
Оставлен 20 Июль 2008 в 22:26 ¶get_latest_postsиз первого примера. Сижу уже час уже вроде понимаю что такое декораторы, но такую кучу аргументов переварить не могу :(Так вроде бы есть пример в посте:
Напишите поподробней, что конкретно не ясно. Тогда я смогу помочь.
Оставлен 23 Июль 2008 в 15:29 ¶У тебя в заголовке страницы указан неправильный Atom feed:
Не работает автоопределение. Вот тебе и «webnewage». Добро пожаловать в волшебный мир Веб 2.0 :-)
Оставлен 10 Сентябрь 2008 в 16:57 ¶Sorry, markdown $%бал HTML :)
Atom feed:
<link rel="alternate" type="application/atom+xml" title="Кеширование. Инвалидация сигналами. Трапеза | Интернет нового века | webnewage.org" href="/feeds/atom/wna/47/">
Оставлен 10 Сентябрь 2008 в 16:59 ¶А еще, совсем не обязательно, наверное, присылать мне на почту мои же комментарии?
Оставлен 10 Сентябрь 2008 в 17:01 ¶Огромный респект за статью, попровбовал прикрутить к сайту заработало (под виндой). А когда перенес на сервер unix+mode_python обнаружилось что триггеры не срабатыват. Перепробывал все настройки кеша (оперативная память, база даных и файловая система) - но результат идин и тотже: не очищает данные.
Есть у кого-нибудь какие-нибудь предположения куда копать?
Оставлен 18 Январь 2009 в 22:43 ¶Спасибо:-)
Как вы могли заметить статья уже почти годичной давности и с этого времени много воды утекло. Как показала практика, такая реализация системы кеширования не всегда адекватно работает. Она лишь будет справляться со своей задачей при большой интенсивности запросов.
Почему так и как это полечить, я скор напишу новый пост и другую реализацию предоставлю.
Кстати, почему на разных конфигурациях ведет себя по разному - для меня загадка.
Спасибо за замечание. обращу и на это внимание.
Оставлен 19 Январь 2009 в 19:35 ¶сейчас стоит подобная задача у меня, можно тезисно проблемы этого подхода и куда стоит себе копать?
Оставлен 01 Май 2009 в 17:08 ¶Актуальна ли статья в свете выхода django.views.decorators.http.condition django1.1?
Оставлен 29 Октябрь 2009 в 17:06 ¶Конечно. Я описываю кеш на уровне сервера, а http condition это кеш на уровне http протокола.
Оставлен 30 Октябрь 2009 в 03:10 ¶Пингбеки 3
Да, нужно. Для облегчения процесса могу посоветовать — http://webnewage.org/2008/03/09/keshirovanie-invalidatsiya-signalami-trapeza/
[...]Алексей Кошелев написал подробную статью про инвалидацию кешей. Самое интересное (для меня, по крайней мере) в ней то, что там сделана попытка придумать декларативный синтаксис описания процесса инвалидации. И хотя в общем случае такая задача не решается, было[...]
[...]Кеширование. Инвалидация сигналами.[...]
Оставьте комментарий