Проблема одновременного перестроения кэша

В одном из своих нагруженных проектов я столкнулся с проблемой, когда при истечении срока кеша или при удалении ключа. Большой поток запросов устремлялся к БД. Вследствие чего БД не выдерживала и ложилась.

 Для решения этой проблемы нужно:
  • Создавать блокировочный ключ (что бы остальные потоки не перестраивали кеш)
  • Время ожидания для генерации кеша (что бы остальные потоки ждали)
  • После сохранения данных в кеш сбросить блокировочный ключ



 Так как проект был написан на yii, мы расширили родной CMemCache. И вот что получилось.
<?php

/**
 * Class AMemCached
 */
class AMemCached extends CMemCache
{
 /**
  * @var bool
  */
 public $hashKey = false;
 /**
  * @var string
  */
 public $keyPrefix = '';

 /**
  * Return lock key
  * @param string $key
  * @return string
  */
 protected function getLockKey($key)
 {
  return $key . '_block';
 }

 /**
  *
  */
 public function waitingUnlock($key, $timeout = 3)
 {
  $i = 1;

  while ($i <= $timeout && ($unlock = $this->get($this->getLockKey($key))) === true) {
   sleep(1);
   $i++;
  }

  return $unlock === false;
 }

 /**
  * Lock record
  * @param string $key
  * @param int $timeout
  * @return bool
  */
 protected function lock($key, $timeout)
 {
  return $this->add($this->getLockKey($key), true, $timeout);
 }

 /**
  * Unlock record
  * @param string $key
  * @return bool
  */
 protected function unlock($key)
 {
  return $this->delete($this->getLockKey($key));
 }

 /**
  * Set value with lock
  * @param string $key
  * @param int $expire
  * @param callable $updater
  * @param int $waitTimeout
  * @param int $lockTimeout
  * @return bool|string
  */
 public function setWithWaiting($key, $expire, Closure $updater, $waitTimeout = 3, $lockTimeout = 5)
 {
  if ($this->lock($key, $lockTimeout)) {
   $value = call_user_func($updater);
   $this->set($key, $value, $expire);
   $this->unlock($key);
  } else {
   $value = $this->get($key);
   if (false === $value && $this->waitingUnlock($key, $waitTimeout)) {
    $value = $this->get($key);
   }
  }

  return $value;
 }
}

Для удобства использование создал метод в расширив CActiveRecord:
/**
  * Check and save result on cache
  * @param string $key
  * @param string $expire
  * @param callable $callback
  * @return mixed
  */
 protected function cacheResult($key, $expire, Closure $callback)
 {
    $result = Yii::app()->cache->get($key);
    if ($result === false) {
        $result = Yii::app()->cache->setWithWaiting($key, $expire, $callback);
    }

    return $result;
 }
Теперь им удобно пользоваться:
public function someMethod($limit = 5) {
    // Cache key
    $key = 'cache_' . $limit;

    return $this->cacheResult($key, 43200, function() use($limit, $query) {
        $c = new CDbCriteria();
        $c->order = 't.created ASC';
        $c->limit = $limit;

        return new CActiveDataProvider($this, $c);
});
P.S. Надеюсь кому нибудь поможет это решение.

Комментарии

Популярные сообщения из этого блога

strtolower() strtoupper() и кириллица

Dog-pile effect или блокировка в Memcached

Проблема обновления пакетов composer на Yii2