
La meilleur requête est celle que l’on a pas à faire — un inconnu
Dans mon actuelle mission, nous développons une application sous Symfony2 avec Doctrine comme ORM. Voici grossièrement à quoi ressemble l’architecture globale :
Comme vous le voyez, l’application est dédoublée sur deux serveurs distincts qui ont interdiction de se parler, en dehors de la base de données. Nous avons été confrontés à des problématiques de performances qui nous a contraint d’utiliser le cache de Doctrine.
Il faut savoir qu’il y a 3 types de cache pour Doctrine :
- Query Cache : transformation DQL -> SQL;
- Result Cache : résultat de la requête;
- Metadata Cache : annotation des entities.
Si on regarde la liste des drivers, on s’aperçoit qu’il n’est pas évident de mutualiser du cache entre plusieurs serveur (qui ne peuvent pas communiquer directement ensemble).
C’est là que Redis arrive ♥
En quelques mots, Redis (pour REmote DIctionary Server) est un SGBD clé-valeur qui s’inscrit dans la mouvance NoSQL. En plus d’être simple d’utilisation, sa performance qui ferait pâlir Usain Bolt. Cela est principalement dû au fait que tout est persisté dans le cache du serveur. Si vous doutez encore de cette dernière phrase, sachez que YouPorn, Stack Overflow, Github… l’utilisent ;-)
Voici un exemple de fonctionnement :
> SET name "maxence"OK> GET name"maxence"> KEYS *1) "name"# Assigner un TTL sur une variable> SETEX mission 10 "votre mission si vous l'acceptez... ce message s'autodétruira dans 10s"OK# 10' après, le couple clef/valeur a disparu> GET mission(nil)
Installation
L’installation de Redis est plutôt simple et se fait en quelques lignes de commande :
# downloadwget http://download.redis.io/redis-stable.tar.gztar xvzf redis-stable.tar.gzcd redis-stablemake# installsudo make install# startredis-server
Il va ensuite falloir installer les bundles qui vont bien :
composer require snc/redis-bundle 2.x-devcomposer require predis/predis ^1.0
Et on modifie le app/AppKernel.php:
<?phppublic function registerBundles(){ $bundles = array( // ... new Snc\RedisBundle\SncRedisBundle(), // ... ); //...}
Dans le config.yml :
imports: - { resource: redis.yml }
# Doctrine Configurationdoctrine: dbal: #... orm: auto_generate_proxy_classes: '%kernel.debug%' naming_strategy: doctrine.orm.naming_strategy.underscore # IMPORTANT! auto_mapping: true metadata_cache_driver: redis query_cache_driver: redis
# redis.ymlsnc_redis: clients: default: type: predis alias: default dsn: redis://1.2.3.4 doctrine: type: predis alias: doctrine dsn: redis://1.2.3.4 doctrine: metadata_cache: client: doctrine entity_manager: default document_manager: default result_cache: client: doctrine entity_manager: default query_cache: client: doctrine entity_manager: default
Et voilà pour l’installation.
A ce stade, seuls les caches de metadata et de query sont opérationnels. Pour la mise en cache du
résultat, il faudra le faire manuellement sur chaque requête.
Mettre en cache le résultat
Terminé les requêtes inlines dans les controllers ! Vous allez désormais devoir utiliser le DQL ou le QueryBuilder.
<?phppublic function findBeers(){ $query = $this->getEntityManager() ->createQuery( 'select beers from MaxpouBeerBundle:Beers b' ) ;
$query->useResultCache(true); $query->setResultCacheLifetime(3600); //3600sec = 1 hour
return $query->getResult();}
Maintenant, si l’on recharge la page, cette requête ne se fera plus via MySQL mais bien via Redis.
On peut vérifier tout cela en allant sur Redis et en rentrant la commande suivante : KEYS *
. Voici
ce que l’on va avoir :
> KEYS *1) "[Maxpou\\BeerBundle\\Entity\\Beer$CLASSMETADATA][1]"2) "[809cd863587594a754a7ffda5c2c06ee4640ebe3][1]"
La première ligne va contenir les métadonnées de la classe Beer. La seconde, contiendra la requête
et son résultat. Si vous voulez avoir une clé un peu plus digeste, vous pouvez utiliser cette
méthode $query->setResultCacheId('my_wonderful_key');
ou même faire 1 pierre 3 coups :
$query->useResultCache(true, 3600, 'my_wonderful_key');
.
Voici ce que nous aurons : (je n’ai pas réussi à supprimer le [1]
)
> KEYS *1) "[Maxpou\\BeerBundle\\Entity\\Beer$CLASSMETADATA][1]"2) "[my_wonderful_key][1]"
Pour nettoyer le cache, voici quelques commandes :
# Nettoyer cache des queriesphp app/console doctrine:cache:clear-query# Nettoyer cache des metadatasphp app/console doctrine:cache:clear-metadata# Nettoyer cache des résultatsphp app/console doctrine:cache:clear-result# Vider la base redisphp app/console redis:flushdb
Invalider le cache des requêtes
C’est bien de mettre en place un système de cache, mais vous n’allez pas demander à vos utilisateurs de lancer la commande après chaque opération. Il va donc falloir utiliser les événements de Doctrine, et plus particulièrement l’entity listeners Si vous faites des tests, pensez à commenter le cache des métadonnées ;-)
Définissez le service :
<?php
namespace Maxpou\BeerBundle\Service;
use Doctrine\ORM\Event\LifecycleEventArgs;use Maxpou\BeerBundle\Entity\Beer;
class BeerListener{ private $cacheDriver;
public function __construct($cacheDriver) { $this->cacheDriver = $cacheDriver; }
public function postPersist(Beer $beer, LifecycleEventArgs $args) { $this->cacheDriver->expire('[beers_all][1]', 0); }
public function postUpdate(Beer $beer, LifecycleEventArgs $args) { $this->cacheDriver->expire('[beers_all][1]', 0); }
public function postRemove(Beer $beer, LifecycleEventArgs $args) { $this->cacheDriver->expire('[beers_all][1]', 0); }}
Ici, beer_all
est le nom de l’id assigné au cache.
Le service.yml :
beer_listener: class: Maxpou\BeerBundle\Service\BeerListener arguments: - '@snc_redis.doctrine' tags: - { name: doctrine.orm.entity_listener }
Et enfin l’annotation sur l’entity :
<?php/** * Beer * * @ORM\Table(name="beers") * @ORM\Entity(repositoryClass="Maxpou\BeerBundle\Repository\BeerRepository") * @ORM\EntityListeners({"Maxpou\BeerBundle\Service\BeerListener"}) */class Beer implements BeerInterface{//...
Les plus pointilleux d’entre vous auront remarqués que je redéfini le TTL de ma clef au lieu de la
supprimer. En effet, si je supprime ma clef, la nouvelle clef créée sera [beers_all][2]
. Et le
compteur augmentera ainsi de suite…
Avec cette technique de fainéant, on garde la main sur le nom de la clef qui sera toujours
[beers_all][1]
.
Pour aller plus loin
- Tutoriel/Sandbox Redis : http://try.redis.io
- 2e niveau de cache de Doctrine (encore à l’état expérimental)
Tips
Utiliser les pipes unix avec le redis-cli : echo "KEYS *" | ./path/to/redis-cli | grep beer
Vérifier le TTL d’une clef: TTL [beer-id-42][1]
About the author

Hey, I'm Maxence Poutord, a passionate software engineer. In my day-to-day job, I'm working as a senior front-end engineer at Orderfox. When I'm not working, you can find me travelling the world or cooking.
Follow me on BlueskyRecommended posts

Thomas Harris is a fraudulent YouTuber who uses fake crypto tutorials to spread malware. He artificially inflates his channel's popularity and promotes a malicious script that downloads harmful files and steals crypto. This scam, powered by AI-generated content, highlights the growing sophistication of online threats.

I've been maintaining a massive Vue.js codebase for the last 3 years. Here are 9 lessons I've learned along the way.