-
Notifications
You must be signed in to change notification settings - Fork 52
Ru: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 запроса.
- Мы сформировали SQL запрос
- Определяем к какой таблице относится, т.е. определяем име ключа по которому хранится версия таблицы
- Пытаемся получить версию таблицы из мемкеша. Если необходимого ключа нет - значит лезим в базу (п. 5)
- Если ключ нашли. То формируем ключ где должен хранится SQL запрос и смотрим есть ли он. Если есть используем, если нет лезим в базу (п. 5)
- Сделав запрос в базу ещё раз проверяем не появился ли ключ версии таблицы. Если не появился - формируем его и сохраняем иначе просто используем уже созданный.
- Формируем с ключём версии ключ для сохранения SQL запроса и сохраняем его.
Для раскешивания всех запросов завязанных на эту таблицу достаточно просто удалить запись о ключе версии или обновить.
Самый простой способ кеширования по тегам - при формировании ключа по которому кладётся результат sql запроса использовать несколько подряд идущих "версий тегов".
Минус в данном подходе это ограничение на длину ключа - в мемкеше это 256 символов. Чем больше тегов тем больше вероятность что мы упрёмся в это ограничение.
Другой вариант реализации тегов - внести теги в хранимый по ключу объект. Таким образом ключ будет иметь вид <префикс><sha от sql>, а хранимы объект выглядеть например так:
[
'tags' => [
'users_list' => 198798739.78687,//какое-то значение по ключу <префикс>tags_users_list
'city_33' => 34998743.897,//<префикс>tags_users_city_33
],
'sqlresult' => <SQL RESULT>
];
Алгоритм кэширования в свою очередь примерно таков:
- Вычисляем ключ по которому должен хранится результат sql запроса.
- Вычисляем теги которыми должен обладать sql запрос
- Пытаемся получить sql запрос по ключу из хранилища, если не получаем то делаем запрос в БД (п. 6)
- Сравниваем ожидаемый список тегов и тот который мы получили из хранилища, если не совпали - делаем запрос в БД (п. 6).
- Для всех тегов получаем актуальные версии по их ключам из мемкеша. Сравниваем версии в запросе и в мемкеше, если они не совпали или в мемкеше нет какого-то тега, то делаем запрос в БД (п. 6). Если всё совпало - считаем результат в мемкеше актуальным.
- Сделав запрос в БД получаем из мемкеша версии тегов по которым должны сохранить результат запроса. Если какого-то тега в мемкеше нету - создаём его с текущим временем.
- Сохраняем результат запроса с тегами из п. 2 и их версиями из п. 6 в виде описаной выше структуры по вычисленому в п. 1 ключу.
P.S. в данный момент этих реализаций в мастере нету. Их можно посмотреть в моей ветке - TaggableDaoWorker и TaggableSmartHandler