Persistance objet avec Redis et Jackson

Redis est la base de données NoSql que je préfère. Le concept est ultra-simple et j’aime la simplicité. Il faut voir Redis comme une grosse Map<K,V>. Avec la possibilité de faire des requêtes sur les clés.

Du coup la documentation de Redis est simple. Un autre truc que j’aime c’est qu’elle spécifie la complexité de chaque opération, ce qui permet au développeur de vérifier à chaque fois que la commande qu’il s’apprête à utiliser n’est pas trop gourmande. De plus, il n’a pas le risque d’oublier de mettre un index sur un champ (combien de fois cela arrive en SQL ou avec MongoDb ! ) car toutes les clés sont par définition « indexées » dans une (Hash) Map.

Simple et (donc ?) performant. Redis est sans doute ce qu’il y a de plus performant en terme de base de données. Nous avons donc intérêt à nous y intéresser avant de chercher des solutions plus complètes et donc plus complexes et moins performantes.

Sauf que, Redis ne stocke que des chaînes de caractères. Comment faire pour stocker nos objets métiers complexes ?


On pourrait simplement utiliser une base de données SQL pour stocker notre modèle et utiliser un ORM pour que ce soit « simple ». Ensuite dans Redis on ne stockerai que la dé-normalisation de certaines requêtes : 3 meilleurs clients => { 1 => id:130 ; 2 => id:345 ; 3 => id:456 } Ainsi la requête pour récupérer les identifiants des trois meilleurs clients se fera en temps constant O(1) puis la requête pour récupérer les données des trois clients dans la base SQL se fera également en temps constant,  car les clés primaires sont indexées dans les bases SQL.

Mais il faut avouer que c’est embêtant de devoir gérer deux bases de données, surtout quand on pourrait simplement sérialiser les objets directement dans Redis.

Stocker les objets Java dans Redis grâce à Jackson

Dans notre exemple, le besoin est de pouvoir insérer des liens dans des listes et de pouvoir récupérer en temps constant tous les liens d’une liste donnée.
Jackson est un sérialiseur Objet -> JSON. Ce qui est parfait pour pouvoir lire facilement les objets sérialisé où pour pouvoir les utiliser directement en JavaScript, sans passer par une dé-sérialisation.

On va utiliser JAX-B pour annoter nos objets Java, par exemple :

@XmlRootElement(name = "links")
@XmlAccessorType(FIELD)
public class BannerLink {
   private String label;
   private String url;

   public BannerLink(String label, String url) {
      this.label = label;
      this.url = url;
   }

   public String getLabel() {
      return label;
   }

   public String getUrl() {
      return url;
   }

   protected BannerLink(){}
}

Notez que Jackson sait aussi sérialiser des POJOs (sans JAX-B mais avec Setter)

Ensuite il suffit de sérialiser l’objet avec Jackson pour l’insérer dans une liste Redis.

      AnnotationIntrospector introspector = new JaxbAnnotationIntrospector();
      mapper.getDeserializationConfig().setAnnotationIntrospector(introspector);
      mapper.getSerializationConfig().setAnnotationIntrospector(introspector);
      jedis.rpush("listKey", mapper.writeValueAsString(link));

Pour la lecture, il suffit de dé-sérialiser les objets de la liste.

   mapStringsToLinks(jedis.lrange(key, 0, jedis.llen(key)));

   private List mapStringsToLinks(List jedisResult) {
      return Lists.transform(jedisResult, toBannerLink());
   }

   private Function<String, BannerLink> toBannerLink() {
      return new Function<String, BannerLink>() {

         @Override
         public BannerLink apply(@Nullable String link) {
            return mapper.readValue(link, BannerLink.class);
         }
      };
   }

Je ne sais pas vous, mais moi je trouve ça vraiment plus simple de persister directement et simplement les instances d’objets, telle quelle, sans se prendre la tête avec du mapping, jointure ou autre joyeuseté.

Performances

Je n’ai pas fait le test avec une base SQL et un ORM de type Hibernate mais n’hésitez pas à forker le code et à le faire, ça m’intéresse.

Aucun tuning n’a été fait sur les bases de données. Mongo est bien meilleur en écriture et peut être encore meilleur, car il ne garantie pas que les données sont effectivement écrites sur le disque, j’avoue ne pas avoir cherché à optimiser Redis pour l’écriture, j’ai gardé la configuration par défaut.

En revanche, Redis est bien meilleur en lecture, malgré le surplus de traitements dû à la désérialisation Jackson. Et c’est ce qu’on cherche dans notre cas d’usage, nos listes de liens vont être lus beaucoup plus souvent que modifiées. Et pour ceux qui se poseraient la question, oui j’ai bien créé les indexes dans Mongo.

Ce qu’il faut retenir, c’est que dans bien des cas, une base de données document ou SQL n’est pas forcément obligatoire et qu’il vaut mieux démarrer simple et (très) efficace, quitte à changer par la suite…

Un peu de lecture si vous souhaitez en savoir plus sur Redis : Redis: the Definitive Guide: Data Modeling, Caching, and Messaging

9 Responses to “Persistance objet avec Redis et Jackson”

  1. Pitton Olivier dit :

    Bonsoir,

    J’utilise MongoDB et je n’ai jamais testé Redis, je ne connais que de nom. Je me demandais si pour le benchmark présenté, des index avaient été mis dans MongoDB ? La différence me paraît énorme.

    Cordialement,

  2. Jean-Baptiste dit :

    Oui, sans index on a le temps de s’endormir avant que le test se termine :)

    Attention tout de même l’objet de l’article n’est pas de tester Redis vs Mongo. Le (mauvais) score de Mongo peut venir de son driver ou de ma façon de l’utiliser. N’hésite pas à checker le code, je ne suis pas expert :)

    btw, lui aussi arrive à de grosses différences entre les deux : http://stackoverflow.com/questions/5252577/how-much-faster-is-redis-than-mongodb

  3. Pitton Olivier dit :

    Bonsoir,

    Dans le cas du benchmark sur stackoverflow, la version de MongoDB est « ancienne ». Les performances ont été améliorées, notamment en supprimant le global lock du serveur (chose qui m’a fait peur quand je l’ai appris). En soit je ne suis pas un « MongoDB fanboy », il faudra que j’aille regarder Redis.

    Sinon, je n’aime pas du utiliser le driver MongoDB (que j’ai vu dans ton code), j’utilise généralement un wrapper qui allège pas mal le code -> http://jongo.org/

    En tout cas, merci pour ce retour sur Redis.

    Cordialement,

  4. Pitton Olivier dit :

    Un post sur l’amélioration des locks dans MongoDB, que j’ai décrit précédemment : http://blog.serverdensity.com/2012/05/23/goodbye-global-lock-mongodb-2-0-vs-2-2/

  5. Thomas Queste dit :

    Côté Mongo, tu dois t’assurer que tu utilises le mode WriteConcern.SAFE afin de garantir que l’écriture a bien été envoyé au serveur. Par défaut, c’est le mode « Normal » qui n’attends pas de réponse serveur.
    Entre les lignes, ça veut dire que Mongo n’est pas sûr « par défaut », mais qu’il est bien plus rapide.

    Détail des modes Normal et Safe :
    – Normal : Exceptions are raised for network issues, but not server errors
    – Safe : Exceptions are raised for network issues, and server errors; waits on a server for the write operation

    Pas mal de gens se font avoir dans leur bench, dont moi :-)
    Le mode Safe étant bien plus lent que Normal (2 voir 3 fois plus lent).

    Pour info, Mongo gère la durabilité en mode cluster, pas mono-machine (d’où le mode « normal » par défaut).
    Voir: http://www.thebuzzmedia.com/mongodb-single-server-data-durability-guide/

    Tom

  6. Jean-Baptiste dit :

    Salut et merci, tu as dû regarder le code car effectivement le WriteConcern n’est pas en SAFE. Cependant, Pour le benchmark, j’étais passé en SAFE car en mode normal, l’écriture est quasiment instantanée :) le bench était à 13ms en NORMAL alors qu’il est à 200+ms en mode SAFE.
    N’empêche que même en SAFE, Mongo est meilleur que Redis en écriture, je pense qu’il doit y avoir une optimisation à faire coté Redis pour écrire plus vite, je chercherai quand j’en aurai besoin ^^

  7. Salut,

    Je me plante peut-être mais il me semble que les temps de réponse peuvent aussi être liés à la volumétrie et la concurrence.
    Je ne vois pas du tout de concurrence dans ton benchmark donc a mon avis on ne peut pas en tirer beaucoup de conclusions.

    D’après ce que je vois, MongoDB viennent de virer le « global lock » lors des écritures, ce qui veut je crois dire que le write concurrent était avant un problème qui est maintenant amélioré.
    http://blog.serverdensity.com/2012/05/23/goodbye-global-lock-mongodb-2-0-vs-2-2/

    Donc pourquoi pas faire un test en 2.0 puis 2.2, avec des accès concurrents, pour voir la différence? :)

    Redis c’est pas une DB in memory? Du coup comment ça se fait que ça soit plus lent en écriture que MongoDB?

  8. hugo dit :

    C’est amusant j’ai testé moi aussi et j’obtiens des résultats assez différents.
    Par contre pour le coup j’ai sans doute des conditions différentes :

    – j’utilise jongo pour dialoguer avec mongo
    – j’utilise le build redis pour windows qui n’est officiellement pas supporté
    – pour jongo comme pour morphia, j’utilise un WriteConcern FSYNC_SAFE donc c’est plus lent (mais bon, je préfère garder mes données)

    Sur mon test j’ai inséré 900 000 objets et fait le test avec morphia, jongo, redis et une base mysql.

    Pour la base il faut que je recommence, j’ai pas eu la patience d’attendre l’insertion des 900 000 lignes après plus de 40minutes… (pour 300 000 lignes)

    avec morphia :
    Count in 1ms
    Find by login in 482ms
    Save in 28ms
    Find by address 435ms

    avec jongo :
    Count in 0ms
    Find by login in 13ms
    Save in 26ms
    Find by address 497ms (non indexé)
    Find by location 8ms (index géospatial)

    avec redis :
    Count in 0ms
    Find by login in 263ms
    Save in 21ms

    avec redis il faut bien voir que l’opération de save correspond à plusieurs sets pour dénormaliser les différentes infos dont j’ai besoin ensuite pour faire des finds. Donc les 21ms sont très bien en considérant que je fais plusieurs set et un lpush

    avec la base de données (mais avec 300 000 lignes) :
    Count in 369ms
    Find in 686ms
    Save in 6ms

    Bref, dans mon cas redis est meilleur en écriture mais moins bon en lecture finalement.

  9. Mathieu Leblanc dit :

    Bonjour,

    Merci pour cet article très interessant. Par contre je n’arrive à lancer le code fourni, j’ai de erreurs dans le POM.xml j’utilise Eclipse et Tomcat. Je vous remercie

Leave a Reply