Un breve tutorial per mostrare la perfetta integrazione tra il database NoSQL Redis e l’ORM Doctrine in un progetto di sviluppo basato su Symfony.

Redis (REmote DIctionary Server) è un engine NoSQL open source nato nel 2009 e scritto dall’italiano Salvatore Sanfilippo. I dati sono immagazzinati nella memoria RAM e ciò rende Redis molto performante per tutte le operazioni di lettura e scrittura; appare subito evidente che uno dei limiti imposti da questa struttura è la dimensione della memoria volatile. Esiste anche un meccanismo di snapshot che, periodicamente, salva i dati su disco fisso per garantirne la persistenza.

Queste caratteristiche ben si sposano con i requisiti richiesti dal motore di caching di Doctrine che si basa su 3 tipologie di cache:
Metadata Cache – caching delle annotazioni, del nome delle tabelle, colonne e relazioni tra entità. Evita che ad ogni richiesta vengano lette e tradotte le caratteristiche delle entità stesse
Query Cache – caching delle traduzioni dei comandi DQL in SQL
Result Cache – caching dei risultati delle query

L’uso e la configurazione di questi 3 livelli di cache consentono di migliorare le performance dell’applicazione.

SncRedisBundle, l’elemento di unione
In ambito di un’applicazione scritta in Symfony, l’unione tra Doctrine e Redis si concretizza grazie ad un bundle open source che con poche configurazioni ci permette di implementare l’integrazione delle prime due modalità di caching. Installazione e configurazione del bundle è ben descritta nella documentazione ufficiale per cui ci limitiamo ad indicare il link da cui attingere le informazioni.
Attivare metadata e query cache è facilissimo e il guadagno in termini di performance è subito evidente. Non ci sono controindicazioni per cui è sempre utile attivare queste funzionalità in ambiente di produzione. L’aspetto che merita un approfondimento è invece la gestione del result cache.

Result cache
L’accesso ai dati della result cache permette di diminuire gli accessi alla basedati ed ottenere dei significativi miglioramenti nella gestione delle risorse. Non c’è una strategia comune con cui gestire questa cache per cui ma c’è soprattutto da far attenzione a invalidare correttamente la cache per non trattare dati non più corretti cioè, appunto, non validi. Per sfruttare il driver di Redis, sono sufficienti un paio di righe di codice. Vediamo un esempio di una semplice funzione di repository:

<?php
namespace FooBundle\Entity;

class FooRepository extends EntityRepository
{
    public function find($id)
    {
        $qb = $this->createQueryBuilder('f')
            ->andWhere('f.id = :id')->setParameter(':id', $id)
        ;
        $qb = $qb->getQuery();

        $cacheDriver = $this->getEntityManager()->getConfiguration()->getResultCacheImpl();
        $qb->setResultCacheDriver($cacheDriver)->useResultCache(true, 3600, 'redis_foo_key');

        return $qb->getOneOrNullResult();
    }
}

Quindi è sufficiente indicare al query builder quale driver da usare.
Il comando setResultCacheDriver accetta 3 parametri:
se usare o meno i risultati in cache
il tempo di validità della cache espresso in secondi
la chiave con cui saranno identificati i dati nella cache. Bisogna far attenzione che la chiave sia univoca per evitare collisioni

Possiamo raffinare la gestione dell’invalidazione della cache creando una classe che sta in ascolto (listener) su un particolare evento di doctrine: onFlush.
Definiamo l’ascoltatore in un file yaml di configurazione di Symfony, services.yml per esempio:

cache_invalidator.listener:
     class: FooBundle\Listener\CacheInvalidator
public: false
     tags:
          - { name: doctrine.event_listener, event: onFlush }

Creiamo poi il listener vero e proprio:

<?php
namespace FooBundle\Listener;

class CacheInvalidator
{
    /**
     * Invalida (svuota) la cache (Redis).
     */
    public function onFlush(OnFlushEventArgs $eventArgs)
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        $cacheDriver = $em->getConfiguration()->getResultCacheImpl();

        $scheduledEntityChanges = array(
            'insert' => $uow->getScheduledEntityInsertions(),
            'update' => $uow->getScheduledEntityUpdates(),
            'delete' => $uow->getScheduledEntityDeletions()
        );

        foreach ($scheduledEntityChanges as $change => $entities) {
            foreach($entities as $entity) {
                if ($entity instanceof Foo) {
                    $cacheDriver->delete('redis_foo_key');
                }
            }
        }
    }
}

Durante il ciclo di vita dell’entity manager e in particolare sul flush, il listener fa uso dello Unit Of Work per andare a leggere le entità che dovranno essere aggiunte (insert/persist), aggiornate (update) o cancellate (delete). Se tra queste c’è un’entità i cui dati sono in qualche modo coinvolti con la cache, invalideremo quest’ultima semplicemente rimuovendo la relativa chiave su Redis che corrisponde proprio a quel terzo parametro descritto prima parlando del comando setResultCacheDriver.

Siti di riferimento:
Sito di riferimento di Redis:
Repository del bundle SncRedisBundle
Documentazione ufficiale della cache di Doctrine