Skip to content

Ru:TaggableSmartHandler

Alexey Denisov edited this page Feb 4, 2014 · 10 revisions

TaggableDaoWorker + TaggableSmartHandler

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

Пусть у нас есть две таблицы и соотвествующие бизнес объекты - User и City (таблицы users и cities). Каждый из пользователей имеет обязательное свойство city ссылающееся на город. У этих таблиц/объектов могут быть дополнительные поля/аттрибуты, вроде name.


Если мы хотим получить список всех пользователей, то делаем запрос вида SELECT * FROM users; При этом единственный тэг который можно привязать к такому запросу - это тег с названием таблицы. При изменении любого пользователя запросы по этому тэгу должны раскешиваться.
Тоже самое касается и запросов SELECT * FROM users WHERE users.name = 'Ivan' или, например, ... WHERE users.birthday IS NOT NULL. Все эти запросы привязываются к единственному тегу на основе названия таблицы users.

Если нужно искать пользователей которые живут в конкретном городе SELECT * FROM users WHERE city_id = 33, то запрос будет привязан к тегу "cities|33". Почему такой запрос повешенный на тэг из другой таблицы будет актуален (или что нужно делать что бы он был актуален)?

Обновим пользователя (c id=55) у которого city_id = 33. В таком случае необходимо обновить следующие теги:

  • тег users|55 (т.е. раскешиваем самого пользователя)
  • тег users (нужно сделать неактуальными все общие запросы на таблицу users описанные двумя абзацами выше)
  • теги связанных объектов (городов) на которые ссылается текущий (пользователь), в данном случае cities|33
  • если у пользователя меняется само свойство city_id, то нужно раскешить как тэг старого города так и нового

Т.к. в списке тегов для раскешивания есть cities|33, то приведённый выше запрос будет всегда актуален при измении пользователя связанного с запросом. При этом при изменении пользователя не связанного с городом раскешивание происходить не будет. Тоже самое касается и обновления города. Как только мы изменяем город 33, то раскешиваются запросы с тегом cities|33 и запрос сохранит актуальность при изменении города.


Если попадаются сложные запрос с JOIN'ами, то тэги создаются по названию таблиц, т.к. запрос вида SELECT * FROM users JOIN cities ON cities.id = users.city_id WHERE cities.name = 'Moscow' будет бесхитростно повешен на два тега cities и users. При измении/добавлении записей в любой из этих таблиц следующий такой запрос будет браться из базы.

Плюсы подобного подхода:

  • Не раскешиваются запросы основанные на связях OneToMany (дай мне список пользователей из такого-то города)
  • Запросы на несколько таблиц изменяются при редактировании любой из таблиц

Минусы подобного подхода:

  • Для кэширования необходима хитрая логика анализа SQL запроса что бы решить по каким тегам необходимо закешировать запрос.
  • Для раскешивания необходимо смотреть свойства объекта и соседние с ним объекты.

Механизм кэширования объектов по нескольким тегам

Сначала опишем обычное кеширование объектов и запросов.

У нас есть memcache хранящий данные по ключам.

В простом варианте кэширования обычно ключ по которому хранятся данные состовляется из нескольких частей:

  • статичный префикс
  • префикс идентифицирующий "версию таблицы" (в случае списков)
  • часть уникально идентифицирующая объект (sha/md5 от sql в случае запроса или имя класса + идентификатор объекта в случае объекта)

Что бы раскешить объект мы можем вычислить по нему самому его ключ. Тут всё просто.

Но что бы раскешить sql запросы мы не знаем сколько их в данные момент лежит в мемкеше и по каким ключам они там находятся. То что мы знаем о запросе так это то что он привязан к какой-то конкретной таблице. Введём для всех таблиц "их версию" которую будем хранить по отдельному ключу в мемкеше. Версией будет называть sha от текущего mctime.

Далее рассмотрим алгоритм кеширований sql запроса.

  1. Мы сформировали SQL запрос
  2. Определяем к какой таблице относится, т.е. определяем име ключа по которому хранится версия таблицы
  3. Пытаемся получить версию таблицы из мемкеша. Если необходимого ключа нет - значит лезим в базу (п. 5)
  4. Если ключ нашли. То формируем ключ где должен хранится SQL запрос и смотрим есть ли он. Если есть используем, если нет лезим в базу (п. 5)
  5. Сделав запрос в базу ещё раз проверяем не появился ли ключ версии таблицы. Если не появился - формируем его и сохраняем иначе просто используем уже созданный.
  6. Формируем с ключём версии ключ для сохранения SQL запроса и сохраняем его.

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

теперь перейдём к реализации кеширования с несколькими тегами

Самый простой способ кеширования по тегам - при формировании ключа по которому кладётся результат sql запроса использовать несколько подряд идущих "версий тегов".

Минус в данном подходе это ограничение на длину ключа - в мемкеше это 256 символов. Чем больше тегов тем больше вероятность что мы упрёмся в это ограничение.

Другой вариант реализации тегов - внести теги в хранимый по ключу объект. Таким образом ключ будет иметь вид <префикс><sha от sql>, а хранимы объект выглядеть например так:

[
    'tags' => [
        'users_list' => 198798739.78687,//какое-то значение по ключу <префикс>tags_users_list
        'city_33' => 34998743.897,//<префикс>tags_users_city_33
    ],
    'sqlresult' => <SQL RESULT>
];

Алгоритм кэширования в свою очередь примерно таков:

  1. Вычисляем ключ по которому должен хранится результат sql запроса.
  2. Вычисляем теги которыми должен обладать sql запрос
  3. Пытаемся получить sql запрос по ключу из хранилища, если не получаем то делаем запрос в БД (п. 6)
  4. Сравниваем ожидаемый список тегов и тот который мы получили из хранилища, если не совпали - делаем запрос в БД (п. 6).
  5. Для всех тегов получаем актуальные версии по их ключам из мемкеша. Сравниваем версии в запросе и в мемкеше, если они не совпали или в мемкеше нет какого-то тега, то делаем запрос в БД (п. 6). Если всё совпало - считаем результат в мемкеше актуальным.
  6. Сделав запрос в БД получаем из мемкеша версии тегов по которым должны сохранить результат запроса. Если какого-то тега в мемкеше нету - создаём его с текущим временем.
  7. Сохраняем результат запроса с тегами из п. 2 и их версиями из п. 6 в виде описаной выше структуры по вычисленому в п. 1 ключу.

P.S. в данный момент этих реализаций в мастере нету. Их можно посмотреть в моей ветке - TaggableDaoWorker и TaggableSmartHandler