Pull to refresh

Пишем модель для Redis на nodejs

Пишем модель для Redis.


В целях изучения Redis и nodejs, давайте напишем небольшую модель.

И так задача:

Для примера, пусть мы пишем бота, который посещает веб страницы. А сохранять мы будем адреса сайтов и время когда заходили на сайт.
И так, давайте приступим. Первое что нам нужно это скачать модуль для работы с Redis в nodejs, рекомендуемый к использованию модуль лежит по адресу: github.com/mranney/node_redis и его легко можно установить из менеджера пакетов nodejs выполнив команду:

npm install redis

Автор библиотеки рекомендует так же использовать hiredis, библиотека для разбора ответов Redis, что увеличивает производительность модуля. Устанавливать все так же просто из менеджера пакетов nodejs.

npm install redis hiredis

API библиотеки, практически полностью повторяет API Redis, поэтому можно сразу смотреть список команд Redis redis.io/commands и использовать их.

Hello Redis

Давайте напишем небольшой пример в стиле “hello world”.

// test.js
var    redis = require('redis')
     ,    client = redis.createClient()
     ;
// отлавливаем ошибки
client.on("error"function (err) {
  console.log("Error: " + err);
});

// Попробуем записать и прочитать
client.set('myKey''Hello Redis'function (err, repl) {
    if (err) {
           // Оо что то случилось при записи
           console.log('Что то случилось при записи: ' + err);
           client.quit();
    } else {
           // Прочтем записанное
           client.get('myKey'function (err, repl) {
                   //Закрываем соединение, так как нам оно больше не нужно
                   client.quit();
                   if (err) {
                           console.log('Что то случилось при чтении: ' + err);
                   } else if (repl) {
                   // Ключ найден
                           console.log('Ключ: ' + repl);
               } else {
                   // Ключ ненайден
                   console.log('Ключ ненайден.')

           };
           });
    };
});


Ну все, можно запускать

node test.js

Если все хорошо, то в ответ мы получим: Ключ: Hello Redis

Приступая к модели

Приступая к написанию модели, давайте узнаем какие типы ключей есть в Redis.
1) String — это самый простой тип ключей, представляет собой структуру Ключ -> Значение. Несмотря на то что он называется String, сюда можно записывать строковые, числовые и битовые значения.
2) List — этот тип данных представляет собой аналог массивов.
3) Hashes — это специальный тип данных, представляющий собой структуру Поле -> Значение. В качестве типов полей могут быть строки и числа.
4) Set / Sortedset — Последние два типа. представляют собой множества. Причем sortedset является отсортированным множеством. Значения сортируются по весу, вес нужно задавать самостоятельно.

Как мы видим, Redis это не просто Key Value хранилище. Для нашего тестового примера мы будем использовать два типа, Set и Hashes. В Set мы будем сохранять хешь от URL адреса на котором мы были, а в Hashes мы будем сохранять время захода на страницу.

Что ж, начнем


Первое что я делаю, это в начале файла модели описываю структуру которую она представляет, это очень помогает в будущем, когда уже забываешь что и как.

//siteModel.js
var        crypto = require('crypto');

/**
* Структура представлена в бд в виде:
* (hashes) sites:<hash>: {
*                            url: <url>
*                            lastTime: <time>    
*                         }
* (set)     sites:set: <hash>
*/


Модель представляет собой фабрику самой себя. Так же все методы модели (за исключением create()) передают в callback третьим аргументом указатель на this, это очень помогает в некоторых ситуациях
// Модель для Сайтов
var    SiteModel = module.exports =  function (client) {
   this.client = client;
   this.isCreate = false;
};

// Функция фабрика
SiteModel.prototype.create = function (url, time) {
   var site = new SiteModel(this.client);
   site.url = url;
   site.time = time || new Date().getTime();
   site.isCreate = true;
   return site;
};

SiteModel.prototype.__defineGetter__('hash'function () {
   return this.getHash(this.url);
});

// Функция возвращает md5 хеш
SiteModel.prototype.getHash = function (url) {
   return crypto.createHash('md5').update((url || this.url)).digest('hex');;
};

// Префиксы
SiteModel._prefix_ = 'sites:';

// Возвращает имя ключа для Hashes
SiteModel.prototype.pHashes = function (url) {
   return SiteModel._prefix_ + this.getHash(url) + ':';
};

// Возвращает имя поля для URL
SiteModel.prototype.kUrl = function () {
   return 'url:';
};

// Возвращает имя поля для времени последнего входа
SiteModel.prototype.kLastTime = function () {
   return 'lastTime:';
};

// Возвращает имя ключа для Set
SiteModel.prototype.pSet = function () {
   return SiteModel._prefix_ + 'set:';
};

//SiteModel.save();
SiteModel.prototype.save = function (callback) {
   // проверяем была ли создана модель через .create()
   if (this.isCreate) {
       this._save.call(this, callback);
   } else {
       if (callback) callback.call(thisnew Error('Модель должна быть создана перед сохранением'), nullthis);
   };
};


Метод multi() тут используется, для того что бы выполнить сохранение в 1 запрос, а в случае необходимости сохранения только уникальных сайтов, мы можем легко сделать это добавив перед client.multi() client.watch();

// Основная функция выполняющая сохранение
SiteModel.prototype._save = function (callback) {
   // Сохраняем все в один запрос
   this.client.multi([
       ['hmset'this.pHashes(), this.kUrl(), this.url, this.kLastTime(), this.time],
       ['sadd'this.pSet(), this.hash]
   ]).exec(function (err, repl) {
       if (err) {
           if (callback) callback.call(this, err, nullthis);
       } else {
           if (callback) callback.call(thisnull, repl, this);
       };
   }.bind(this));
};
////

// SiteModel.remove()
SiteModel.prototype.remove = function (url, callback) {
   //  Если аргумент 1, то это callback
   if (arguments.length == 1) {
       var callback = arguments[0];
       url = undefined;
   };
   
   this.client.multi([
       ['del'this.pHashes(url)],
       ['srem'this.pSet(), this.hash]
   ]).exec(function (err, repl) {
       if (err) {
           if (callback) callback.call(this, err, nullthis);
       } else {
           if (callback) callback.call(thisnull, repl, this);
       };
   }.bind(this));
};
////


Еще одно удобство библиотеки node-redis это возможность конструирования
запроса как массива.
Метод hmget() возвращает массив результатов, именно в такой последовательности в которой мы передавали имена полей, именно по этому мы можем спокойно делать так this.create(repl[0], repl[1]); (точно так же работает похожей метод hget() для стрингов)

// SiteModel.findByUrl
SiteModel.prototype.findByUrl = function (url, callback) {
   // Конструрируем запрос
   var q = [this.pHashes(url), this.kUrl(), this.kLastTime()];
   
   this.client.hmget(q, function(err, repl) {
       if (err) {
           if (callback) callback.call(this, err, nullthis);
       } else if (repl) {
           var res = this.create(repl[0], repl[1]);
           
           if (callback) callback.call(thisnull, res, this);
       } else {
           if (callback) callback.call(thisnullnullthis);
       };
   }.bind(this));
};
/////


Ну вот и все. Давайте попробуем нашу модель:

//main.js
var        redis = require('redis')
   ,    client = redis.createClient()
   ,    SiteModel = require('./siteModel')
   ;

var Site = new SiteModel(client);
var google = Site.create('www.google.ru');

google.save(function (err, res) {
   console.log('Save: ' + this.url + ' on: ' + google.time + '; err: ' + err);
   
   Site.findByUrl('
www.google.ru'function (err, res, s) {
       if (err) {
           console.log('Find by url error: ' + err);
           Site.client.quit();
       } else if (res) {
           console.log('Site found url: ' + res.url + '; time: ' + res.time);
           
           google.remove(function(err, res) {
               console.log('Remove');
               this.client.quit();
           });
       } else {
           console.log('Site not found');
           this.client.quit();
       }
   });
});


Заключение

Ну вот и все, наша простая модель готова. Теперь несколько слов почему была выбрана именно такая структура данных.
1) Нам нужно где то хранить все наши сайты, конечно можно было бы обойтись просто ключом на каждый сайт и выбирать все сайты командой keys, но такой способ не позволит нам сортировать и ограничивать результаты поиска, кроме того, если мы захотим узнать кол-во сайтов, нам придется либо заводить отдельный ключ или каждый раз пересчитывать результат команды keys. Поэтому нам остается использовать одно из трех, list, set, zset. Из этих трех типов, проще всего использовать set, т.к. он легко позволяет добавлять и удалять любой член, в отличии от list и на него расходуется меньше памяти чем на zset.
2) Hashes был выбран как структура хранящая информации о сайте, почему hashes а не обычные string, потому что hashes хорошо оптимизирован, и его рекомендуется использовать именно в таких случаях. Представ те, что нам нужно было бы сохранять еще десятка три параметров сайта, все это правильней помещать в Hashas

И напоследок, покажу как можно выбрать 20 последних сохраненных сайтов:
sort sites:set: by sytes:*:->lastTime: get sytes:*:->url: desc limit 0 20
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.