Affichage des articles dont le libellé est redis. Afficher tous les articles
Affichage des articles dont le libellé est redis. Afficher tous les articles

vendredi 17 septembre 2010

Stockez vos sessions PHP dans Redis !

Par défaut PHP stocke les sessions sur le système de fichiers. C'est bien, mais dans un environnement "Load-Balancé", ça ne marche en pratique pas très bien, à moins d'écrire les sessions sur un système de fichiers distants (NFS par exemple), et encore.

Pour palier à ce problème PHP permet de stocker les sessions PHP dans memcache, ce qui est déjà nettement mieux.

Ok, memcache c'est bien, mais Redis, c'est mieux (oui, je suis totalement subjectif là).

J'ai donc développé une petite classe qui permet de stocker vos sessions dans Redis, et donc de bénéficier de ses multiples avantages : vitesse, persistance.

Le code est disponible sous LGPLv3 sur github.

A l'usage, c'est extrêmement simple et transparent :

require_once 'RedisSession.php';

RedisSession::init ( array ( 
        'session_name' => 'redis_session', 
        'cookie_path' => '/', 
        'cookie_domain' => '.acme.org', 
        'lifetime' => 3600, 
        'server' => array ( 
                'host' => 'redis.acme.org', 
                'port' => 6379 ) ) );

jeudi 5 août 2010

Redis 2.0 : Mémoire virtuelle, hash et bien plus encore

Les Release Candidate de Redis 2.0 s'enchainent depuis quelques temps, et je ne resiste pas à l'envie de vous en faire partager les alléchantes nouveautés.

Le support de la mémoire virtuelle

La version 1 de Redis nécessitait que toutes les données soient stockées en mémoire, ce qui limitait fortement la taille des datasets. La version 2.0 apporte donc le support de la "Virtual memory".
Pourquoi avoir attendu tout ce temps alors que les OS permettent déjà ce mécanisme :

  • L'OS n'a aucune connaissance des structures utilisées par redis
  • La taille d'un bloc mémoire alloué par l'OS peut être insuffisant pour stocker une valeur, qui peut donc être répartie sur plusieurs blocs mémoires non adjacents
  • La structure des données n'est pas optimisée pour un recherche en RAM
On peut donc optimiser grandement la structure des données sur le disque en connaissant la structure, redis 2 implémente donc sa propre gestion de la mémoire virtuelle :
  • L'espace de stockage est toujours divisé en pages, mais leur taille est libre
  • Toutes les clefs restent en RAM
  • Les valeurs peuvent être soient en RAM, soient sur le disque, et redis connait leur position exacte sur le disque, ce qui limite le IO
  • Redis choisit quelles valeurs sont sur le disque en fonction d'un indice = taille * derniere utilisation. Les valeurs les plus souvent utilisées, dans la mesure où elles ne sont pas trop grosses, restent en RAM.
Allez faire un tour sur le blog de Salvatore Sanfilippo pour des explications complètes.


Transactions
Redis 2.0 amène avec lui les transactions : les commandes MULTI, EXEC et DISCARD permet d'assurer l'atomicité d'une liste de commandes.

Le type hash
Redis 2.0 apporte le support du type hash qui facilite grandement le stockage d'objets. La gestion mémoire du stockage des hash est très bien optimisé, et permet donc de stocker efficacement les objets. Pour plus d'informations, consulter l'article sur le wiki de redis.

Notification
Redis 2.0 possède désormais un système de notifications interne qui permet, par exemple de supprimer automatiquement les clefs dont les valeurs deviennent "vides", cf encore une fois le blog de Salvatore Sanfilippo.

Ca promet donc d'envoyer du lourd comme on dit, je ne peut que vous inviter à tester par vous même, et, pourquoi pas, à remplacer vos serveurs memcache ;).

mercredi 4 août 2010

Moteur de Blog NoSQL - Parte 2 : Redis

Idée

On va reprendre le moteur de blog NoSQL écrit ici, en remplaçant Cassandra par Redis

Présentation du moteur

Comme un petit rappel ne fait jamais de mal, re-voici les fonctionnalités implémentées par notre moteur :
  • Écriture d'un post
  • Ajout de tag aux posts
  • Affichage des derniers posts
  • Affichage des posts liés à un tag

On ajoute donc la gestion de redis à notre architecture :



Redis


Présentation

Redis se situe dans la lignée des bases de données clefs-valeurs, et on peut donc le situer entre Memcached et Cassandra.

La grande force par rapport à Memcached est la persistance des données. En effet, bien que redis travaille sur une hashtable en mémoire, les données sont écrites sur le disque. Il supporte aussi différentes types de valeurs (là où memcached ne stocke que des chaînes de caractères) :
  • Les chaînes de caractères : les opérations disponibles sont SET, GET, INCR, DECR
  • Les listes : ce sont des listes de chaînes triées par ordre d'insertion. Les principales opérations disponibles sont : LPUSH/RPUSH, LPOP/RPOP et LRANGE.
  • Les "sets" : ce sont des ensembles (au sens mathématique) d'objets sur lesquels ont peut effectuer les opérations ensemblistes classiques : UNION, INTERSECTION, DIFFERENCE
  • Les hashes : la valeur stockée est une hashmap, ce qui permet donc de structurer la donnée (JSON like).

Redis supporte aussi nativement la réplication master/slave, ce qui le rend scalable (par rapport à memcached).

Utilisation en php

De nombreux bindings existent pour php : Predis, Rediska en php pur, ou PHPRedis en module.
C'est PHPRedis que j'ai choisi d'utiliser içi, pour des raisons de performances.

Implémentation

Pour le stockage des posts, c'est très simple, on va se servir du type hash : chaque entrée aura pour clef le slug du post, et la valeur sera un mapping de l'objet post :
post:my-first-post : {
  slug => "my-first-post",
  title => "Yeah, my first blog post",
  text => "a little NoSQL stuff"
}

La commande redis pour stocker un post serait donc :
HMSET post:my-first-post slug "my-first-post" title "Yeah, my first blog post" text "a little NoSQL stuff"

Pour le stockage des tags d'un post, on va se servir du type liste :
post:my-first-post:tags : {"nosql", "tech"}

RPUSH post:my-first-post:tags "nosql"
RPUSH post:my-first-post:tags "tech"

Pour pouvoir récupérer la liste des derniers posts publiés, on va se servir de la même astuce que pour Cassandra, et créer un tag fictif qui sera associé à tous les posts.
L'idée est donc de créer, pour chaque tag, une liste, dont la clef sera le tag, est la valeur la liste des clefs des posts associés. En faisant une insertion à gauche des posts à chaque fois, l'ordre chronologique inversé sera automatique :

tagpost:nosql : {"post:my-first-post"}
tagpost:tech : {"an-another-post, "post:my-first-post"}

LPUSH tagpost:nosql post:my-first-post
LPUSH tagpost:tech post:my-first-post
LPUSH tagpost:tech post:an-another-post

LPUSH tagpost:__allposts__ post:my-first-post
LPUSH tagpost:__allposts__ post:an-another-post

Pour récupérer les 10 derniers posts d'un tag, du plus récent au plus ancien, il suffira donc de faire :

LRANGE nosql 0 9

Qui nous renverra les clefs des posts concernés, que l'on devra alors charger :

GET post:my-first-post
LRANGE post:my-first-post:tags 0 9

L'implémentation php de tout ça est très simpl, et tient en moins de 100 lignes de code :
class RedisPostRepository extends RedisRepository implements IPostRepository {

 const KEY_SEP = ':';
 const KEYPREFIX_POST = 'post';
 const KEYPREFIX_TAGPOST = 'tagpost';
 const KEY_ALLPOSTS = 'allposts';

 public function __construct() {
  parent::__construct();
 }

 /**
  * @param string $_sSlug
  * @return string
  */
 private function generatePostKey($_sSlug){
  return self::KEYPREFIX_POST . self::KEY_SEP .$_sSlug;
 }

 /**
  *
  * @param string $_sSlug
  * @return Post
  */
 public function getPost($_sSlug) {
  $aPost = $this->moClient->hGetAll($this->generatePostKey($_sSlug));
  $oPost = new Post();
  $oPost->slug = $aPost['slug'];
  $oPost->title = $aPost['title'];
  $oPost->text = $aPost['text'];
  $oPost->tags = $this->moClient->lGetRange($this->generatePostKey($_sSlug) . self::KEY_SEP . 'tags', 0, 10);
  return $oPost;
 }


 /**
  * @param int $_iCount
  * @param int $_iPage
  * @return array
  */
 public function getLastPosts($_iCount = 5, $_iPage = 1) {
  return $this->getLastPostsByTag(self::KEY_ALLPOSTS, $_iCount, $_iPage);
 }

 /**
  * @param string $_sTag
  * @param int $_iCount
  * @param int $_iPage
  * @return array
  */
 public function getLastPostsByTag($_sTag, $_iCount = 5, $_iPage = 1) {
  $aPosts = array();
  $aSlugs = $this->moClient->lGetRange(self::KEYPREFIX_TAGPOST.  self::KEY_SEP . $_sTag, ($_iPage - 1) * $_iCount,$_iPage * $_iCount - 1);
  foreach($aSlugs as $sSlug) {
   $aPosts[] = $this->getPost($sSlug);
  }
  return $aPosts;
 }

 /**
  * @param Post $_oPost
  * @return boolean
  */
 public function insertPost($_oPost) {
  $sPostKey = $this->generatePostKey($_oPost->slug);
  if ($this->moClient->exists($sPostKey) === false) {
   $this->moClient->hMset($sPostKey, array('slug'=> $_oPost->slug, 'title'=> $_oPost->title, 'text'=> $_oPost->text));
   $this->moClient->delete($sPostKey .  self::KEY_SEP . 'tags');
   foreach ($_oPost->tags as $sTag) {
    $this->moClient->rPush($sPostKey .  self::KEY_SEP . 'tags', $sTag);
    $this->moClient->lPush(self::KEYPREFIX_TAGPOST.  self::KEY_SEP . $sTag, $_oPost->slug);
   }
   $this->moClient->lPush(self::KEYPREFIX_TAGPOST.  self::KEY_SEP . self::KEY_ALLPOSTS, $_oPost->slug);

   return true;
  } else {
   return false;
  }
 }

}

Voilà, notre blog peut maintenant tourner sur une base redis. La prochaine étape pourrait être d'enrichir ses fonctionnalités (commentaires, auteurs, ...), ou d'ajouter un autre moteur de stockage, il me reste encore quelques trucs que j'aimerais tester : MongoDB, CouchDB ou Neo4j par exemple.

Comme toujours, le code est disponible sur github.