nodeJS Быстрый веб-сервер на javascript движке V8

13Апр/110

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

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

Итак, задача

Для примера, пусть мы пишем бота, который посещает веб страницы. А сохранять мы будем адреса сайтов и время когда заходили на сайт.

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

npm install redis

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

npm install redis hiredis

API библиотеки практически полностью повторяет 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.

  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(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();
   	 }
    });
});

Заключение

Ну вот и все, наша простая модель готова. Теперь несколько слов почему была выбрана именно такая структура данных.

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

И напоследок, покажу как можно выбрать 20 последних сохраненных сайтов:
sort sites:set: by sytes:*:->lastTime: get sytes:*:->url: desc limit 0 20

Комментарии (0) Пинги (0)

Пока нет комментариев.


Оставить комментарий


Нет обратных ссылок на эту запись.