Redis : autre approche NoSQL

Redis : REmote DIctionary Server / Redistribute

Redis is an in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries.

À retenir

In-memory $\Rightarrow$ très rapide

Redis = data-structure store

Aperçu des structures de données Redis – source

Application 1 : cache web

Observation : the bigger the storage is, the slower it will be.

A cache is a component that stores recently accessed data in a faster storage system. Each time a request for that data is made, it can (with some probability) be pulled from the faster memory.

"Fast library" application

Application permettant de publier et lire des livres en ligne. MongoDB ou SQLite (ou autre...) peut être utilisé pour la base principale, l'idée étant d'accéder plus rapidement aux livres consultés souvent : on utilise Redis pour ça.

Ajout d'un livre
  1. Requête HTTP "POST /book"
    avec titre + contenu
  2. Ajout à la base principale
  3. Ajout à l'ensemble Redis des
    livres en cache
Lecture d'un livre
  1. Requête HTTP "GET /book"
    avec titre
  2. Interrogation du cache,
    puis si échec
  3. Interrogation de la base principale

Taille du cache ?

If you plan to use Redis just as a cache where every key will have an expire set, you may consider using the following configuration

maxmemory 2mb
maxmemory-policy allkeys-lru

In this configuration all the keys will be evicted using an approximated LRU algorithm as long as we hit the 2 megabyte memory limit.

Voir par ici pour quelques algorithmes de remplacement de cache.

Application 2 : serveur de chat

Idée
  • Envoi d'un message chat = publish to some channel
  • Réception d'un message = subscribe to some channel

$\Rightarrow$ Redis PUB/SUB

Avantage : les messages peuvent être lus par n'importe quelle application dialoguant avec le server Redis

Exemple minimal en local

Fichier "settings.py"

# pip install redis ...

import redis
config = {
  'host': 'localhost',
  'port': 6379,
  'db': 0,
}
r = redis.StrictRedis(**config)

Fichier "pub.py"

from settings import r
import sys

if __name__ == '__main__':
  name = sys.argv[1]
  channel = sys.argv[2]
  print('Welcome to ' + channel)
    while True:
      print('Enter a message:')
      message = sys.stdin.readline()
      if message.lower() == 'exit':
        break
      message = name + ' says: ' + message;
      r.publish(channel, message)

Fichier "sub.py"

from settings import r
import sys

if __name__ == '__main__':
  channel = sys.argv[1]
  pubsub = r.pubsub()
  pubsub.subscribe(channel)
  print('Listening to ' + channel)
  while True:
    for item in pubsub.listen():
      print(item['data'])

Ne reste plus qu'à taper (démo ?)
python sub.py PYTHON dans un terminal (serveur), et
python pub.py Ben PYTHON dans un autre (client).

Dans une application réelle...

Scaling by adding servers?

Redis to the rescue!

Q: Can't I Just Use MongoDB for Everything?

"We are already using MongoDB to store our records, can't we just put everything in one place?"

MongoDB is a good tool. We like to think of it as the Hammer of Databases: a general purpose Datastore that works well for most NoSQL situations. But if you need to save/access Millions of Records Per Second then there's only one place to store your data: Redis.

Autres exemples

Base dans le navigateur

  • LocalStorage : pour données simples ; synchrone
  • IndexedDB : permet de stocker + ; asynchrone

Différences entre les deux.

Exemple basique

var indexedDB = window.indexedDB
// Open (or create) the database
var open = indexedDB.open("MyDatabase", 1);

// Create the schema
open.onupgradeneeded = function() {
  var db = open.result;
  var store = db.createObjectStore("MyObjectStore",
    {keyPath: "id"});
  var index = store.createIndex("NameIndex",
    ["name.last", "name.first"]);
};
open.onsuccess = function() {
  // Start a new transaction
  var db = open.result;
  var tx = db.transaction("MyObjectStore", "readwrite");
  var store = tx.objectStore("MyObjectStore");
  var index = store.index("NameIndex");

  // Add some data
  store.put({id: 12345,
    name: {first: "John", last: "Doe"}, age: 42});
  store.put({id: 67890,
    name: {first: "Bob", last: "Smith"}, age: 35});
	
  // Query the data
  var getJohn = store.get(12345);
  var getBob = index.get(["Smith", "Bob"]);
  getJohn.onsuccess = function() { // => "John"
    console.log(getJohn.result.name.first); }
  getBob.onsuccess = function() { // => "Bob"
    console.log(getBob.result.name.first); }

  // Close the db when the transaction is done
  tx.oncomplete = function() { db.close(); }; }

Observations

IndexedDB API : assez complexe à utiliser directement,
les requêtes ne se font pas très naturellement.

$\Rightarrow$ Alternatives de "plus haut niveau" :

Choix pour ce cours : Dexie.js

TODO offline app

var db = new Dexie("todos-dexie");
db.version(1).stores({ todo: '_id' })
db.open()
  .then(refreshView);

function onClick(e) { //clic sur une tâche
  db.todo
    .where('_id').equals(e.target.getAttribute('id'))
    .delete()
    .then(refreshView); }

function onSubmit(e) { //nouvelle tâche
  db.todo.put({ text: input.value, _id: String(Date.now()) })
    .then(function() { input.value = ''; })
    .then(refreshView); }

function refreshView() {
  return db.todo.toArray()
    .then(renderAllTodos); }

function renderAllTodos(todos) { /* ... */ } //...

Application "tournois"

Pas encore écrite... mais nécessite au moins les tables :

  • "joueurs" (nom, numéro de licence, club)
  • "matchs" (phase, résultat, joueurs)

Puis éventuellement des tables "équipes", "arbitres" ...

"En Dexie" :

db.version(1).stores({
  players: 'name',
	matchs: '++id' });

db.players.put({name: "Nicolas", license: "1234ABCD"})
  .then (function() { ... }) //...

Imaginons...

Lister les clubs par taille décroissante

db.players.get('project on club only?').distinct()
  .then( function(allClubs) { //...
    allClubs.forEach( c => {
      db.players.where(club).equals(c).count() //...

Obtenir les nombres de points par joueur

// Par exemple pour name = "Alice"
db.matchs.where("joueurs").includes("Alice")
  .where('Elle a gagné...').count()

$\rightarrow$ Requêtes à adapter...

Et SQLite ?

Dans le navigateur aussi ! (compilé en js via Emscripten)

var sql = require('sql.js'); //or sql = window.SQL
var db = new sql.Database(); //or new sql.Database(data)

sqlstr = "CREATE TABLE hello (a int, b char);";
sqlstr += "INSERT INTO hello VALUES (0, 'hello');"
sqlstr += "INSERT INTO hello VALUES (1, 'world');"
db.run(sqlstr); // Run the query without returning anything

var res = db.exec("SELECT * FROM hello");
// [ {columns:['a','b'], values:[[0,'hello'],[1,'world']]} ]

// Prepare an sql statement
var stmt = db.prepare(
  "SELECT * FROM hello WHERE a=:aval AND b=:bval");
// Bind values to the parameters and fetch the results
var result = stmt.getAsObject(
  {':aval': 1, ':bval': 'world'});
console.log(result); // Will print {a:1, b:'world'}

Bilan

Bases dans le navigateur utiles

  • pour des applications hors ligne, mais aussi
  • pour des applications riches ("one-page") en ligne, pouvant éventuellement être utilisées hors ligne.

Exemple : un jeu vidéo, dont les données sont sauvegardées localement en mode offline puis synchronisées avec le serveur au retour de la connection (on peut imaginer un jeu d'aventure type Zelda 2D où une partie de la carte est chargée localement).

Bilan du cours

Sujets abordés

  • Concevoir une base convenablement normalisée
  • Interroger cette base via des requêtes SQL (complexes)
  • Optimiser certaines requêtes lentes (réécriture, index)
  • Envisager l'utilisation d'alternatives au SQL, pouvant être mieux adaptées (Mongo pour une base orientée documents, Redis pour effectuer des statistiques en temps réel ...)

Sujets non abordés (cf. suite de cursus ?)