Кэширование данных
Кэширование — это один из способов оптимизации Web приложений. В любом приложении встречаются медленные операции (SQL запросы или запросы к внешним API), результаты которых можно сохранить на некоторое время. Это позволит выполнять меньше таких операций, а большинству пользователей показывать заранее сохраненные данные.
Наиболее популярная технология кеширования для Web приложений — Memcache.
Когда нужно кэшировать
Старайтесь избегать кэширования, пока в этом не будет прямой необходимости. Это простая техника, но это снижает гибкость приложения.
Не делайте лишнюю работу заранее, но закладывайте возможность использования кэширования в будущем:
- Используйте классы или функции, для работы с данными. Не используйте повторяющихся SQL выборок в основном приложении.
- Используйте обертки для работы с внешними API.
Что кэшировать?
Кэшировать нужно данные, которые медленно генерируются и часто запрашиваются. На практике это обычно:
- Результаты запросов к внешним сервисам (RSS, SOAP, REST и т.п.).
- Результаты медленных выборок из базы данных.
- Сгенерированные html блоки либо целые страницы.
Кэширование выборок из баз данных
Запросы к базе данных — наиболее распространенный пример. На основе Мemcache реализуется очень просто:
$q = mysql_query($sql); while ($row = mysql_fetch_assoc($q)) $list[] = $row; memcache_set('online_users', $list, 60*60); } return $list; } $list = get_online_users(); ...
Запрос на получение пользователей кэшируется на 1 час
Обновление данных
Если Вы кэшируете данные, которые могут обновляться, необходимо очищать кэш после каждого обновления:
memcache_connect('localhost', 11211); function get_user($id) { if ( !$data = memcache_get('user' . $id) ) { $sql = 'SELECT * FROM users WHERE id= ' . intval($id); $q = mysql_query($sql); $data = mysql_fetch_assoc($q); memcache_set('user' . $id, $data, 60*60); } return $data; } function save_user($id, $data) { mysql_query('UPDATE users SET ... WHERE id = ' . intval($id)); memcache_delete('user' . $id); }
Кэширование списков
Допустим, Вы кэшируете данные каждого пользователя, как в примере, а также их списки (например, список online пользователей). При обновлении данных пользователя, Вы удаляете данные из кэша только для указанного пользователя. Но его данные могут также присутствовать в списке online пользователей, которые тоже лежат в кэше. Сбрасывать списки при каждом обновлении данных любого пользователя не эффективно. Поэтому обычно используют такой подход:
- Кэшируют списки, которые состоят только из ID пользователей.
- Для вывода списка отправляют отдельный запрос для получения данных каждого пользователя.
Реализация выглядит так:
$q = mysql_query($sql); while ($row = mysql_fetch_assoc($q)) $list[] = $row['id']; memcache_set('online_users', $list, 60*60); } return $list; } $list = get_online_users(); foreach ( $list as $id ) { $user = get_user($id); ... }
Получим список ID пользователей и для каждого из них получим актуальные данные
Для получения данных сразу нескольких объектов можно использовать Multiget.
Повторные запросы
Некоторые данные могут запрашиваться несколько раз в рамках одной страницы, например:
... Email: ... Моя страница ...
Каждый вызов get_user() будет получать данные из кэша. Если Memcache стоит на отдельном сервере, это вызовет большой сетевой трафик и задержки.Чтобы этого избежать, можно использовать дополнительный кэш внутри самого приложения:
memcache_connect('localhost', 11211); function get_user($id) { global $app_cache; if ( $app_cache['user' . $id] ) return $app_cache['user' . $id]; if ( !$data = memcache_get('user' . $id) ) { $sql = 'SELECT * FROM users WHERE id= ' . intval($id); $q = mysql_query($sql); $data = mysql_fetch_assoc($q); memcache_set('user' . $id, $data, 60*60); $app_cache['user' . $id] = $data; } return $data; } function save_user($id, $data) { global $app_cache; mysql_query('UPDATE users SET ... WHERE id = ' . intval($id)); memcache_delete('user' . $id); unset($app_cache['user' . $id]); }
В реальных приложениях, имеет смысл иметь обертку для Memcache с дополнительным кэшом:
$data = memcache_get( $this->resource, $key ); $this->inner_cache[$key] = $data; return $data['value']; } public static function set( $key, $value, $ttl ) { memcache_set($key, $value, $ttl); $this->inner_cache[$key] = $value; } public static function del( $key ) { memcache_delete($key); unset($this->inner_cache[$key]); } }
$inner_cache хранит дополнительный кэш
Внимание. Использование этого подхода может приводить к утечкам памяти в случаях, когда идет работа с большим количеством данных в кэше. Например, в cron-задачах (допустим, мы перебираем всех пользователей для отправки рассылки). Тогда лучше добавить отключение внутреннего кэша:
$data = memcache_get( $this->resource, $key ); $this->inner_cache[$key] = $data; return $data['value']; } public static function set( $key, $value, $ttl ) { memcache_set($key, $value, $ttl); if ( self::$inner_cache_enabled ) $this->inner_cache[$key] = $value; } public static function del( $key ) { memcache_delete($key); unset($this->inner_cache[$key]); } } ... mem_cache::$inner_cache_enabled = false;
Отключаем внутренний кэш
Подогревание
При обновлении особенно тяжелых данных следует использовать не сброс кэша, а прямое обновление данных в нем:
memcache_connect('localhost', 11211); function get_rss($id) { if ( !$data = memcache_get('rss') ) { $data = file_get_contents('http://rss.com/rss'); memcache_set('rss', $data, 60*60); } return $data; } function update_rss_feed($id, $data) { # операции по обновлению внешних ресурсов $data = file_get_contents('http://rss.com/rss'); memcache_set('rss', $data, 60*60); }
Это позволит избежать дополнительной нагрузки при выполнении тяжелых выборок, когда ключ удаляется. Такую методику обычно используют в cron задачах, чтобы периодически обновлять результаты очень тяжелых выборок.Время жизни (ttl)
ttl (время жизни) — это время, после которого, данные будут удалены из кэша. В Memcache устанавливается в секундах:
memcache_set('rss', $data, 60*60);
Установка ttl на 1 час
Чаще всего ttl ставят от нескольких минут до нескольких дней. Не используйте значение 0 (бесконечное хранение), это может засорить память.LRU
Любой кэш работает по принципу вытеснения если ему не хватает памяти. Т.е. если Memcache может использовать максимум 1G памяти, а Вы пытаетесь сохранить ключей на 2G, то половину из этих данных Memcache удалит. Для определения, какие именно ключи удалять, используется алгоритм LRU (Least Recently Used):Memcache постарается удалить прежде всего те данные, которые запрашивались очень давно (т.е. менее популярные удалит, а более популярные оставит).
Кэширование очень медленных запросов
Представьте, что у Вас есть запрос, который выполняется 10 секунд. Вы сохраняете его в кэш на 1 час. Когда проходит это время, данные в кэше удаляются. В первые 10 секунд после этого Вы сталкиваетесь с ситуацией, когда несколько пользователей одновременно вызывают этот тяжелейший запрос. Это может привести к катастрофическим последствиям, т.к. в течение 10 секунд может быть несколько сотен или тысяч таких вызовов.
Чтобы этого избежать, необходимо использовать специальную методику дублирования.
Атомарные операции
Иногда в кэше хранятся счетчики (например, количество пользователей). При добавлении новых пользователей, вместо сброса счетчика и повторной выборки, можно просто увеличить значение кэша на единицу. Но сделать это через приложение нельзя, т.к. это приведет к потере данных от двух одновременно выполненных запросов:
---$count = memcache_get('count'); $count++; memcache_set('count', $count);--- Memcache поддерживает две атомарные операции увеличения и уменьшения чисел: memcache_increment('count');
Увеличит счетчик на 1, функция memcache_decrement() уменьшает счетчик
Самое важное
Кэширование в приложениях на основе Memcache — это очень сильный инструмент. Не забывайте, что Memcache не гарантирует Вам сохранности данных. Это значит, что нельзя рассчитывать на то, что сохраненные на 60 минут данные будут находиться в кэше именно 60 минут.
Этот текст был написан несколько лет назад. С тех пор упомянутые здесь инструменты и софт могли получить обновления. Пожалуйста, проверяйте их актуальность.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: