Pull to refresh

Использование составных ключей для манипуляции данными в memcached

Reading time 7 min
Views 3.1K
Часто, при работе с memcached, возникает ситуация, когда необходимо удалить данные в самых различных местах. Например, при добавлении нового комментария, необходимо обновить не только кеш самих комментариев этой страницы, но и ленты комментариев на главной странице, списока комментариев пользователя, счетчика комментариев пользователя, общего счетчика комментариев сайта, счетчика комментариев статьи и т.д. Можно запомнить все ключи этих данных и множество раз вызвать delete() с этими ключами.

<?php
$cache->delete('comments_art123');
$cache->delete('comments_tape');
$cache->delete('comments_user123');
$cache->delete('comments_counters_art123');
$cache->delete('comments_counters_user123');
......
?>


* This source code was highlighted with Source Code Highlighter.


Как известно, memсached хранит данные плоско, то есть одному ключу соответствует всегда одно значение. Вложенных ключей не существует. Так же нет возможности удалить группу ключей, скажем по маске. Хорошо бы было, если бы можно было сделать, например, так: $cache->delete('comments*'); Но так нельзя.

Но если нельзя, но очень хочется, то можно ;)

Задача: Реализовать возможность групового удаления данных из кеша
Решение: Будем хранить признак удаленности той или иной группы в отдельном месте кеша, и факт наличия данных будем определять не по факту наличия самих данных, а по факту наличия признака. Назовем это место кешхеадер. Для простоты будем хранить там обычный хеш. Для примера с комментами хеш будет выглядеть так:

array(
'comments' => array(
  'art123' => array(),
  'tape' => array(),
  'user123' => array(),
  'counters' => array('art123' =>array(), 'user123' => array(),),
  )
);


* This source code was highlighted with Source Code Highlighter.


Таким образом, что бы пометить некую группу как удаленную, нам надо просто удалить эту ветку из кешхеадера. Например если мы хотим обновить счетчики, удалим ключ 'counters', и тогда наш кешхеадер бедет выглядить так:
array(
'comments' => array(
  'art123' => array(),
  'tape' => array(),
  'user123' => array(),
  )
);

* This source code was highlighted with Source Code Highlighter.


Это будет знаком, что данные надо вытащить из базы, и положить в кеш, добавив, конечно же, соответствующую информацию в кешхеадер.

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

Итак, код класса, реализующего описанное выше:

<?php

/**
* Класс для работы с memcached, поддерживающий групповое удаление кеша.
*
* @author rvk
*/
class MemcacheCacher implements DataCacher {

  protected $daemon = null;
  protected $key = '';
  protected $header = array();
  const HEADER_KEY_NAME = 'cacheheader';

  public function __construct($key) {
    $this->key = explode('|', $key);

    $this->daemon = new Memcache();
    $this->daemon->connect('localhost', 11211) or die ("Could not connect");

    //Инициализируем кэшхеадер. В нем будем хранить признак смерти элементов
    $this->header = $this->daemon->get(MemcacheCacher::HEADER_KEY_NAME);
    if (!is_array($this->header)) {
      $this->daemon->add(MemcacheCacher::HEADER_KEY_NAME, array(), false, 10000);
      $this->header = $this->daemon->get(MemcacheCacher::HEADER_KEY_NAME);
    }

  }

  /**
   *
   * Устанавливает значение элемента
   *
   * @param mixed $data
   */

  public function set($data) {
    $this->createHeaderElement();
    $this->daemon->set(implode('|',$this->key), $data, false, 10000) or die ("Failed to save data at the server");
  }

  /**
   *
   * Возвращает значение по ключу
   *
   * @return mixed
   */

  public function get() {
    if ($this->exists()) {
      return $this->daemon->get(implode('|',$this->key));
    }

    return null;
  }

  /**
   *
   * Проверяет значение элемента
   *
   * @return bool
   */

  public function exists() {
    $element = $this->getHeaderElement();
    return is_array($element);
  }

  /**
   * Удаляет элемент. На самом деле он остается в памяти, но просто помечается удаленным в кешхеадере.
   * Соответвенно, все вложенные элементы тоже оказываются удаленными
   */

  public function del() {
    if ($this->exists()) {
      $element = $this->setHeaderElement(null);
      $this->daemon->set(MemcacheCacher::HEADER_KEY_NAME, $this->header, false, 10000);
    }
  }

  /**
   * Очищает кеш
   */
  public function flush() {
    $this->daemon->flush();
  }

  /**
   *
   * Возвращает ссылку на текущий элемент из кешхеадера
   *
   * @return mixed
   */

  protected function & getHeaderElement() {
    $header = & $this->header;

    foreach ($this->key as $subkey) {
      if (!isset ($header[$subkey])) {
        return $header[$subkey];
      }
      $header = & $header[$subkey];
    }
    return $header;
  }

  /**
   *
   * Устанавливаем значение текущего элемента
   *
   * @param mixed $value
   * @return mixed
   */

  protected function setHeaderElement($value) {
    $element = & $this->getHeaderElement();
    $element = $value;
    $this->daemon->set(MemcacheCacher::HEADER_KEY_NAME, $this->header, false, 10000);
  }

  /**
   * Создает элемент в кешхеадере и все элементы верхнего уровня
   */

  protected function createHeaderElement() {
    $header = & $this->header;

    foreach ($this->key as $subkey) {
      if (!isset ($header[$subkey])) {
        $header[$subkey] = array();
      }
      $header = & $header[$subkey];
    }
    $this->daemon->set(MemcacheCacher::HEADER_KEY_NAME, $this->header, false, 10000);
  }

}
?>

* This source code was highlighted with Source Code Highlighter.


Что бы обновить все, что относится к комментариям надо выполнить только вот такой код:

$cache->delete('comments');

или обновляем только счетчики

$cache->delete('comments|counters');

ну или только счетчик пользователя

$cache->delete('comments|counters|user123');

Лучше всего о работе класса расскажет юнит-тест (они вообще часто лучше любой документации):

<?php
class TestOfMemcacheCacher extends UnitTestCase {

  public function setUp() {
    $this->cache = new MemcacheCacher('key');
    $this->cache->set(1);
  }

  public function testOfCacheExists() {
    $cache = new MemcacheCacher('key');
    $this->assertTrue($cache->exists());
    $cache = new MemcacheCacher('key1');
    $this->assertFalse($cache->exists());
  }

  public function testOfCacheDelete() {
    $cache = new MemcacheCacher('key');
    $this->assertTrue($cache->exists());
    $cache->del();
    $this->assertFalse($cache->exists());

  }

  public function testOfCacheGet() {
    $cache = new MemcacheCacher('key');
    $this->assertEqual($cache->get(), '1');
    $this->assertNotEqual($cache->get(), '2');
  }

  public function testOfTreeCacheExists() {
    $cache = new MemcacheCacher('key|key1');
    $cache->set(123);
    $this->assertEqual($cache->get(), '123');
    $this->assertTrue($cache->exists());
    $cache = new MemcacheCacher('key');
    $cache->del();
    $cache = new MemcacheCacher('key|key1');
    $this->assertFalse($cache->exists());
  }

  public function tearDown() {
    $this->cache->flush();
  }

}

?>


* This source code was highlighted with Source Code Highlighter.


Подобный подход с успехом работал на проекте photofile.ru под нагрузкой около 4-х млн хитов в сутки.

P.S Не стоит относится к коду выше как полностью законченному. Это пример, его можно и нужно оптимизировать.
Tags:
Hubs:
+16
Comments 52
Comments Comments 52

Articles