Skip to content

Хранилища данных в Node.js: Redis

10/08/2010

Последний раз я серьёзно заинтересовался Redis после посещения DevConf 2010 — там был очень интересный доклад о возможностях и применении этой системы. Теперь, когда API Ноды несколько устаканилось и коннекторы пришли в более-менее стабильный вид, можно поэкспериментировать и с Redis, помимо прочих хранилищ 🙂

Установка redis

Redis будем собирать из исходников. Мы возьмём вторую ветку:

        wget http://redis.googlecode.com/files/redis-2.0.0-rc4.tar.gz        

Распаковываем полученный tgz-файл и в получившейся директории делаем make. Всё, Redis должен скомпилироваться. Как вариант, можете установить Redis из менеджера пакетов, но тогда внимательно смотрите на результаты тестов коннектора — в старых версиях Redis некоторых возможностей может не оказаться.

По умолчанию Redis запускается не в фоновом режиме, и отправляет всякие debug-сообщения прямо в консоль. Чтобы это исправить, надо отредактировать его конфиг. Он лежит в той же папке и называется redis.conf. Меням там:

daemonize no
logfile stdout

…на…

daemonize yes
logfile /var/log/redis.log

Теперь запускаем redis с этим конфигом:

./redis-server ./redis.conf        

Я скопировал конфиг в /etc/redis.conf, но это уже дело вкуса.

Использование с Node.js

Нам нужен будет коннектор node-redis-client, но не основной ветки, а redis-2.0. Делаем это так:

git clone git://github.com/fictorial/redis-node-client.git
cd redis-node-client
git checkout -b redis-2.0 origin/redis-2.0        

Теперь у нас есть клиент для второй ветки Redis. Чтобы убедиться что всё работает, запускаем тесты из соответствующей директории:

cd test
node test.js        

Если сервер скажет [INFO] All tests have passed., значит всё установлено и настроено правильно, и мы готовы к использованию коннектора.

Есть ещё тест фунционала Publish/Subscribe в папке examples, там очень простой и понятный код. Так реализуется Publish-часть:

var
  sys = require("sys"),
  client = require("../lib/redis-client").createClient();

// Publish a message once a second to a random channel.

setInterval(function () {
  var
    channelName = "channel-" + Math.random().toString().substr(2),
    payload = "The time is " + (new Date());

  sys.puts('Channel is ' + channelName);

  client.publish(channelName, payload,
    function (err, reply) {
      sys.puts("Published message to " +
        (reply === 0 ? "no one" : (reply + " subscriber(s).")));
    });
}, 1000);        

Здесь Node соединяется с Redis-сервером и каждую секунду отправляет сообщение в случайный канал (channelName) с текущим временем. А вот так выглядит клиент:

var
  sys = require("sys"),
  client = require("../lib/redis-client").createClient();

sys.puts("waiting for messages...");

client.subscribeTo("*",
  function (channel, message, subscriptionPattern) {
    var output = "[" + channel;
    if (subscriptionPattern)
      output += " (from pattern '" + subscriptionPattern + "')";
    output += "]: " + message;
    sys.puts(output);
  });        

Здесь мы подписываемся на все каналы («*»), и выводим все полученные сообщения на экран.

Практика

Но просто так соединяться с Redis из Node не очень интересно. Надо сделать хоть какой то сервис, чтобы опробовать технологию в деле. Я хочу сделать твиттероподобный сайт, как в PHP-мануале Redis.

Вначале — о разных видах данных в Redis. Помимо простого сохранения строки по ключу Redis умеет работать со множествами и списками. Множество — неупорядоченный набор элементов, можно добавлять и удалять элементы атомарно. Список — упорядоченный набор. Множества я буду использовать там где порядок элементов не важен (фолловеры, фолловинги), списки будут использоваться собственно для постов, чтобы они шли по порядку.

Но прежде чем приступать к постам, надо сделать способ регистрации и входа для пользователей. При регистрации мы проверяем не занято ли имя пользователя и сохраняем в Redis примерно такую структуру:

SET uid:1000:username kurokikaze
SET uid:1000:password 5f4dcc3b5aa765d61d8327deb882cf99

Здесь 1000 — уникальный идентификатор пользователя. Чтобы назначить новому пользователю уникальный ID, используем атомарный инкремент. Операция INCR возвращает значение переменной и вместе с тем увеличивает её на единицу, так что разные вызовы будут возвращать последовательные ID. Этот паттерн применяется везде, где в MySQL Вы воткнули бы AUTO_INCREMENT. ID для нового пользователя будет храниться в переменной global:nextUserId

Сам процесс регистрации выглядит следующим образом:

var registerUser = function(username, password, callback) {
	// check username availability
	client.incr('global:nextUserId', function(err, uid) {
	 if (uid) {
	  sys.puts('Uid is ' + uid);
	  client.get('username:' + username + ':id', function(err, id) {
		if (!id) {
		  client.set('uid:' + uid + ':username', username, function(err) {
			client.set('uid:' + uid + ':password', hash.md5(password), function(err) {
			  client.set('username:' + username + ':id', uid, function(err) {
				callback(true, parseInt(uid.toString()));
			  });
			});
		  });
		} else {
		  sys.puts('Username already taken!');
		  callback(true, parseInt(id.toString()));
		}
	  });
	 } else {
		sys.puts('Cannot get UID: ' + err);
		callback(true);
	 }
	});
}

По хорошему, чтобы избежать спагетти-кода, для серии последовательных вызовов здесь лучше использовать Do, но пока сойдёт и так. MD5 я использую из библиотеки hashlib.

Логин работает примерно так же:

var login = function(username, password, callback) {
        client.get('username:' + username + ':uid', function(err, uid) {
            if (uid) {
              client.get('uid:' + uid + ':password', function(err, password){
                if (hash.md5(password) == password) {
                  callback(false, parseInt(uid.toString()));
                } else {
                  sys.puts('Wrong password');
                  callback(true);
                }
              });
            } else {
              sys.puts('No such user');
              callback(true);
            }
        });
    }

Все ID, возвращаемые из Redis (особенно числовые) лучше пропускать через toString(), чтобы преобразовать их из буферов в строки. Теперь сделаем фолловинг. Фолловинг будет работать с помощью множеств. Множества поддерживают несколько команд, нас интересуют sadd (добавление элемента), srem (удаление элемента) и smembers — получение списка элементов множества. Фолловим:

var follow = function(uid, fid, callback) {
	client.sadd('uid:' + uid + ':following', fid, function(err) {
	  if (err) {
		sys.puts('Error following');
		callback(true);
	  } else {
		client.sadd('uid:' + fid + ':followers', uid, function(err) {
		  if (err) {
			callback(true);
			sys.puts('Error following');
		  } else {
			callback(false);
		  }
		});
	  }
	});
}

И unfollow (пока без проверки на ошибки):

var unfollow = function(uid, fid, callback) {
  client.srem('uid:' + uid + ':following', fid, function(err) {
    client.srem('uid:' + fid + ':followers', uid, function(err) {
  	  callback(false);
    });
  });
}

Redis-коннектор следует обычному соглашению для Node — первым в callback передаётся флаг ошибки. Это должно позволить использовать его с Do без дополнительного геморроя.

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

var post = function(uid, body, callback) {
  // В global:nextPostId хранится ID следующего поста, мы его используем как автоинкремент
  client.incr('global:nextPostId', function(err, pid) {
    // Получаем фолловеров
    client.smembers('uid:' + uid + ':followers', function(err, followers) {
      if (err) {
        sys.puts('Error writing to followers');
      }
      // Получаем ID текущего автора
      client.get('uid:' + uid + ':username', function(err, username) {
        if (!err) {
          // Размещаем пост в своём таймлайне
          client.lpush('uid:' + uid + ':posts', JSON.stringify({'author':uid, 'body':body}), function() {
            callback(false, pid);
          });
          // Размещаем пост в таймлайнах фолловеров, но не дожидаемся выполнения
          for (var fid in followers) {
            sys.puts('To ' + followers[fid] + ' timeline');
            client.lpush('uid:' + followers[fid] + ':posts', JSON.stringify({'author':uid, 'body':body}), function() {});
          }
        } else {
          sys.puts('Post from non-existing username');
          callback(true);
        }
      });
    })
  });
}

В принципе, что то уже получается. Объединим это в один модуль и уже можно пробовать:

ritter.registerUser('user_leader','password', function(err, uid_leader) {
  ritter.registerUser('user_follower', 'password', function(err, uid_follower) {
    sys.puts('Leader is ' + uid_leader + ', follower is ' + uid_follower);
    ritter.follow(uid_follower, uid_leader, function(err) {
      if (err) {
        sys.puts('Error following leader');
      } else {
        sys.puts('Now following leader');
      ritter.post(uid_leader, 'To all followers: hello', function(err) {
        if (err) {
          sys.puts('Error posting to leader timeline');
        }
        ritter.getPosts(uid_follower, function(err, data) {
          if (err) {
            sys.puts('Something wrong');
          } else {
            sys.puts('##Posts:');
            for (var pid in data) {
              sys.puts('Post: ' + data[pid].toString());
            }
          }
        });
      });
      }
    });
  });
});

Без Do опять получается callback-спагетти 🙂 Здесь мы регистрируем двух юзеров, заставляем одного фолловить другого и проверяем, появятся ли его пост в ленте фолловера. У меня всё сработало нормально. В принципе, этот модуль уже можно использовать с Express для создания твиттероподобного сайта. У нас есть возможность регистрироваться, просматривать фолловеров, фолловингов, размещать статусы и просматривать свою ленту — минимальный набор возможностей для подобного сервиса.

Ссылки по теме

Реклама
2 комментария
  1. Как у вас отлично все это получается, надо наконец собраться и попробовать 🙂

Trackbacks & Pingbacks

  1. Хранилища данных в Node.js: Redis « nodeJS

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: