Memcached для новичков

Статья для новичков. Memcached – это такая штука для кэширования данных в оперативной памяти сервера.

В чем суть. Сайт обычно берет данные из базы, а база — это большой файл на диске, а чтение с диска априори медленнее чем из памяти. Это начинает проявляться не сразу — как только посещаемость переваливает за несколько десятков тысяч человек, а таблицы в базе вырастают до сотен тысяч строк. Более того, сама база данных по определению не эффективна. Допустим, в базе храним 1 000 000 постов, но за последние несколько дней 95% всех просмотров составляют только 100 постов. Но каждый раз нам приходится лезть в огромный файл базы и искать в нем несколько часто запрашиваемых записей — а это увеличивает нагрузку на сервер и время открытия сайта. Если же мы положим эти записи в кэш, то мы ускорим сайт и не нужно покупать мощные сервера. Одним словом, кэш — это профит!

Кэширование бывает разным. Самое простое — кэширование на файлах. Минус в том, что данные по-прежнему хранятся на диске, а это может привести к печальным последствиям. Можно кэшировать промежуточные результаты в базе (например, результаты поиска в некоторых форумных движках хранятся в базе). Ну и самое эффективное это конечно хранение в оперативной памяти. Для этого существует куча сторонних программ: Memcached, eAccelerator, APC, XCache. Кстати, MySQL тоже умеет хранить данные в своем кэше (речь не об индексах в памяти).

Вообще, пишут, что eAccelerator и XCache эффективней чем Memcached, если вы используете один сервер, так как в случае Memcached необходимо открывать TCP-соединение. Но у Memcached есть главное преимущество — это возможность разнесения данных по нескольким серверам. Например, кэш ЖЖ не уместится в памяти ни одного самого мощного сервера. Собственно, Memcached и был придуман для ЖЖ, чтобы можно было хранить данные на нескольких серверах. Но нам, новичкам, пока рано об этом думать.

Особенности Memcached

  • Простая структура хранения данных (ключ-значение).
  • Максимальное время жизни кэша — 30 дней.
  • Максимальный объем одного элемента — 1 Mb
  • Можно хранить объекты, массивы как есть. При кэшировании в файлах или в базе подобные вещи нужно загонять в строку при помощи сериализации перед сохранением.
  • Нет авторизации (пароль-логин). Т.е. если на сервере стоит Memcached, то любой пользователь на этом же сервере может получить к нему доступ.
  • Скорость доступа к данным не зависит от кол-ва элементов в кэше. Да-да, именно так.

Установка

В сети есть куча инструкций по установке, хоть на Unix, хоть на Windows. Кроме самого Memcached нужно еще поставить либу для обращения к Memcached через PHP (по аналогии с базой MySQL — кроме самой базы нужно еще поставить расширение mysql или mysqli).

Но проще всего написать хостеру. На fastvps при заказе сервера Memcached ставят по умолчанию. Главное, указать сколько памяти нужно выделить под кэш. По умолчанию это 67 Mb. У меня 4 Gb оперативы, то можно смело выделить 1 Gb. Вообще, самый простой способ оценить сколько нужно памяти под кэш, это умножить объем базы на 2. Например, базы на всех наших сайтах весят 300 Мб, то под кэш выделяем 600 Мб, а лучше брать 1 Гб, с запасом.

Memcached можно увидеть в phpinfo

Проверка

Обычно Memcached стоит на localhost и доступен через порт 11211
Смотрим статистику

<?php
$memcache = new Memcache;
$memcache->connect('localhost',11211);
print_r($memcache->getStats());
?>

Результат:
Array
(
[pid] => 5915
[uptime] => 583
[time] => 1309538445
[version] => 1.2.2
[pointer_size] => 64
[rusage_user] => 0.000000
[rusage_system] => 0.004000
[curr_items] => 0
[total_items] => 0
[bytes] => 0
[curr_connections] => 1
[total_connections] => 2
[connection_structures] => 2
[cmd_get] => 0
[cmd_set] => 0
[get_hits] => 0
[get_misses] => 0
[evictions] => 0
[bytes_read] => 7
[bytes_written] => 0
[limit_maxbytes] => 1073741824
[threads] => 1
)

Через какое-то время статистика будет выглядеть примерно так

Array
(
[pid] => 5915
[uptime] => 6202245
[time] => 1315740107
[version] => 1.2.2
[pointer_size] => 64
[rusage_user] => 3.464216
[rusage_system] => 10.868679
[curr_items] => 298
[total_items] => 17728
[bytes] => 120366
[curr_connections] => 1
[total_connections] => 28654
[connection_structures] => 4
[cmd_get] => 133296
[cmd_set] => 17728
[get_hits] => 124758
[get_misses] => 8538
[evictions] => 0
[bytes_read] => 11125692
[bytes_written] => 103815319
[limit_maxbytes] => 1073741824
[threads] => 1
)

Оновные параметры:
[curr_items] => 298 — сколько текущих элементов в кэше.
[total_items] => 17728 — сколько всего было элементов в кэше (в том числе и удаленных)
[bytes] => 120366 — сколько байт сейчас лежит в кэше
[limit_maxbytes] =>1073741824 — сколько байт вообще доступно под кэш (тут 1 Gb)
[get_hits] => 124758 — сколько раз мы взяли данные из кэша
[get_misses] => 8538 — сколько раз мы пытались взять данные из кэша, но его там не было или время жизни кэша истекло.

Отношение get_misses/get_hits показывает эффективность использования кэша. Чем оно меньше, тем эффективней используется кэш. В данном случае у нас получается, что 93% данных берется из кэша. Если у вас get_misses/get_hits=1, то значит вы делаете что-то не так (скорее всего ставите слишком малое время жизни кэша).

визуальная статистика
Код выше выводит статистику в сухом виде типа print_r()
Есть красивый вывод статистики — phpMemcachedAdmin

Это обычный php-скрипт. Открываете его у себя на сайте и получаете красивое оформление.
Настраивать ничего не нужно. По умолчанию он коннектится к localhost:11211
Скачать можно на официальной странице.

Примеры использования Memcache

Допустим, у нас есть строка 'test111', мы хотим ее закэшировать на 1 день. Придумаем ей какой-нибудь ключ 'key1'.

<?php
$memcache = new Memcache;
$memcache->connect('localhost',11211);
$memcache->set('key1', 'test111', false, 86400); // кэшируем на 1 день.
$get_result = $memcache->get('key1'); // получаем данные
print_r($get_result);
?>

Усложним немного

<?php
$memcache = new Memcache;
$key = 'key2';
// пытаемся взять данные из кэша
if ($memcache->get($key)) {
     $get_result = $memcache->get($key);
     print_r($get_result);
}
else {
     $result = 'test222'; // $result – результат каких-то вычислений или выборка из БД
     $memcache->set($key, $result, false, 86400);
     echo 'записали кэш на 1 сутки';
}
?>

Только со второго запуска этого скрипта мы увидим наши данные.

Еще забыл добавить про время жизни кэша. Memcached имеет ограничение на время жизни — 1 месяц. Так вот, если вы поставите 365 дней, то Memcached просто не сохранит их, при этом не выдаст ни какой ошибки. Поэтому, если ваши данные долго не меняются и вы хотите поставить максимальный срок жизни, то указывайте false
$memcache->set($key, $result, false, false);

Особенность именования ключей. Лучше в качестве ключа брать md5(ключ), потому что максимальная длина ключа 250 символов и нельзя использовать пробелы. А когда вы будет кэшировать SQL-запросы с условием, то ключ будет типа $key = 'blog_id_1 WHERE activity=1 AND … AND … LIMIT 10'

Более того, в ключ нужно еще добавить какую-то константу, которая определяет, к какому сайт принадлежит кэш.
$key = md5(PROJECT.'key2'); // где константа PROJECT='site.com'

Если этого не сделать, то второй сайт на том же сервере может перезаписать данные первого сайта с тем же ключом. Дело в том, что Memcached не имеет авторизации, как например база данных, поэтому приходится вот таким способом ограничивать доступ. Короче говоря, Memcached — это такая большая свалка пар ключ-значение. Поэтому все сайты хранят кэш в одном Memcached. При этом мы не можем, например, взять 10 последних записанных элементов (типа как в базе LIMIT 10). Структура Memcached необычайна проста, но за счет этого мы получаем высокую производительность.

Тегирование

Так как Memcached чрезвычайно прост (данные никак не связаны между собой — есть только связь ключ-значение), то возникают некоторые трудности на практике. Допустим, у нас есть блоги как на Хабре. Мы написали пост, сохранили его в кэш. Создали несколько пар ключ-значение: кэш под сам пост, кэш для блога, в котором отображается этот пост, кэш для прямого эфира, кэш для вывода постов пользователя в профиле этого пользователя и т.д.

$memcache->set('post_id_2211', 'данные');
$memcache->set('post_blog_id_11', 'данные');
$memcache->set('live_posts', 'данные');
$memcache->set('post_user_id_331', 'данные');

Но вдруг модератор удалил пост. Нам нужно очистить все кэши, которые связаны с этим постом.

$memcache->delete('post_id_2211');
$memcache->delete('post_blog_id_11');
$memcache->delete('live_posts');
$memcache->delete('post_user_id_331');

Короче, из-за простоты Memcached нам приходится ручками плодить лишний код. Более того, мы должны постоянно помнить какие кэши связаны с другим кэшем. Решение проблемы очень простое. Мы к каждому элементу кэша прикрепляем тег или несколько тегов.
Подробнее можно почитать здесь.

Практика

На практике чистый Memcached никто не использует. Обычно используют какой-то класс-обертку, который поддерживает тегирование. Самым распространенным является решение ZendCache

Скачать одним архивом со всем примерами
Положим класс ZendCache в папку lib

Структура должна быть такой, если смотреть от корня
/lib/DklabCache/...
/class/Cache.class.php
/stat.php
/get_post.php
/update_post.php

Класс-обертка (или как еще называют — врапер (wrapper)) с использованием ZendCache

<?php
require_once($_SERVER['DOCUMENT_ROOT'].'/lib/DklabCache/config.php');
require_once($_SERVER['DOCUMENT_ROOT'].'/lib/DklabCache/Zend/Cache.php');
require_once($_SERVER['DOCUMENT_ROOT'].'/lib/DklabCache/Cache/Backend/TagEmuWrapper.php');
require_once($_SERVER['DOCUMENT_ROOT'].'/lib/DklabCache/Zend/Cache/Backend/Memcached.php');

class Cache {

	private $memcache;
	static private $instance = NULL;

	static function getInstance() {
		if (CACHE_USE == false) {
			return NULL;
		}
	    if (self::$instance == NULL) {
	      if (extension_loaded('memcache')) {
	      	  $aConfigMem = array(
					'servers' => array(
						array(
							'host' => MEMCACHED_HOST,
							'port' => MEMCACHED_PORT,
							'persistent' => false
						),
					),
					'compression' => false,
				);
			  self::$instance = new Cache;
		      self::$instance->memcache = new Dklab_Cache_Backend_TagEmuWrapper(new Zend_Cache_Backend_Memcached($aConfigMem));
	      }
	      else {
	      	  return NULL;
	      }
	    }
	    return self::$instance;
	}

	public function get($key) {
		return $this->memcache->load($key);
	}

	public function set($key, $value, $tags=array(), $timeLife=false) {
		return $this->memcache->save($value, $key, $tags, $timeLife);
	}

	public function delete($key) {
		$this->memcache->remove($key);
	}

	public function clean($cMode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {
		return $this->memcache->clean($cMode,$tags);
	}

	public function __construct() {
	}

	public function __clone() {
	}
}
?>

Это у нас класс-синглтон, говоря простыми словами, при первом вызове создается экземпляр класса (читай, подключение к Memcached) и используется при следующем вызове. Таким образом, в пределах одного скрипта мы не плодим лишние подключения и экономим ресурсы.

Константа CACHE_USE прописывается отдельно в конфиге. С помощью нее можно включать/выключать кэширование.

Параметр 'compression' => false означает, что мы не сжимаем данные в кэше. Сжатие нужно для экономии места в памяти, но сжатие требует некоторого времени. Поэтому, если для вас не критичен объем памяти, то сжатее отключаем.

Параметр 'persistent' => false означает выключение постоянного соединения (по аналогии с mysql_pconnect())

Кстати говоря, тут видно как использовать несколько серверов. Если у нас 1 сервер

'servers' => array(
array(
'host' => 'localhost',
'port' => 11211,
'persistent' => false
),
)

Например у нас 3 сервера Memcached

'servers' => array(
 array(
'host' => '11.33.45.11',
'port' => 11211,
'persistent' => false
),
array(
'host' => '11.33.45.12',
'port' => 11211,
'persistent' => false
),
array(
'host' => '11.33.45.13',
'port' => 11211,
'persistent' => false
),
)

По-хорошему, подобные вещи нужно вынести из класса в конфиг.

Теперь подключаем этот класс в скрипт, в котором мы хотим что-то кэшировать
Допустим скрипт вывода поста (в архиве это get_post.php)
Примеры конечно не самые лучшие, но лучше так, чем совсем ничего.
Работу с базой я специально отключил, что бы у вас было меньше проблем.

<?php
header('Content-Type: text/html; charset=utf-8');

define('CACHE_USE', true);
define('PROJECT','site.com');
require_once($_SERVER['DOCUMENT_ROOT'].'/class/Cache.class.php');

// выборка поста по ID
function get_post($postID) {
    $key_cache = md5(PROJECT.'post'.$postID);
    $Cache  = Cache::getInstance();
    if (isset($Cache) && ($data = $Cache->get($key_cache))){
    	echo 'данные взяли из кэша';
        return $data;
    }
    else {
	// находим пост в базе (выполняем select)
	//$data = $DB->selectRow('SELECT * FROM post WHERE id=?d', $postID);

	// для упрощения примера беем готовый массив (пост привязан к блогу с ID=3)
        $data = array('id'=>$postID, 'blog_id'=>3, 'title'=>'Новость 111', 'text'=>'какой-то текст');

        if (!empty($data)) {
            if (isset($Cache)) {
            	$Cache->set($key_cache, $data, array('post_update', 'post_update_'.$postID, 'post_blog_'.$data['blog_id']), 3600*24*10);
            	echo 'сохранили данные в кэш';
            }
            return $data;
        }
        else return null;
    }
}

$postID = 25;
$post = get_post($postID);
print_r($post);
?>

Хотим взять пост с id=25. При первом вызове вы должны увидеть надпись 'сохранили данные в кэш'. При повторных вызовах вы увидите надпись 'данные взяли из кэша'. 

Теперь попробуем обновить пост (запускаем скрипт update_post.php)
<?php
header('Content-Type: text/html; charset=utf-8');

define('CACHE_USE', true);
define('PROJECT','site.com');
require_once($_SERVER['DOCUMENT_ROOT'].'/class/Cache.class.php');

// редактирование поста
function update_post($postID, $blogID, $title, $text) {
	// обновляем пост (выполняем update в базе)
	//$DB->query('UPDATE post SET blog_id =?d, title=?, text=? WHERE id=?d', $blogID, $title, $text, $postID);

	// чистим теги, связанные с эим постом
	$Cache  = Cache::getInstance();
        if (isset($Cache)) {
              $Cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('post_update', 'post_update_'.$postID, 'post_blog_'.$blogID));
        }
	return true;
}

$postID = 25;
update_post($postID, 3, 'test', 'test test');
?>

После чего запускаем скрипт get_post.php и видим, что данных в кэше не было и мы снова сохранили их туда: 'сохранили данные в кэш'.

На самом деле самое сложное в кэшировании, это простановка правильных тегов. Чтобы при обновлении поста обновилась данные, в которых лежит этот пост.

На примере выше это были теги

  • 'post_update_'.$postID — сам пост (обычно страница вывода поста)
  • 'post_blog_'.$blogID — страница блога, где выводится список постов этого блога
  • 'post_update' — тег связанный с главной страницей или списком самых лучших постов

Dog-pile эффект

Переводится как «стая собак». Допустим, у вас есть какая-то сложная выборка из базы на 2 секунды. В кэше ее еще нет (или мы сбросили кэш). Приходит первый пользователь, идет запрос в базу, и только спустя 2 секунды эти данные появятся в кэше. Так вот, за эти 2 секунды на сайт могут зайти еще 10 человек, которые инициируют еще 10 таких же сложных запросов в базу (так как данных в кэше еще нет, а первый запрос всё еще выполняется). Отсюда и получается стая собак, которые нагружают сервак.

выглядит это примерно как-то так:)

Решение проблемы описано здесь.
Код выше не предусматривает защиты от dog-pile эффекта. В моем случае нет такой высокой посещаемости и долгих запросов.

#1

Максимальный объем одного элемента можно изменить в конфиге memcached.

Рустам, 18.12.2011 - 21:14
#2

Хороший пост, спасибо !
Во втором примере добавте только после создания экземляпа объекта "$memcache = new Memcache;" соеденение с сервером.

$memcache->connect('localhost',11211) or die ('Could not connection');

А так все простосупер.

kpoT, 26.03.2012 - 19:30
#3

В чём смысл выделять под кэш памяти в два раза больше, чем размер базы данных?

Reebka, 12.10.2012 - 16:26
#4

Друзья, помогите, пожалуйста!

Как закешировать mysql запрос, выводящий данные в цикле ?
Обычные selectы кеширую нормально, а как запрос с циклом кешировать? Такой к примеру:
$sql="SELECT * FROM rb_user";
$query=mysql_query($sql);
$numrows=mysql_num_rows($query);

if($numrows<1){
это...
}else{
while ($row=mysql_fetch_array($query)) {
это..
}

Статья супер, спасибо автору, только бы с циклом этим разобраться...прошу помощи

olуа, 1.12.2012 - 02:32
#5

сохраняй всю выборку сразу в кэш.

1) для этого в цикле while ($row=mysql_fetch_array($query))
наполняем массив $users

2) создаем массив $data = array($users,$numrows) чтобы в кэше у нас сразу лежала выборка пользователей и кол-ва пользователей.

3) в кэш сохраняем с ключом например 'all_rb_user'. при добавлении или изменении таблицы rb_user не забываем удалять устаревшие данные из кэша $Cache->delete(md5('all_rb_user'))

4) когда мы берем данные из кэша, то мы получаем массив array(пользователи,кол-во пользователей) - смотри шаг 2. для вывода на экран пишем
print_r($data[0]);
echo $data[1];

$key_cache = md5('all_rb_user');
$Cache = Cache::getInstance();
if (isset($Cache) && ($data = $Cache->get($key_cache))){
echo 'данные взяли из кэша';
print_r($data[0]);
echo $data[1];
}
else {
$sql = "SELECT * FROM rb_user";
$query = mysql_query($sql);
$numrows = mysql_num_rows($query);

$users = array();
if ($numrows<1) {
это...
}
else{
while ($row=mysql_fetch_array($query)) {
$users[] = $row;
}
}

$data = array($users,$numrows);
if (isset($Cache)) {
$Cache->set($key_cache, $data, array(), 3600*24*10);
echo 'сохранили данные в кэш';
}
print_r($data);
}

admin, 1.12.2012 - 10:32
#6

admin, спасибо за отклик на вопрос. Подскажите, как сделать не вывод массива на экран print_r($data); а именно данных с таблицы по типу:
$data = array($users,$numrows);
if (isset($Cache)) {
$Cache->set($key_cache, $data, array(), 3600*24*10);
echo 'сохранили данные в кэш';
}
print"Много пользователей: $data[name] ";
}

то есть, чтобы соответственно циклом вывелись на экран все пользователи с таблица (имя, аватар и т.д.) ?

olуа, 2.12.2012 - 12:14
#7

Другими словами, по примеру моего запроса без кеширования работает так:

$sql="SELECT * FROM rb_user WHERE id>200";
$query=mysql_query($sql);
$numrows=mysql_num_rows($query);

if($numrows200 по ка нет...всякая другая информация
}else{
while ($row=mysql_fetch_array($query)) {
print"Пользователь: $row[name] Профиль: $row[link] Аватар: $row[ava]";
}

Собственно, смысл вашего примера уловила, но не получается разобраться, как сделать вывод данных с таблицы ($row[name] и др.) ?

olуа, 2.12.2012 - 12:46
#8

$key_cache = md5('all_rb_user');
$Cache = Cache::getInstance();
if (isset($Cache) && ($data = $Cache->get($key_cache))){
echo 'данные взяли из кэша';
foreach ($data[0] as $row) {
print"Пользователь: $row[name] Профиль: $row[link]";
}
}

admin, 2.12.2012 - 14:54
#9

olуа, я бы порекомендовал вам почитать книжку "Дмитрий Котеров - PHP 5. Наиболее полное руководство"
это книжка хорошо подойдет для вашего уровня.

admin, 2.12.2012 - 14:57
#10

admin, Спасибо большое. То что нужно.

Всего месяц занимаюсь изучением php :-( надеюсь, найду данную книгу в электронном виде, поскольку из-за работы ближайшие 5 недель дома буду только ночевать :(

Ещё раз спасибо!

olуа, 2.12.2012 - 15:09
#11

А что за каша такая где MemcacheD, где Memcache

вы пишете о Memcached или о Memcache ???

уточните плиз - а для старта статья отличная!

Андрей, 8.01.2013 - 22:13
#12

и вопрос - как можно

Максимальный объем одного элемента можно изменить в конфиге memcached???

не нашел такого - натыкаюсь на ограничение объема- где поменять - всё изрыл((

Андрей, 10.01.2013 - 00:15
#13

объем одного элемента в конфиге
memcache.chunk_size=8192

admin, 10.01.2013 - 12:03
#14

Есть ли какой скрипт, чтобы загнать страницы в Memcached и повторять это каждые 3600 сек.

Задача такая: страница должна быть 99% времени в кеше, при времени жизни 3600 сек и отсутствии посещаемости некешированной страницы.

Помогите пожалуйста.

euph0ria, 16.09.2013 - 02:27
Оставить комментарий