Publié jeudi 27 mars 2008 12:17 par Luke77

Une partie de Cache - Cache / Partie 1

Dans tout bon site web, il est je pense très important de monter des informations en cache afin de limiter les aller-retour entre les serveurs web et la base de données. Je dirai même qu'il s'agit de la pierre angulaire de tout site internet qui est supposé tenir la charge. Je me propose donc de réaliser un petit cache au travers de 3 posts dont voici le premier. Le cache qui sera présenté ici devra donc pouvoir contenir des éléments (c'est un peu la base), faire expirer les éléments qui sont à l'intérieur et surtout être Thread-Safe !!!

Comme je l'indiquai dans le premier post, je compte "partager" mes développements avec ceux qui me liront, ceux qui me diront que c'est n'importe quoi ce que je raconte, mais aussi ceux, dont le nombre tends vers 0, qui me considèreront comme un dieu (l'espoir fait vivre n'est-ce pas ?) . C'est pourquoi il s'agira de notre code, notre Cache.

Comme dans beaucoup d'autres classe que nous écrirons, nous allons utiliser les Generics afin de typer notre cache. Cela nous permettra d'éviter de devoir caster nos object, mais aussi nous évitera le boxing / unboxing, ce qui devrait théoriquement améliorer aussi les performances.

Voici la première description de notre classe :

   1: namespace JALUM.Utility.Cache
   2: {
   3:     public class Cache<K, V> : IDictionary<K, V>, IDisposable
   4:     {
   5:     }
   6: }

Comme tu peux le voir, cette dernière agira exactement comme un dictionnaire Key / Value.

Afin de vérifier quels éléments doivent être conservés dans le cache, et lesquels doivent être supprimer, nous allons utiliser un Timer, par conséquent nous implémenterons aussi l'interface IDisposable afin de libérer correctement toutes les ressources.

Nous allons nous contenter, à peu de choses près, d'encapsuler un Dictionnary<K, V> en le rendant Thread-Safe. La méthode la plus simple serait d'utiliser le mot clé lock fourni par le framework .net mais cette utilisation a deux principaux inconvénients :

  • Elle peut lorsqu'elle est mal utilisée conduire à des Dead-Locks. Si je lock A pour traiter B qui elle même tente de locker A -> l'appli freeze...
  • Elle induit des locks lorsque cela n'est pas nécessaire. Si deux threads tentent d'accéder en lecture à la même ressource, le mot clé lock bloque le second thread jusqu'à ce que le premier ait terminé. Or les deux threads auraient pu y accéder en même temps sans problèmes.

La seconde méthode, celle que nous utiliserons, est d'utiliser un ReaderWriterLock, ou sa nouvelle version dans le Framework 3.5 : ReaderWriterLockSlim, qui permet de différencier les locks dédiés à la lecture des locks dédiés à l'écriture. Grosso modo, on signale que l'on souhaite faire une lecture. Si aucun lock en écriture n'est posé, on accède directement à la ressource, peu importe si d'autres sont aussi en train d'y accéder en lecture ; si un lock en écriture est déjà posé, le thread attends que le thread en écriture ait terminé son boulot. Lorsque l'on signale vouloir acquérir un lock en écriture, le thread attends que les thread en lecture déjà en cours se terminent. Si de nouveau locks en lecture sont demandés, ils attendront que le thread ait obtenu son lock et ai pu faire son boulot avant d'avoir à leur tour le droit d'accéder à la ressource.

Son utilisation est relativement simple mais comme les geeks que nous sommes, mieux vaut pour nous 20 lignes de code que 5 lignes d'explications ...

La lecture :

   1: _lock.EnterReadLock();
   2: try
   3: {
   4:  
   5: }
   6: finally
   7: {
   8:     _lock.ExitReadLock();
   9: }

L'écriture :

   1: _lock.EnterWriteLock(); 
   2: try 
   3: { 
   4:  
   5: } 
   6: finally 
   7: { 
   8:     _lock.ExitWriteLock(); 
   9: }

L'upgrade de la lecture en écriture (cela consomme moins que relâcher le lock en lecture et en acquérir un nouveau en écriture) :

   1: _lock.EnterUpgradeableReadLock(); 
   2: try 
   3: {  
   4:     _lock.EnterWriteLock(); 
   5:     try 
   6:     { 
   7:     } 
   8:     finally 
   9:     { 
  10:         _lock.ExitWriteLock(); 
  11:     } 
  12: } 
  13: finally 
  14: { 
  15:     _lock.ExitUpgradeableReadLock(); 
  16: }

Voila pour la première partie de cette magnifique classe. A bientôt pour la suite.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

# re: Une partie de Cache - Cache / Partie 1 @ jeudi 27 mars 2008 15:17

Ca m'a l'air interessant mais pourquoi ne pas utiliser System.Web.Caching ? Certes cette classe n'est pas générique mais elle est TRES puissante :

- absoluteExpirationDate

- slidingExpirationDate

- priorité : si la mémoire est trop faible, les items peu prioritaires sont dégagés ...

Pour l'utiliser en win, il faut juste instancier un HttpRuntime et elle fonctionne aussi en win.

Mis à part la généricité et l'envie de réinventer la roue, je n'arrive pas à voir l'avantage de créer son propre système de cache.

Mais l'article reste interessant ;-)

cyril

# re: Une partie de Cache - Cache / Partie 1 @ jeudi 27 mars 2008 17:03

Il est vrai que j'aime bien réinventer la roue :-p, mais ici il ne s'agit de cela.

Effectivement System.Web.Caching.Cache est très puissante et sert à beaucoup de chose pour nous et pour le Framework (je pense notamment au PartialCaching).

Cependant il y a plusieurs choses qui me gènent un peu dans cette classe.

Premièrement, comme tu l'as indiqué, il est nécessaire de référencer System.Web dans un projet qui n'est pas forcement du web (bon ok je pinaille).

Deuxième point, la généricité garantie un typage que le Cache du Framework ne fait pas (là aussi je pinaille parce que c'est vraiment pas la mort).

Par contre, et là je considère ne pas pinailler, il n'est pas possible, à ma connaissance en tout cas, de savoir si un élément est présent dans le Cache du Framework (méthode du style ContainsKey()), or c'est une fonctionnalité dans un cache dont j'ai besoin.


Je prends un cas concret pour lequel j'ai eu un problème. Dans un site web disponible dans plusieurs langues, on a créé un nouveau ResourceProvider qui va lire les traductions en base de données.

Ensuite tu définies en ressource locale à une page le titre que celle-ci doit avoir.

Et ensuite tu as une ressource globale contenant le titre que toutes les pages doivent avoir si jamais la ressource locale n'existe pas.

Avec le Cache du Framework, comment savoir si la ressource n'existe pas ou bien si cette dernière a expirée. Tu seras obligé de requêter à chaque fois ta base de données.

Le cache que je suis en train d'exposer ici permettra de faire cette distinction de manière simple.

Luke77

# re: Une partie de Cache - Cache / Partie 1 @ jeudi 27 mars 2008 18:32

Je reformule : ce que tu n'aimes pas dans le cache .net c'est que tu ne peux pas savoir si un item n'existe pas ou n'existe plus.

Tu peux créer ta classe Cache qui te sert de proxy pour accéder à Caching.Cache (qui est malheureusement sealed). Dans la méthode Insert tu peux ajouter un Delegate qui sera lancé lorsque l'item sera supprimé à partir de là, tu peux facilement avoir une méthode HasExpired, ContainsKey ou je ne sais quoi :)

cyril

# re: Une partie de Cache - Cache / Partie 1 @ jeudi 27 mars 2008 19:08

Effectivement, mais c'est quand moins marrant que d'en écrire un à soi.

Bon en dernier argument et désespoir de cause pour trouver un intérêt à cette classe je dirai : Y a pas de Flush non plus sur Cache... (autrement que par des Remove successifs ...)

Luke77

# re: Une partie de Cache - Cache / Partie 1 @ jeudi 27 mars 2008 21:40

Je me suis quand même posé des questions, et j'ai donc effectué quelques tests de performances entre mon Cache, dont je décris la première étape ici, et le Cache du Framework.

Avant tout, je le concède, celui que j'ai écrit n'a pas autant de fonctionnalités que celui du Framework mais j'ai envie de dire qu'elles me suffisent... :-)

Et donc de ce test est ressorti que le Cache du Framwork est environ 20% plus lent que le mien (et je n'ai même pas la couche en plus qui s'abonne aux events du Cache...).

Pour les curieux, le protocole de tests est 500 threads concurrents qui effectuent chacun environ 12500 lectures et 12500 écritures réparties aléatoirement tout au long du test.

Luke77


Les 10 derniers blogs postés

- L’application des MiniDrones Parrot est aussi disponible pour Windows 8.1 par Blog de Jérémy Jeanson le 10-28-2014, 15:01

- L’application des MiniDrones Parrot est enfin disponible pour Windows Phone par Blog de Jérémy Jeanson le 10-27-2014, 09:49

- Mise à jour Samsung 840 EVO sur core server par Blog de Jérémy Jeanson le 10-27-2014, 05:59

- MVP Award 2014 ;) par Blog de Jérémy Jeanson le 10-27-2014, 05:42

- « Naviguer vers le haut » dans une librairie SharePoint par Blog de Jérémy Jeanson le 10-07-2014, 13:21

- PowerShell: Comment mixer NAGIOS et PowerShell pour le monitoring applicatif par Blog Technique de Romelard Fabrice le 10-07-2014, 11:43

- ReBUILD 2014 : les présentations par Le blog de Patrick [MVP Office 365] le 10-06-2014, 09:15

- II6 Management Compatibility présente dans Windows Server Technical Preview avec IIS8 par Blog de Jérémy Jeanson le 10-05-2014, 17:37

- Soft Restart sur Windows Server Technical Preview par Blog de Jérémy Jeanson le 10-03-2014, 19:43

- Non, le certificat public du CA n’est pas un certificat client !!! par Blog de Jérémy Jeanson le 10-03-2014, 00:08