Cache database results is a must in every mid-big project nowadays. And one of the best db engines for this job is Redis. Even doing a middle implementation to only cache critical entities, it makes a huge perfomance step forward, significantly bigger in concurrent requests.
Similar to Mongo or any other NoSQL database, Redis stores a key and value for every row. Simple.
Take a look to Redis Introduction and I assume you're able to install the server yourself. In Linux should be enough: sudo apt install redis
or redis-server
.
Setting up Redis in Symfony 2.8 or superior become very simple using SncRedisBundle, which gives you a out-of-box configuration to store entities, metadata and queries.
But... I feel so confidence to do this setup by myself and prevent adding another dependency in composer.json
.
This could increase perfomance that we want to get, becouse we'll work directly to Doctrine Cache exactly as expected in every Entity and/or event.
First ensure you've installed doctrine-cache
package:
composer install doctrine-cache
Now go to config.yml
and add the redis connection values from parameters file.
doctrine_cache:
aliases:
redis_cache: redis_provider
providers:
redis_provider:
type: redis
redis:
host: "%redis_host%"
port: "%redis_port%"
database: 0
timeout: 10
persistent: false
In parameters.yml
:
parameters:
redis_host: 127.0.0.1
redis_port: 6379
Next step is enable Second Level Cache and create some regions to store data. Different lifetimes in regions will allow us save static data for more time, increasing the performance of our application by the usage of a memory cache system. For the data that's usually updated frequently we must take care about data-invalidations and deletions when exist relations.
doctrine:
# ...
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
result_cache_driver:
type: service
id: doctrine_cache.providers.redis_provider
query_cache_driver:
type: service
id: doctrine_cache.providers.redis_provider
metadata_cache_driver:
type: service
id: doctrine_cache.providers.redis_provider
second_level_cache:
enabled: true
regions:
default:
lock_lifetime: 10
lifetime: 10
cache_driver:
type: service
id: doctrine_cache.providers.redis_provider
cache_long_time:
lock_lifetime: 60
lifetime: 86400 # 1 day
cache_driver:
type: service
id: doctrine_cache.providers.redis_provider
Above you can see we call redis_provider
created. Therefore we are using a default Doctrine Cache configuration.
In addition we have 2 regions: default
which will be used almost time, and cache_long_time
that will store read-only data structures like languages or system options, for 24 hours.
Let's start get second level cache working in our entities. Supposing we have next entity:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="language")
* @ORM\Entity
*/
class Language
{
/**
* @var integer
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
* @ORM\Column(name="name", type="string", length=45, nullable=true)
*/
private $name;
// Getters and setters...
To make it work, we have to add some comments from ORM\Cache
class. We need to choose a caching mode between READ_ONLY
(default), READ_WRITE
or NONSTRICT_READ_WRITE
. Take a look to differences in Doctrine Docummentation.
Remember you must have enabled annotations in Doctrine configuration. This is the result:
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="language")
* @ORM\Entity
* @ORM\Cache(usage="READ_ONLY", region="cache_long_time")
*/
class Language
{
/**
* @var integer
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
* @ORM\Column(name="name", type="string", length=45, nullable=true)
*/
private $name;
// Getters and setters...
Here we're saving results under cache_long_time
region, if we don't choose one Doctrine will create a region for this entity.
Now we have a relation to other table named locale
which is cached too in Redis. So we must configure cache mode and region to fetch cached Locale entity.
class Language
{
/**
* @var integer
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
* @ORM\Column(name="name", type="string", length=45, nullable=true)
*/
private $name;
/**
* @var \Locale
* @ORM\Cache(usage="READ_ONLY", region="cache_long_time")
* @ORM\ManyToOne(targetEntity="Locale")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="locale_id", referencedColumnName="id")
* })
*/
private $locale;
Be aware of target Entity cache usage and set the same in relation, as well the region.
Note: The target entity must also be marked as cacheable.
Enable cache in Repository classes is pretty simple once previous steps are working. We just must call some methods after getQuery
to set the lifetime, region and choose if store metadata and query output in second level cache. Here you have a simple example:
use Doctrine\ORM\EntityRepository;
use AppBundle\Entity;
class LanguageRepository extends EntityRepository
{
/**
* @param Entity\Locale $locale
* @return array
*/
public function getLanguageByLocale(Entity\Locale $locale) {
$queryBuilder = $this->createQueryBuilder('s')
->innerJoin('s.locale', 'l')
->andWhere('l = :locale')
->setParameter(':locale', $locale)
->orderBy('s.name', 'ASC');
$query = $queryBuilder->getQuery();
// Caching
$query
->setCacheable(true)
->setLifetime(60)
->useResultCache(true, 60)
->setCacheRegion('cache_long_time');
$result = $query->getResult();
return $result;
}
}
Query results are stored in cache for 60 seconds. Play with lifetimes to optimize how long should keep data alive in memory. If data isn't updated frequently (like system options), maybe you would like to keep it in memory for a longer time.
Our main tool to test Redis behaviour on Symfony is the Symfony Profiler Toolbar. This tool tell us how many hits and misses take in place for a request. A hit means result was already cached and fetched successfully. A miss happen when data was not found in cache and Doctrine fetch results and then store them into Redis.
In screenshot above, left side is first request and right side was another request to same path 2 seconds later. We see on first request cache did a miss and two puts (saved into cache) and in second all data was already in memory and therefore a faster query execution. Besides we can see the Doctrine Cache work in detail for every request:
Due to the complex nature of caching systems, take somethings in consideration before implement it:
Redis become more efficient when your app or site will get thousands of requests in a short time, and most of them are concurrent. Take care of data invalidations when update any object and differs from master database. Redis could make a huge perfomance improvement and scalabilty in your project but also be a headache in a bad implementation.