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

Так. продолжаем процесс употребления кеширования, основанного на сигналах в 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 тоже в следующих сериях:)

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

  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
  20. andrus написал:

    Тема уже, по прошествию времени, возможно неактуальна, но я попробовал приспособить, и в той версии, которую использовал, инвалидатор реализовывался в __call__, то есть, если в процессе Апача, к примеру, то, что декорируем, не вызывалось, то в post_save.receivers, например, инвалидатор не попадал

    Оставлен 05 Август 2010 в 18:22
  21. i.virabyan@gmail.com написал:

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

    Можно поподробней, почему не всегда адекватно? какие тут подводные камни?

    Оставлен 08 Февраль 2011 в 17:10

Пингбеки 4

  1. От Как правильно организовать кэширование модели 27 Август 2012 в 18:19

    а Кошелева по кэшированию в Django.http://webnewage.org/2008/03/09/keshirovanie-invalidatsiya-signalami-trapeza/Сразу отпало много вопросов.Но вот что осталось1) Если пересчитывать queryset каждый раз, после изменения любой из моделей, которая участвует в его формировании, не нагнется ли сервер (вычисление занимает несколько секунд и съедает одно ядро в полку)Т.е., если в какую либ

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

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

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

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

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

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