Comments
Спасибо за ссылку. Изучим.
Реализованный наш метод отличается отсутствием необходимости явно указывать тег при сохранении в кеш. Что тоже может быть полезным в различных случаях.
Интересное решение.
Признаюсь сам не тестировал, потому такой вопрос к автору.
Скажем у нас есть имя пользователя в одной таблице и его адрес в другой, что бы получить за раз Имя и Адрес мы используем два разных запроса с join-нами.
И тут поменяли адрес, и сразу же вызвали те два запроса с join-нами.
Получим ли мы старые данные или новые?
По-моему оно так не сработает, так как ключ формируется с названием одной таблицы…
Возможно я не прав. Повторюсь, что сам не тестировал.
Изначально алгоритм предназначен для кеширования данных каждой модели (таблицы) отдельно (без join'ов). Сделано так в связи с установкой, что join'ы между большими таблицами с динамическими данными — дело медленное, поэтому их лучше избегать.
Но при желании можно развить и для более сложных ситуаций, включая join'ы.
Насколько я Вас понял, в Вашем примере делаются два отдельных запроса (один за именем, один за адресом). Если делать их с join'ами, но использовать для кеширования разные базовые модели (в первом случае — модель таблицы с именем, во втором — модель таблицы с адресом), то при изменении адреса должны будут удалиться из кеша все записи для объекта с адресом. Таким образом при следующем запросе адреса информация в кеше найдена не будет и пройдёт новый запрос в БД на получение обновлённых данных.
(Уточню, что тег для объекта строится на основе имени таблицы и первичного ключа объекта в БД.)
Возможен и другой вариант:
Если результирующий объект, содержащий имя и адрес, собирается сразу из нескольких таблиц (через join), то в этом случае изменение адреса является изменением параметров объекта, и для класса этого объекта во всех методах, где идёт сохранение изменённых параметров в БД, должна вызываться очистка кеша для сохраняемого объекта.
Хорошая заметка. Но все-таки мысью или мысею по древу растекаются. И вообще, эта идиома уже мертва, не надо ее использовать.
Ваш класс содержит потенциально опасные места, которые с некоторой вероятностью станут причиной трудноуловимых глюков.
Например:
public function updateTagList($tag, $cacheId) {
// Получаем список ключе кеширования для тега
$list = $this->getListByTag($tag);
$list[] = $cacheId;
// Добавляем в него новый ключ и пересохраняем список
$this->saveListByTag($tag, $list);
}

При большой нагрузке вполне может сложиться так, что два потока почти одновременно захотят обновить список тэгов и более везучий поток у нас обязательно затрет тег невезучего.
В данном случае можно либо хранить список тегов в текстовом виде и добавлять через memcached::append либо использовать memcached::cas.
Спасибо за отзыв.
Согласен, такие места часто могут появляться при работе с хранилищем данных.
Хоть вероятность появления подобных глюков небольшая, но при увеличении нагрузки и их появлении отловить и понять их становится проблематичным — было дело, уже сталкивались.
По приведённому примеру: последствия глюка в принципе не критичны (кеш используется с небольшим TTL, поэтому через некоторое время всё равно сбросится и данные станут актуальными), и не будут явными для конечных пользователей. Спасибо за подсказку с append и cas. Подумаем, потестируем и попытаемся внедрить, хотя стандартный класс Zend_Cache_Backend_Memcached в ZF это не поддерживает — будем его расширять.

Конечно, в перспективе хотелось бы обеспечить большую стабильность. Не подскажите, какие еще варианты в этом случае возможны? Ведь подобные проблемы (борьба за ресурсы и параллельное выполнение кода) могут возникнуть много где, в том числе и в нашем коде.
С увеличением нагрузки захочется увеличить ttl и ситуация одновременного доступа будет встречаться всё чаще. Тем более, вы выкладываете код на для общего пользования и кто знает где и как его будут применять.
На всякий случай пишу, что cas, append и прочие полезные штуки доступны только в php-расширении memcached. Судя по документации, за работу с ним отвечает Zend_Cache_Backend_Libmemcached, а Zend_Cache_Backend_Memcached работает с memcache, где такие функции недоступны. А для разработки под windows можно использовать эмулятор memcached.
Что ещё можно улучшить не могу сказать, с ZF не работаю и код читал по диагонали.
Мы на нашем проекте как-раз столкнулись с такой же проблемой, но условия были несколько другими — вместо полноценных моделей использовался слой DAO — 1 метод класса = 1 sql запрос, что-то типа getNewsForMainBlock($limit). Поэтому решили отталкиваться от чистого SQL-запроса, выделив роль тегов именам таблиц, которые используются в запросе. При операциях обновления ключи сбрасываются, при выборках, соответственно, устанавливаются.

Мы изначально стремились полностью уйти от потери записей в кэше, отдав под него всю память (исключая выталкивание по LRU) и устанавливая TTL записи в 0. Таким образом, единственно возможная ситуация, при которой ключ пропадал, являлся бы сброс из приложения.

Далее, мы использовали свою расширенную версию адаптера кэширования, во многом похожую на Вашу, но столкнулись с рядом проблем, из-за которых пришлой отказаться от такой схемы хранения тегов. Изначально мы имели в кэше запись вида:
AllCacheTags => [
    tag1 => [
        key_id1,
        key_id2,
        key_id3,
        ...
    ],
    tag2 => [ ... ]
]


Но после внедрения такой схемы, у нас очень сильно поднялся average load на сервере — как выяснилось, у расширения memcache есть параметр compress_threshold, который устанавливает лимит на размер ключа, по достижении которого это значение перед вставкой в memcache будет сжиматься в принудительном порядке, независимо от установленного при вызове add() флага. Так, поскольку чтение и запись этого ключа проходили довольно часто, постоянные операции сжатия/распаковки и давали такой эффект повышения нагрузки.

Также, из-за состояния гонки при записи этого мастер-ключа, мы стали получать значения, которые оставались «намертво» висеть в кэше, не удалявшиеся при сбросе. Как первую ситуацию, так и вторую решили разбиением этого «массива массивов» на составляющие части — 1 тег = 1 запись (хотя первую можно было решить поднятием лимита в параметре).

Пока работает без нареканий :)

Из планов по оптимизации можно выделить внедрение защиты от dogpile эффекта методом имплементации механизма локов и отдачей не-актуальной записи на момент лока.
Only those users with full accounts are able to leave comments. Log in, please.