Пишем модель для Redis
В целях изучения Redis и nodejs, давайте напишем небольшую модель.
Итак, задача
Для примера, пусть мы пишем бота, который посещает веб страницы. А сохранять мы будем адреса сайтов и время когда заходили на сайт.
Итак, давайте приступим. Первое что нам нужно это скачать модуль для работы с Redis в nodejs, рекомендуемый к использованию модуль лежит по адресу: https://github.com/mranney/node_redis и его легко можно установить из менеджера пакетов nodejs выполнив команду:
npm install redisАвтор библиотеки рекомендует так же использовать hiredis, библиотека для разбора ответов Redis, что увеличивает производительность модуля. Устанавливать все так же просто из менеджера пакетов nodejs.
npm install redis hiredisAPI библиотеки практически полностью повторяет API Redis, поэтому можно сразу смотреть список команд Redis и использовать их.
Hello Redis
Давайте напишем небольшой пример в стиле “hello world”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // 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.
- String — это самый простой тип ключей, представляет собой структуру Ключ -> Значение. Несмотря на то что он называется String, сюда можно записывать строковые, числовые и битовые значения.
- List — этот тип данных представляет собой аналог массивов.
- Hashes — это специальный тип данных, представляющий собой структуру Поле -> Значение. В качестве типов полей могут быть строки и числа.
- 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(this, new Error('Модель должна быть создана перед сохранением'), null, this); }; };
Оператор 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, null, this); } else { if (callback) callback.call(this, null, 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, null, this); } else { if (callback) callback.call(this, null, 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, null, this); } else if (repl) { var res = this.create(repl[0], repl[1]); if (callback) callback.call(this, null, res, this); } else { if (callback) callback.call(this, null, null, this); }; }.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(); } }); });
Заключение
Ну вот и все, наша простая модель готова. Теперь несколько слов почему была выбрана именно такая структура данных.
- Нам нужно где то хранить все наши сайты, конечно можно было бы обойтись просто ключом на каждый сайт и выбирать все сайты командой keys, но такой способ не позволит нам сортировать и ограничивать результаты поиска, кроме того, если мы захотим узнать кол-во сайтов, нам придется либо заводить отдельный ключ или каждый раз пересчитывать результат команды keys. Поэтому нам остается использовать одно из трех: list, set, zset. Из этих трех типов, проще всего использовать set, т.к. он легко позволяет добавлять и удалять любой член, в отличии от list и на него расходуется меньше памяти чем на zset.
- Hashes был выбран как структура хранящая информации о сайте. Почему hashes, а не обычные string? Потому что hashes хорошо оптимизирован, и его рекомендуется использовать именно в таких случаях. Представьте что нам нужно было бы еще сохранять еще десятка 3 параметров сайта — все это правильней помещать в Hashes.
И напоследок, покажу как можно выбрать 20 последних сохраненных сайтов:
sort sites:set: by sytes:*:->lastTime: get sytes:*:->url: desc limit 0 20
Нет обратных ссылок на эту запись.