Как обычно работает кеш контента: берем данные по ключу из кеша и, если их нет, генерируем новый контент и сохраняем в кеше.
Недостаток: если во время генерации нового контента те же данные понадобятся другому процессу, он повторит ту же процедуру.
Если за время генерации контента поступит много запросов, все они, кроме первого, будут делать пустую работу и мешать друг другу, занимая ресурсы, напрягая БД, обращаясь на другие серверы и т.д.
Чтобы избежать этого, надо при промахе кеша дать понять остальным процессам, что контент скоро будет — надо немного подождать. Я реализовал вот такую логику:
class Cache
{
protected $cache;
protected $locker;
protected $defaultTtl = 120;
protected $magicTtl = 30;
protected $magicText = '<!-- magic string-->';
protected $loopDelay = 0.1;
// ...........
public function fetch($key)
{
while (true)
{
if ($this->cacher->exists($key))
{
$data = $this->cacher->read($key);
if ($data != $this->magicText)
return $data; // данные есть к кеше - выходим
usleep(1E6 * $this->loopDelay); // разгрузим процессор
}
else
{
$this->locker->lock();
if (!$this->cacher->exists($key)) // тест, что между cacher->exists и locker->lock не вклинился кто-то другой
{
$this->cacher->write($key, $this->magicText, $this->magicTtl); // застолбил ключ
$this->locker->unlock();
return null; // выходим для генерации нового контента
}
$this->locker->unlock();
}
}
}
}
Недостаток в том, что на практике она приводит к блокировке процессов. Причем не сразу, а в часы наибольшей нагрузки. Не могу понять, как это происходит — не вижу изъяна в логике.
Класс блокировки такой:
class FileLocker extends BaseLocker // BaseLocker - абстрактный класс
{
protected $fd;
protected $locked;
public function __construct($object = false)
{
$lockfile = $object ? $object : __FILE__; // выбираем файл для блокировки
if (!file_exists($lockfile))
file_put_contents($lockfile, 'lockfile'); // новый файл
$this->fd = fopen($lockfile, 'r'); // открываем для чтения
$this->locked = false;
}
public function lock()
{
flock($this->fd, LOCK_EX); // эксклюзивная блокировка
$this->locked = true;
}
public function unlock()
{
flock($this->fd, LOCK_UN); // снятие блокировки
$this->locked = false;
}
public function isLocked()
{
return $this->locked;
}
public function __destruct()
{
if ($this->locked)
$this->unlock(); // принудительное снятие блокировки
fclose($this->fd); // закрываем файл
$this->fd = null;
}
}
Проверил: файл создается. Проверка по lsof показывает, что все заблокированные процессы держат этот файл открытым.
Прошу посмотреть свежим взглядом — мой уже замылился. Что смог, уже исправил.