5 minutos

Redis setting up in Symfony

Caching database results is a must in every mid-big project nowadays. And one of the best engines for this job is Redis. Even with a not totally implementation, it makes a huge perfomance step forward, significatly 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

Symfony

Setting up Redis in Symfony 2.8 or superior become very easy via SncRedisBundle, which gives us a out-of-box configuration to store entities, metadata and queries.

But... I feel so confidence to do this myself without adding another dependency in composer.json.

This could increase perfomance that we want to get, becouse we'll work directly with 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 than dynamic, making sense usage of a memory cache system.

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 before. Therefore we are using default Doctrine Cache configuration. That's cool, trust me! ;)

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.

Entities engage Cache

Let's start get second level cache working in any of 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 setup 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.

Cache associations

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.

Custom Queries

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 long time.

Debugging Redis

Our main tool to test Redis working is the Symfony Profiler Toolbar. This tool will tell us how many hits and misses take place in cache. A hit means result was already cached and fetched successfully. A miss happen when data was not found and Doctrine do the query and then cache result into Redis.

Redis get miss and none on next request 2 seconds later

In screenshot above, left side is first request and right side was taken 2 seconds later. We see on first request cache did a miss and two puts (means save into cache) which in second all data was already in memory. Besides we can see the Doctrine Cache work in detail for every request:

Doctrine Cache Symfony Profiler

Due to the complex nature of caching systems, take somethings in consideration before implement it:

  • It is not so much bad that a single cache miss. They sadly happen quite frequently depending on memory usage and TTL lifetimes.
  • Having data already loaded usually results in free instructions. Since you can store n values in cache but only pay the cost for a single load.

Final thoughts

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.