Je suis actuellement en train de travailler sur une couche d’abstraction de données (pour améliorer celle de mon framework afin de la rendre plus transparente).
J’ai buté sur un problème assez classique, que je vais partager avec vous car cela pourrait vous intéresser.
Ma couche d’accès aux données sert habituellement à faire des requêtes sur une base SQL. Afin de réduire le nombre de requêtes réellement effectuées, et d’améliorer les performances, cette couche a la capacité de mettre les résultats des requêtes en cache. Ce cache est basé sur Memcache, qui garde les données en RAM pendant une durée maximale définie, et la bibliothèque PHP qui y accède sérialise les données sans qu’on ait à s’en préoccuper.
Mais comment stocker ces données en cache ? Memcache permet d’enregistrer une variable en la nommant par une chaîne de caractère.
Ainsi, si je vais récupérer le contenu d’un enregistrement d’une table à partir de sa clé primaire, je commence par créer une chaîne constituée de la sorte :
__dao:nom_de_la_base:nom_de_la_table:get:clé_primaire
Ensuite, je vais regarder si j’ai une donnée en cache disponible avec ce nom. Si ce n’est pas le cas, je fais la requête, stocke son résultat en cache puis le retourne.
(bon, vous aurez compris que lorsque je parle à la première personne, c’est pour me mettre à la place du code ; c’est bizarre comme habitude, mais on fait tous ça, hein)
Mais dans le cas où on fait une modification sur les données de la table, il est impossible de savoir à l’avance si les données préalablement récupérées sont toujours valables ou non. La démarche la plus logique est donc de vouloir tout invalider d’un coup. On se heurte alors à une limitation de Memcache ; il est impossible de récupérer, modifier ou effacer plusieurs variables de manière groupée. C’est dommage, parce qu’il serait très pratique de pouvoir écrire quelque chose du genre :
$cache->deleteFromPrefix('__dao:nom_de_la_base:nom_de_table');
Si on prend le temps de réfléchir, il n’existe pas des milliards de manières de solutionner ça. En fait, je vois trois grandes possibilités.
Tricher pour contourner le problème
Le plus simple, c’est déjà de faire ce qu’il faut pour ne pas avoir à gérer l’expiration des données. La plupart du temps, on peut se permettre d’afficher des données qui ne soient pas complètement à jour. Par contre, sur un site à très fort trafic, il reste important de ne pas frapper la base de données à chaque affichage de page.
On peut alors diminuer la durée pendant laquelle les données sont mises en cache, à seulement quelques minutes. C’est suffisant pour alléger considérablement le travail de la base. Et une fois que le délai est écoulé, les données se mettront automatiquement à jour.
C’est idiot, mais c’est souvent très efficace. Toutefois, je privilégie ce type de fonctionnement pour du cache applicatif, quand on peut choisir d’appliquer ce genre de stratégie. Pour du cache géré au niveau d’un couche d’accès au données, c’est déjà un peu plus scabreux, car certaines applications pourraient avoir besoin d’avoir des données systématiquement à jour.
Tricher pour contourner le problème (bis)
Extension directe de la méthode précédente, on pourrait simplement se dire que la couche d’accès aux données offrirait la possibilité de désactiver l’utilisation du cache. Ainsi, une application qui aurait des besoins « temps réel » pourrait imposer des requêtes systématiques, quitte à mettre en place du cache au niveau applicatif.
Là encore, c’est simple, c’est con, c’est bourrin, mais qu’est-ce que ça marche bien !
Bon, il n’empêche que ce serait quand même bien de trouver une « vraie » solution.
Lister les variables
Une solution évidente consiste à ajouter une variable de cache supplémentaire, qui contienne simplement la liste des noms des autres variables de cache. Quand on se retrouve à modifier des données, on commence par récupérer cette liste, puis on la parcourt pour effacer les variables une à une (puis on efface la variable de cache qui contient la liste). C’est une approche valable si on compte que la plupart des applications comptent bien plus de lectures que d’écritures.
Avantage : C’est simple à gérer dans le code, il n’y a pas de mauvaise surprise.
Inconvénient : Si on fait beaucoup de requêtes différentes les unes des autres, par exemple des recherches avec des critères très variés, on risque d’avoir beaucoup de variables en cache. Et donc la liste pourrait être très longue ; la parcourir pourrait devenir problématique. En plus de cela, il y a un risque d’incohérence des données au moment où la liste est mise à jour, entre sa lecture et son écriture, ou durant l’effacement de toutes le variables listées.
Versionner les variables
L’autre solution envisageable est de stocker, dans une variable de cache, un « numéro de version ». Memcache permet d’incrémenter la valeur stockée dans une variable en un seul appel, évitant ainsi les problèmes d’incohérence d’un cycle lecture-incrémentation-écriture.
Chaque donnée placée en cache voit alors son nom préfixé ou suffixé par ce numéro de version. À chaque fois qu’une requête est susceptible de modifier les données (et donc qu’il faudra vider les variables en cache), il suffit d’incrémenter le numéro de version. À la lecture suivante, les anciennes variables ne seront plus accédées.
Avantage : Pas de ralentissement lors de l’effacement des variables obsolètes.
Inconvénient : Chaque lecture de donnée impose deux accès au cache, l’un pour récupérer le numéro de version, l’autre pour lire la variable de données, ce qui réduit d’autant le gain en performance. Suivant le nombre d’écritures, le cache peut vite se remplir de variables inutiles.
Au final
La dernière solution reste celle qui paraît la plus intéressante. Dans le cadre d’une couche d’abstraction de données qui est destinée à être étendue intelligemment (comprendre par un développeur) dès qu’on en fait une utilisation qui ne soit pas minimale, c’est l’idée qui m’a semblé la meilleure, et que j’ai implémenté.
Est-ce que vous avez déjà été confronté à ce genre de réflexion ? Avez-vous d’autres idées ?
Quelques lectures intéressantes
Voici, en vrac, quelques liens qui ont alimenté ma réflexion :
http://stackoverflow.com/questions/2916645/implementing-model-level-caching
http://stackoverflow.com/questions/2915395/cache-layer-for-mvc-model-or-controller
http://www.aminus.org/blogs/index.php/2007/12/30/memcached_set_invalidation
http://drupal.org/node/493448
http://stackoverflow.com/questions/4475696/storing-online-users-php-memcache
http://stackoverflow.com/questions/276709/design-pattern-for-memcached-data-caching