До сих пор я использовал кеш memcached. Однако в виду больших возможностей с Redis и лучшей экосистемы и на волне его популярности я решил перейти на него.
В интернете крайне мало материалов по настройке кеша для ZF2 и Doctrine 2. Если и есть материалы, то они не полные или не корректные. Поэтмоу я решил собрать свой опыт в этой статье.
Для начала надо сконфигурировать кеш для Zend2. Тут есть 2 пути. Первый это создание своей фабрики для наполнения \Zend\Cache\Storage\Adapter\Redis конфигурацией через метод getOptions() и далее вызовов методов сеттеров, например setTtl(). Но я считаю этот метод избыточным в виду того, что есть другой путь – опеределить настройки через конфиг.
Для этого надо создать файл config/autoload/cache.local.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
return [ //Zend\Cache\Storage\Adapter\Memcached 'caches' => [ 'memcached' => [ //can be called directly via SM in the name of 'memcached' 'adapter' => [ 'name' => 'memcached', 'options' => [ //2 hours 'ttl' => 7200, 'servers' => [ [ '127.0.0.1', 11211 ] ], 'namespace' => 'ASB2', 'liboptions' => [ 'COMPRESSION' => true, 'binary_protocol' => true, 'no_block' => true, 'connect_timeout' => 100 ] ] ], 'plugins' => [ 'exception_handler' => [ 'throw_exceptions' => false ], ], 'serializer' ], //Zend\Cache\Storage\Adapter\Redis 'redis' => [ 'adapter' => [ 'name' => 'redis', 'options' => [ //2 hours 'ttl' => 7200, 'server' => [ 'host' => '127.0.0.1', 'port' => 6379, ], 'server' => [ 'host' => 'localhost', 'port' => 6379, ], 'namespace' => 'ASB2', 'liboptions' => [ 'serializer' => \Redis::SERIALIZER_PHP ] ] ], 'plugins' => [ 'exception_handler' => [ 'throw_exceptions' => false, ], ], 'serializer' ] ], ]; |
Тут сконфигурирован кеш через memcached и redis адаптеры. Для проверки настроек и примера использования прямо в контроллере написан такой код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * test ZF2 cache * * @return \Zend\Stdlib\ResponseInterface */ public function testCacheAction() { /* @var $redis \Zend\Cache\Storage\Adapter\Redis */ $redis = $this->getServiceLocator()->get('redis'); Debug::dump(get_class($redis)); if (!$check = $redis->hasItem("test")) { $redis->getOptions()->setTtl("60"); $redis->setItem("test", "test123"); } $item = $redis->getItem("test"); Debug::dump($check, $item); return $this->getResponse(); } |
В коде выше в дебаге можно убедиться, что вызывается правильный класс адаптера кеширования и что кеш записывается. Так же у меня установлена админка для redis – phpRedisAdmin. В ней видно список ключей и их значения, а так же время жизни ключа.
Теперь надо настроить redis и memcached для Doctrine.
Для этого нужны будут фабрики. В принципе я не рекомендовал бы использовать анонимные ф-ии в конфигах, т.к. тогда не работает кеш конфигов Zend2, выдается ошибка с Closure.
Создать фабрики можно двумя способами. Способ первый:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
namespace Application\Service\Factory\Doctrine; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; class CacheMemcachedFactory implements FactoryInterface { /** * Create service * * @param ServiceLocatorInterface $serviceLocator * @return mixed */ public function createService(ServiceLocatorInterface $serviceLocator) { $cache = $this->initFromScratch(); return $cache; } /** * @return \Doctrine\Common\Cache\MemcachedCache */ private function initFromScratch() { $cache = new \Doctrine\Common\Cache\MemcachedCache(); $memcached = new \Memcached(); $memcached->addServer('127.0.0.1', 11211); $cache->setNamespace("ASB2"); $cache->setMemcached($memcached); return $cache; } } |
Аналогично для Redis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
namespace Application\Service\Factory\Doctrine; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; class CacheRedisFactory implements FactoryInterface { /** * Create service * * @param ServiceLocatorInterface $serviceLocator * @return mixed */ public function createService(ServiceLocatorInterface $serviceLocator) { $cache = $this->initFromScratch(); return $cache; } /** * @return \Doctrine\Common\Cache\RedisCache */ private function initFromScratch() { $cache = new \Doctrine\Common\Cache\RedisCache(); $redis = new \Redis(); $redis->connect('localhost', 6379); $cache->setNamespace("ASB2"); $cache->setRedis($redis); return $cache; } } |
Далее в файле cache.local.php я регистрирую эти фабрики. В этом файле, чтобы настройки кеша были в одном месте.
1 2 3 4 5 6 7 8 |
'service_manager' => [ 'factories' => [ 'doctrine.cache.redis' => 'Application\Service\Factory\Doctrine\CacheRedisFactory', 'doctrine.cache.memcached' => 'Application\Service\Factory\Doctrine\CacheMemcachedFactory', ] ] |
Почему фабрики называются именно так – станет понятно из конфигурации для Doctrine. Мой файл doctrine.local.php выглядит так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?php return [ 'doctrine' => [ 'connection' => [ 'orm_default' => [ 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', 'params' => [ 'host' => 'localhost', 'port' => '3306', 'user' => 'root', 'password' => 'passwd', 'dbname' => 'dbname', 'charset' => 'utf8', 'driverOptions' => [ 1002 => 'SET NAMES utf8' ], ] ] ], 'migrations' => [ 'migrations_table' => 'migrations', 'migrations_namespace' => 'Application', 'migrations_directory' => 'data/DoctrineORMModule/Migrations/', ], 'configuration' => [ 'orm_default' => [ 'metadata_cache' => 'redis', 'query_cache' => 'redis', 'result_cache' => 'redis', ] ], 'cache' => [ 'redis' => [ 'instance' => 'doctrine.cache.redis', ], 'memcached' => [ 'instance' => 'doctrine.cache.memcached', ], ], ], ]; |
Тут кроме настройки подключения и настройки миграций добавлены настройки кеша. Указывается для Doctrine какие использовать фабрики для создания экземпляра кеша.
И выбирается конкретный экземпляр с помощью которого кешировать. Metadata Сache кеширут данные разметки (маппинга) полученные с аннотаций или с других источников (YAML, XML).
Query Cache кеширует преобразования в DQL. И наконец-то Result Cache ничего не кеширует, а только лишь устанавливает драйвер. Чтобы выполнять кеширование Result Cache, как следует из документации Doctrine и ответов на stackoverflow – это надо делать в ручную.
Как же использовать Result Cache? Вот пример для контроллера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$cache = $this->getEntityManager()->getConfiguration()->getResultCacheImpl(); $cacheItemKey = 'sites'; // test if item exists in the cache if ($cache->contains($cacheItemKey)) { Debug::dump('FROM CACHE'); $items = $cache->fetch($cacheItemKey); } else { Debug::dump('FROM DB'); $items = $siteRepo->findSites(); $cache->save($cacheItemKey, $items, 200); } |
В этом коде получается драйвер (установленный ранее в конфиге) и используется для кеша на 200 секунд.
Но использовать Result Cache в контроллере не лучшее решение, лучше всего этот кеш вызывать в репозитории. В свой базовый класс репозитория я добавил метод, который можно вызывать вместо $query->getResult(), если нужно кешировать результат.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * @param Query $query * @param string $cacheItemKey * @return array|bool|mixed|string */ protected function getResultWithCache(Query $query, $cacheItemKey = '', $ttl = 0) { if (!$cacheItemKey) { $cacheItemKey = get_called_class() . md5($query->getDQL()); } $cache = $this->getEntityManager()->getConfiguration()->getResultCacheImpl(); // test if item exists in the cache if ($cache->contains($cacheItemKey)) { // retrieve item from cache $items = $cache->fetch($cacheItemKey); } else { // retrieve item from repository $items = $query->getResult(); // save item to cache $cache->save($cacheItemKey, $items, $ttl); } return $items; } //....... some find method code $result = $this->getResultWithCache($query, '', 3600); |
Вот и все, теперь настроены кеши для Zend2 и Doctrine2 и этим можно пользоваться. Но в данном конфиге есть некоторые проблемы. Во первых не централизованы настройки кеша.
Во вторых сейчас не определено время жизни по умолчанию для Doctrine кеша. Из-за этого мы имеем бесконечный кеш метаданных в redis.
Чтобы Doctrine конфиг кеша пользовался конфигом кеша Zend имеется класс \DoctrineModule\Cache\ZendStorageCache. Для его использования фабрики кеша меняются на такие
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace Application\Service\Factory\Doctrine; use AppModel\Doctrine\Cache\ZendStorageCache; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; class CacheMemcachedFactory implements FactoryInterface { /** * Create service * * @param ServiceLocatorInterface $serviceLocator * @return mixed */ public function createService(ServiceLocatorInterface $serviceLocator) { $cache = new ZendStorageCache($serviceLocator->get('memcached')); return $cache; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace Application\Service\Factory\Doctrine; use AppModel\Doctrine\Cache\ZendStorageCache; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; class CacheRedisFactory implements FactoryInterface { /** * Create service * * @param ServiceLocatorInterface $serviceLocator * @return mixed */ public function createService(ServiceLocatorInterface $serviceLocator) { $cache = new ZendStorageCache($serviceLocator->get('redis')); return $cache; } } |
В этих новых фабриках берется инстанс кеша Zend, например \Zend\Cache\Storage\Adapter\Redis, и на его основе испольуется кеш для Doctrine.
Почему я указал другой класс AppModel\Doctrine\Cache\ZendStorageCache? Дело в том, что в оригинальном классе \DoctrineModule\Cache\ZendStorageCache в последней версии DoctrineModule 0.8 присутствует баг, из-за которого не устанавливается время жизни при вызове метода $cache->save($cacheItemKey, $items, 200); а берется всегда из настроек.
Исследовав код я нашел причину и исправил этот баг и отправил pull request в DoctrineModule и надеюсь это будет испралвено в 0.9 версии.
Но пока что я решил проблему переопределением.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
namespace AppModel\Doctrine\Cache; class ZendStorageCache extends \DoctrineModule\Cache\ZendStorageCache { /** * {@inheritDoc} */ protected function doSave($id, $data, $lifeTime = false) { if ($lifeTime) { $this->storage->getOptions()->setTtl($lifeTime); } else { $this->storage->getOptions()->setTtl(0); } return $this->storage->setItem($id, $data); } } |
После замены кода фабрик кеш продолжает работать точно так же, как и раньше, но теперь использует настройки из файла cache.local.php. По умолчанию, если не указано время жизни, кеш будет писаться на время указанное в настройках (у меня это 7200 секунд).
UPD: 02.03.2015
Сегодня я обновил мой пулл реквест в связи с тем, что все таки логика работы класса в итоге несколько отличается от той, что я описал.
Дело в том, что в установленной для абстрактного класса ZendStorege сигнатуре метода doSave() параметр $lifeTime идет по умолчанию с 0.
Поэтому все таки по умолчанию кеш записывается со знаением 0, т.е. без истечения срока.
Это во первых. Во вторых я искал способ использовать сконфигурированное в конфиге кеша Zend значение ttl. В итоге я изменил решение на следующее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/** * it's a flag to check if there was a first call already * * @var bool */ private static $firstCallWasSpawned = FALSE; /** * field for saving ttl from zend cache config * * @var null */ private static $savedTtlFromConfig = null; /** * Puts data into the cache. * * @param string $id The cache id. * @param string $data The cache entry/data. * @param int $lifeTime The lifetime. If != 0, sets a specific lifetime for this * cache entry (0 => infinite lifeTime). * If lifetime = -1, sets configured in cache config and * saved in static field ttl value. * * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. */ protected function doSave($id, $data, $lifeTime = 0) { Debug::vars(__METHOD__, $id, $lifeTime); if (!self::$firstCallWasSpawned) { self::$savedTtlFromConfig = $this->storage->getOptions()->getTtl(); self::$firstCallWasSpawned = TRUE; } if ($lifeTime && $lifeTime > 0) { //use ttl passed by parameter $this->storage->getOptions()->setTtl($lifeTime); } else if ($lifeTime === 0) { //use 0 ttl - not expired $this->storage->getOptions()->setTtl(0); } else if ($lifeTime === -1) { //use saved configured ttl if (self::$savedTtlFromConfig !== null) { $this->storage->getOptions()->setTtl(self::$savedTtlFromConfig); } } return $this->storage->setItem($id, $data); } |
Логику работы можно понять из комментария и из моего пояснения к pull request. https://github.com/doctrine/DoctrineModule/pull/485
UPD:
Для GUI Redis я использую следующий проект – https://github.com/ErikDubbelboer/phpRedisAdmin. Он очень похож на phpMyAdmin по стилю и функционалу и очень просто ставится.
Для Memcached раньше я использовал это https://github.com/hgschmie/phpmemcacheadmin, но там отсутствует (или не просто настраиваем) механизм авторизации, и я перешел на следующее рашение – https://github.com/clickalicious/phpMemAdmin