Pull to refresh

Memcached — стратегия кеширования

Reading time 6 min
Views 17K
Хочу поприветствовать хабросообщество. Из приятных впечатлении при регистрации на Хабре — так это атмосфера сказочности, которая бывает только в старых добрых сказках из советского Кинофильма.
Итак, слезы умиления прошли, приступаем. Ниже топик, который привел к инвайту на Хабр.

Memcached применяется для кеширования данных. Это делается для того, чтобы избежать лишних обращений к базе данных, т.е. в Memcached сохраняют результаты запросов. Это ускоряет работу сайта и уменьшают время выдачи страниц.
Кеш кроме преимуществ имеет свои недостатки. Одна из проблем кеша — это его актуальность. В режиме работы «только чтение» трудностей не возникает. Если же мы имеем дело с данными, которые изменяются, или изменяются часто, то эффективность кеширования резко падает. Чем чаще данные меняются, тем менее эффективен кеш. Обычно кеш сбрасывается после первого же изменения. Причем происходит сброс сразу всех закешированных данных. После сброса запросы идут к базе данных и по-новой происходит наполнение кеша. Если еще одно изменение — то кеш снова сбрасывается. Часто оказывается, что такая хорошая вещь как memcached не приносит никакой пользы для производительности сервера, и к тому же влечет за собой еще дополнительные расходы на память, процессорное время.
Один из методов решения данной проблемы — это логическое разделение кеша на независимые части. Если сброс кеша происходит, то только для той части, которая изменилась.

Рассмотрим один из таких подходов в связке Memcached — БД

Если мы будем делать логическое разделение по запросам, то встает вопрос о том, как и что разделять, насколько часто обновлять. Здесь необходимо давать подсказки по каждому запросу, поскольку назначение запросов разное и непонятно какие запросы обновлять и при каких событиях. Это требует больших усилий для внедрения — и мне, как ленивому программисту, это не интересно.

Давайте разделим все обращения к базе по таблицам.

Допустим есть у нас запрос с обращением к нескольким таблицам. Мы берем запрос, анализируем какие в нем таблицы, смотрим, изменились ли в таблице данные. Если данные изменились, то кеш для запроса тоже обновляем. Звучит немного сложновато, возникают вопросы — каким образом это все делать, но в конечном счете реализация довольно простая.

Приступим к наброскам:

* Каждая таблица у нас будет иметь счетчик, который меняется каждый раз когда данные в таблице изменяются.
* Когда мы выполняем удаление и вставку строк, когда записи изменяются — мы увеличиваем эти счетчики.
* Перед выполнением запроса из него берется список затрагиваемых таблиц. По этим таблицам находим значения счетчика. Формируем эти значения в одну строку и добавляем как комментарий к запросу.

Вот и все сложности с таким подходом. Чтобы перейти к новой политике кеширования нам достаточно внести небольшие изменения в код. Пример, демонстрирующий этот подход, предоставлен ниже. Этот пример полностью самостоятельный и может быть выполнен если у вас есть PHP с поддержкой расширений mysql и memcache.
Такой подход увеличивает эффективность кеширования данных. При сбросе кеша удаляются только те данные, которые относятся к измененным таблицам. Если быть более конкретным, то слова «сброс кеша» теряют смысл, измененные данные становятся недоступными и продолжается наполнение кеша по новым ключам для тех же запросов. Если у вас есть «гадкая» таблица, из-за которой часто сбрасывается весь кеш, то теперь такая таблица не будет портить вам всю картину.

Метод жизнеспособен, он был опробован на одном из сайтов(http://www.skachatreferat.ru). Опыт показал, что не следует пренебрегать другими методами кеширования. Что для данных, чья актуальность не критична при частоте обновления раз в 5 минут, лучше применять самое простое кеширование с установкой времени жизни кеша в заданный период, в данном случае это 5 минут.

Возьмем habrahabr, который предоставляет доступ к статьям. Здесь каждая статья представляет собой текстовое поле и набор каких-то атрибутов. Текст меняется редко, в то время как атрибуты статьи меняются часто. По этой причине есть смысл поместить только текст статьи в кеш, а атрибуты независимо выбирать из таблиц. В результате скорость доступа к данным выростает на порядок.

Чем меньше столбцов мы выбираем, тем лучше для производительности. MySQL работает со столбцами с данными простого типа на порядок быстрее, чем со столбцам типа TEXT(где у нас хранится текст статьи). За счет использования этих особенностей достигается значительный выигрыш в производительности.

Ниже расположен скрипт для демонстрации метода разделения кеша по таблицам, исходник которого был вам обещан. Скрипт полностью самостоятельный и не требует каких-либо дополнительных модулей. Не забудьте указать данные для mysql и memcache в начале скрипта:
  1. <?
  2.  header('Content-type: text/html; charset=UTF-8');
  3.  $mysql_host='localhost';
  4.  $mysql_username='root';
  5.  $mysql_password='12345';
  6.  $mysql_database='test';
  7. //укажите имена двух таблиц, эти таблицы не изменяются в этом примере
  8.  $mysql_table1='table1';
  9.  $mysql_table2='table2';
  10.  $memcache_host='localhost';
  11.  $memcache_port=11211;
  12.  
  13.  $mysql=mysql_connect($mysql_host,$mysql_username,$mysql_password);
  14.  if(!$mysql)
  15.   die("Невозможно подсоединиться к MySQL: $mysql_username@$mysql_host/$mysql_password");
  16.  if(!mysql_select_db($mysql_database))
  17.   die("Невозможно подсоединиться к базе данных: $mysql_database");
  18.  $memcache = new Memcache;
  19.  if(!$memcache->pconnect($memcache_host,$memcache_port))
  20.   die("Memcached не доступен: $memcache_host:$memcache_port");
  21.  
  22.  function cacheGet($key)
  23.  {
  24.         global $memcache;
  25.         return $memcache->get($key);
  26.  }
  27.  function cacheSet($key,$data,$delay)
  28.  {
  29.         global $memcache;
  30.         return $memcache->set($key,$data,0,$delay);
  31.  }
  32.  
  33.  function sqlExtractTables(&$query)
  34.  {
  35.         preg_match_all("/\\<\\<([A-Za-z0-9\\_]+)\\>\\>/",$query,$tables);
  36.         if(!$tables[1])
  37.                 die("Запрос не содержит таблиц, доступные для распознавания вида '<<table_name>>': $query");
  38.         $query=preg_replace("/\\<\\<([A-Za-z0-9\\_]+)\\>\\>/","\\1",$query);
  39.         return $tables[1];
  40.  }
  41.  
  42.  function sqlQuery($query)
  43.  {
  44.         $resource=mysql_query($query);
  45.         if(!$resource)
  46.                 die("Неправильный запрос: $query <br> ".mysql_error());
  47.         echo "<b>Запрос был выполнен:</b> $query<br>";
  48.         return $resource;      
  49.  }
  50.  
  51.  function sqlSet($query)
  52.  {
  53.         $tables=sqlExtractTables($query);
  54.         foreach ($tables as $table)
  55.                 cacheSet($table,uniqid(time(),true),24*3600);
  56.         return sqlQuery($query);
  57.  }
  58.  function sqlGet($query)
  59.  {
  60.         $tables=sqlExtractTables($query);
  61.         foreach ($tables as $table)
  62.                 $appendix.=cacheGet($table);
  63.         $appendix="/*".md5($appendix)."*/";    
  64.         $query=$query.$appendix;
  65.         $cache_key=md5($query);
  66.         $result=cacheGet($cache_key);
  67.         if($result!==false)
  68.         {
  69.                 echo "<b>Попадание в кеш:</b> $query<br>";
  70.                 return $result;
  71.         }
  72.         else
  73.                 echo "<b>Кеш не сработал:</b> $query<br>";
  74.         $resource=sqlQuery($query);
  75.         $result=array();       
  76.         while ($row = mysql_fetch_assoc($resource))
  77.         {
  78.                 $result[]=$row;
  79.         }      
  80.         cacheSet($cache_key,$result,3600);
  81.         return $result;
  82.  }
  83.  ?>
  84.  <h2>Демонстрация. Разделение кешированных запросов по таблицам</h2>
  85.  <h3>Делаем 2 запроса</h3>
  86.  <?
  87.  sqlGet("select * from <<$mysql_table1>> limit 1");
  88.  //обычно это селекты вида "select * from <<$mysql_table1>> where id=1", здесь так дано чтобы не надо было привязываться к конкретным столбцам
  89.  ?><br><?
  90.  sqlGet("select * from <<$mysql_table2>> limit 1");
  91.  ?>
  92. <h3>Меняем одну из таблиц</h3>
  93.  <?
  94.  sqlSet("delete from <<$mysql_table2>> where 1=0");
  95.  ?>
  96. <h3>Выполняем те же запросы опять</h3>
  97.  <?
  98.  sqlGet("select * from <<$mysql_table1>> limit 1");
  99.  ?><br><?
  100.  sqlGet("select * from <<$mysql_table2>> limit 1");
  101.  ?>
  102. <h3>Результат: второй запрос должен быть выполнен снова, минуя кеш. Первый запрос продолжает браться из кеша</h3>



исходник здесь: www.skachatreferat.ru/demo.txt
Tags:
Hubs:
+17
Comments 88
Comments Comments 88

Articles