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.

Aucun commentaire:

Enregistrer un commentaire