티스토리 뷰

do/nodejs

Redis

dooo.park 2019. 4. 8. 15:32

Redis(Remote Dictionary Server)

https://redis.io/

"키-값" 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비 관계형 데이터베이스 관리 시스템(DBMS)이다. 

모든 데이터를 메모리로 불러와서 처리하는 메모리 기반 DBMS이다. 

 

특징

① NoSQL

NoSQL 데이터베이스 중 하나로 다양한 자료구조를 저장할 수 있다.

대표적으로 Redis, MongoDB가 있다. 

② In-memory

디스크가 아닌 I/O가 가장 빠른 메모리에 저장함으로써 빠르다. 
단순한 구조의 key-value 방식으로써 빠른 속도를 지원한다. 

 

③ Memory Store + DB

Memory Store는 서버를 재시작하는 순간 저장했던 내용들이 날아간다. 

서버를 재시작해도 데이터를 유지하고 싶다면 DB를 사용해야 하는데

MySQL, MongoDB는 접근과 커넥션을 유지하는데 비용이 많이 들고 속도가 느리다.

이에 비해 Redis는 DB이므로 데이터가 유지되고 데이터를 메모리에 로드하기 때문에 접근 속도가 빠르다.

세션이나 키-값, 간단한 자료 구조 데이터를 유지하면서 빠르게 사용할 수 있다.

 

Redis는 mongoDB처럼 전체 데이터를 영구 저장하기보다는 캐시처럼 휘발성이나 임시 데이터를 저장하는 데 사용된다.

백업이나 복구용으로 디스크에 데이터를 주기적으로 저장하기는 하지만 데이터는 모두 메모리에 저장되기 때문에 접근 속도가 빠르다. 

node.js를 클러스터링할 때 인스턴스 간 상태 정보를 공유하거나, 세션과 같은 휘발성 정보를 저장하거나 또는 캐시로 사용할 수 있다.

 

Redis는 키로 데이터를 저장하고 조회하는 Set/Get 기능이 있으며, 메시지를 전달하기 위한 큐로도 사용할 수 있다.

큐로써의 기능은 하나의 클라이언트가 다른 클라이언트로 메시지를 보내는 1:1 기능뿐 아니라, 

하나의 클라이언트가 다수의 클라이언트에게 메세지를 발송하는 발행/배포(Publish/Subscribe) 기능을 제공한다.

일반적인 Pub/Sub 시스템의 경우 하나의 Topic에서만 Subscribe 하지만,

redis에서는 pattern matching을 통해서 다수의 Topic에서 message를 subscribe 할 수 있다.

 


자료구조

기본적인 데이터 조작(키-값) 
set : 첫번째 인자(키), 두 번째 인자(값), 출력을 통해 Redis에 데이터 추가 
get : 첫번째 인자(키), 두 번째 인자(함수의 첫 번째 인자(err) 두 번째 인자(값)를 통해 출력 

client.set('color', 'red', redis.print);  // redis.print : 수행결과 출력 혹은 오류 출력 
client.get('color', function(err, value) { 
  if (err) throw err; 
  console.log('get_color : ' + value); 
}); 

client.del('name'); 

키 삭제 명령어

client.exists('name');

키가 존재하는 지 확인하는 명령어(있으면 1 없으면 0 반환)

client.rename('animals', 'pets'); 
키 이름을 바꾸는 명령어

 

① String
Key에 대해서 문자열을 저장한다. 텍스트 문자열뿐만 아니라 숫자나 최대 512 mbyte까지의 바이너리도 저장할 수 있다.

set으로 설정하고 get으로 가져온다. 

client.set('data', 'string');  
client.get('data', (err, reply) => {  
  console.log(reply); // string  
}); 

② List
Key에 대해서 List타입을 저장한다.(키-배열)

List에는 값들이 들어갈 수 있으며(중복 허용), index값을 이용해서 지정된 위치의 값을 넣거나 뺄 수 있고

push/pop 함수를 이용하여 리스트 앞뒤에 데이타를 넣거나 뺄 수 있다. 

일반적인 자료 구조에서 Linked List와 같은 자료 구조라고 생각하면 된다.


메모리가 허용하는 한 매우 많은 데이터를 저장할 수 있다.  
List 길이가 길어지면 검색 속도도 느려지게 된다.  

rpush는 자바스크립트의 push랑 비슷하고, lpush는 unshift랑 비슷하다.

 

lpush(리스트명, 값, 출력)
리스트에 값 추가

lrange(리스트명, 시작, 끝, 함수(에러, 값))
리스트에서 값 가져오기

client.rpush('fruits', 'apple', 'orange', 'apple'); 
client.lpush('fruits', 'banana', 'pear'); 
client.lrange('fruits', 0, -1, (err, arr) => { 
  console.log(arr); // ['pear', 'banana', 'apple', 'orange', 'apple'] 
}); 


client.lpush('tasks', 'Paint the bikeshed red.', redis.print); // 리스트에 값 추가  
client.lpush('tasks', 'Paint the bikeshed green.', redis.print); // 리스트에 값 추가  
client.lrange('tasks', 0, -1, function(err, items) { // 시작, 종료인자 이용해 리스트 항목 가져오기  
                                                     // -1는 리스트의 마지막 항목의미, 즉 다 가져오기  
if (err) throw err; items.forEach(function(item, i) {  
    console.log('list '+i+' : ' + item); });  
}); 

③ Set
Set 자료 구조는 집합이라고 생각하면 된다. (키-set)

Key에 대해서 Set을 저장할 수 있는데, List 구조와는 다르게 주의할 점은 집합이기 때문에 같은 값이 들어갈 수 없다.(중복 허용 X)

집합의 특성을 이용한 집합 연산, 교집합, 합집합 등의 연산이 가능하다.

 

Set은 순서가 없는 문자열의 모음이다. 

유일한 요소만 저장한다. 즉, 중복 값이 없다. 

같은 값을 두 개 저장한다면, 두 번째 저장하려 하는 값은 무시가 된다. 

 

saad(set, 항목)

set에 항목을 추가 
smembers(set)

set에 저장된 값을 반환 

client.sadd('animals', 'dog', 'cat', 'bear', 'cat', 'lion'); 
client.smembers('animals', (err, set) => { 
  console.log(set); // ['cat', 'dog', 'bear', 'lion'] 
}); 

client.sadd('test','kkm',redis.print);  -> Reply : 1 
client.sadd('test','kkm2',redis.print);-> Reply : 1 
client.sadd('test','kkm5',redis.print);-> Reply : 1 
client.sadd('test','kkm2',redis.print);-> Reply : 0 
client.sadd('test','kkm3',redis.print);-> Reply : 1 
client.sadd('test','kkm4',redis.print);-> Reply : 1 
client.sadd('test','kkm3',redis.print);-> Reply : 0 

client.smembers('test',function(err,data){ 
    console.log(data); 
}); 

중복은 무시되고, 넣었던 순서와 상관없이 섞인 결과가 출력되었다. 
Reply 값이 0이 되는 것은, 두 번째 저장하려는 것이라 무시가 되는 것이다. 

 

④ Sorted Set
SortedSet은 Set과 동일하지만, 데이터를 저장할 때, value 이외에, score라는 값을 같이 저장한다.(키-정렬 set)

그리고 이 score라는 값에 따라서 데이터를 정렬(소팅)해서 저장한다. 

순차성이나 순서가 중요한 데이터를 저장할 때 유용하게 저장할 수 있다.

 

오름차순으로 정렬되는데 내림차순으로 하고 싶다면 zrevrange를 사용하면 된다. 

client.zadd('height', 180, 'zero', 168, 'aero', 176, 'nero', 172, 'hero'); 
client.zrange('height', 0, -1, (err, sset) => { 
  console.log(sset); // ['aero', 'hero', 'nero', 'zero' 
}); 

⑤ Hash
Key에 해시 테이블을 저장하는데 (field, value) 형태로 field를 해쉬의 키로 저장한다. (키-해시)

키가 있는 데이터를 묶어서 저장하는데 유용하며 데이터의 접근이 매우 빠르다. 

비순차적인 랜덤 액세스 데이터에 적절하다.

 

hmset(해시 테이블명, 항목들)

해시 테이블에 Key로 식별되는 value 값들을 항목으로 추가한다.

hget(해시 테이블명, 항목, 함수(에러, 항목 값))

해시 테이블에서, 인자로 받는 항목의 값을 가져온다. 
hkeys(해시 테이블명, 함수(에러, 항목의 키))

해당 해시 테이블의 저장된 항목의 키값을 가져온다. 

client.hmset('friends', 'name', 'zero', 'age', 24); 
client.hgetall('friends', (err, obj) => { 
  console.log(obj); // { name: 'zero', age: '24' } 
}); 

client.hmset('camping', { 
  'shelter': '2-person tent', 
  'cooking': 'campstove' 
}, redis.print); // 해시 테이블 추가 및 결과 출력 

client.hget('camping', 'cooking', function(err, value) { // camping의 해시테이블에서 cooking의 키값 
  if (err) throw err; 
  console.log('hash Will be cooking with : ' + value); // 해당 값 가져오기 
}); 

client.hkeys('camping', function(err, keys) { // camping의 해시테이블 모든 키 데이터 가져오기. 
  if (err) throw err; 
  keys.forEach(function(key, i) { 
    console.log('hash '+ i+' : ' + key); 
  }); 
}); 

Express-session과 Redis 사용하기

 

① Redis 설치

Redis는 공식적으로 Windows 버전을 제공하지 않는다. 

하지만 MS Open Tech에 의해 비공식적으로 제공되는 Windows 64-bit 바이너리 파일을 다운로드할 수 있다.

https://github.com/MicrosoftArchive/redis

https://github.com/MicrosoftArchive/redis/releases

다운로드한 파일을 압축 해제하면 바이너리 파일 목록이 보인다. 

기본 설정 사용 시 6379번 포트로 Redis가 실행되는 것을 확인할 수 있다.

//Redis 서버 실행
C:\redis-3.0.504> redis-server 

//앞서 실행한 Redis를 백그라운드에서 서비스하려면 서비스를 등록하고 시작해야 한다.
C:\redis-3.0.504> redis-server --service-install 

//서비스 등록을 완료하면 서비스를 실행한다. 
C:\redis-3.0.504> redis-server --service-start 

//서비스의 종료는 아래와 같다. 
C:\redis-3.0.504> redis-server --service-stop 

//Redis 클라이언트 실행하기 
C:\redis-3.0.504> redis-cli 

//127.0.0.1:6379> set foo bar 
//OK 
//127.0.0.1:6379> get foo 
//"bar"

② express-session, redis 연동

npm install express-session
npm install connect-redis

Redis 저장소 생성 방법

app.js

const redis = require('./redis.js');

app.use(session({
    store: new RedisStore({
        client: redis,
        host: 'localhost',
        port: 6379,
        prefix : "session:",
        db : 0,
        saveUninitialized: false,
        resave: false
    }),
    secret: {keyboard cat},
    cookie: { maxAge: 2592000000 }
}));

redis.js

const redis = require('redis');
const client = redis.createClient({port},{host});

client.auth({password}, function (err) {
    if (err) throw err;
});

client.on('error', function(err) {
    console.log('Redis error: ' + err);
});

module.exports = redisClient;

레디스를 사용할 때 연동이 안되면 레디스가 연결이 안 됐을 가능성이 가장 크므로 error를 통해 확인해보면 된다.

 

로컬 호스트의 기본 TCP/IP를 이용하여 Redis 서버와 연결하는 경우

const redis = require('redis'); 
const client = redis.createClient(6379, '127.0.0.1'); // 레디스 서버를 실행해보면 포트번호가 나온다. 
                                                      // 포트번호입력 후, 서버 
client.on('error', function (err) { 
  console.log('Error ' + err); 
}); 

session과 연결

const session = require('express-session'); 
const connectRedis = require('connect-redis'); 
const RedisStore = connectRedis(session); 
const sess = { 
 resave: false, 
 saveUninitialized: false, 
 secret: sessionSecret, 
 name: 'sessionId', 
 cookie: { 
 httpOnly: true, 
 secure: false, 
  }, 
 store: new RedisStore({ url: '레디스 호스팅 주소', logErrors: true }), 
}; 
app.use(session(sess));

store : 세션을 어디에 저장할지를 고르는 옵션

기본값은 메모리 스토어로, 서버의 메모리에 저장한다. 따라서 서버가 꺼지면 세션 데이터들이 다 날아가게 된다.

이것을 RedisStore로 바꾸면 이제 세션 데이터를 레디스에 저장한다. 서버가 꺼져도 데이터가 유지된다.

logErrors는 레디스 에러를 로깅하는 것이라 필요에 따라 껐다 켰다 하면 된다.

 


레디스(Redis)와 연동
로그인 프로토콜과 로그아웃프로토콜에 세션 저장과 세션 삭제 코드를 추가함으로써, 손쉽게 세션의 사용이 가능하다. 

하지만 메모리 세션을 사용하면 익스프레스에서 아래와 같은 경고 메세지가 뜬다.

Warning: connection.session() MemoryStore is not designed for a production environment,  
as it will leak memory, and obviously only work within a single process. 

이 내용은 Production 모드에서는 사용하지 말라. 실제 서버에서는 메모리 용량 제한으로 세션 정보를 유실할 수 있다는 내용이다.

또한, 오토스케일링으로 인한 서버 인스턴스 증가로 세션 정보 공유에 대한 문제가 발생할 수 있기 때문에, 

실제 프로덕션에서는 세션을 별도의 저장소로 레디스, 몽고디비 등을 사용한다.

 

익스프레스 버전에 따라 세션과 레디스 서버를 연결하지 못하는 경우가 있기 때문에, 익스프레스에서 제공하는 기본 세션 미들웨어를 사용하기보다는, express-seesion을 사용하는 게 편하다.


express-session과 redis와 익스프레스를 세팅하는 코드는 아래와 같다.

const session = require('express-session'); 
const RedisStore = require('connect-redis')(session); 

app.use(express.cookieParser()); 
app.use(session({ 
  store: new RedisStore(/*redis config: host, port 등*/), // 세션 저장소를 레디스 서버로 설정 
  /* 이하 express.session 코드와 동일 */ 
})); 

세션 설정하는 부분에서만 레디스 정보를 추가하고 나머지 부분은 express.session과 동일하다.

 

③ session과 redis를 이용하여 로그인 구현하기

레디스가 연동되었다고 한다면, 다음을 진행해보자.

- 로그인 시 세션을 저장.

- 로그아웃 시 세션을 삭제.

- 로그인이 되어있으면 특정 페이지 이동. 그렇지 않다면 HOME 페이지 이동.

 

기본적으로 세션을 접근하는 방식은 아래와 같다.

req.session.{key} = value;
req.session.key = '123'; // 'key' 라는 key에 '123' value 삽입.
req.session.uid = 'ABCD';// 'uid' 라는 key에 'ABCD' value 삽입.
req.session.destroy(); // 레디스 서버에 저장된 세션 삭제.

app.js

app.get('/',function(req,res){  
    // 세션값이 있으면
    if(req.session.key) {
        res.redirect('/admin');
    } else {
        // 없으면 홈으로 이동
        res.render('index.html');
    }
});

app.post('/login',function(req,res){
    // 로그인 시 key값을 email로 session 저장
    req.session.key=req.body.email;
    res.end('done');
});

app.get('/logout',function(req,res){
    // 로그아웃 시 세션 삭제
    req.session.destroy(function(err){
        if(err){
            console.log(err);
        } else {
            res.redirect('/');
        }
    });
});

세션이 저장된다면 redis 명령어를 통해 세션을 확인할 수 있다.

keys * // 모든 key 검색
get {key} // key에 대한 value 검색

 

node.js Redis Session 예제

1.

var express   = require('express');
var router          = express.Router();
var path   = require('path');
var favicon   = require('serve-favicon');
var cookieParser  = require('cookie-parser');
var bodyParser  = require('body-parser');
var http    = require('http');
var expressSession  = require('express-session');
var fs     = require('fs');
var RedisStore      = require('connect-redis')(expressSession);
var redis           = require('redis');
var app = express();
//http////////////////////////////////
var httpServer=http.createServer(app);
httpServer.listen(80, function(){
 console.log("http server listening on port " + 80);
});
////////////////////////////////
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());

var redisConfig = {
    "host": "localhost",
    "port": 6379,
    "prefix": "session:",
    "db": 0,
    "client": redis.createClient(6379,"localhost")
}
const session = expressSession({
    secret : new Date().getMilliseconds()+"brokim",
    resave : false,
    store  : new RedisStore(redisConfig),
    saveUninitialized:true,
    cookie : {
        maxAge : 1000 * 60 * 3
    }
});
app.use(session);
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(express.static(path.join(__dirname, 'public')));
app.get('/',function(req,res){
  fs.readFile('./public/login/index.html', function(error, data) {
        if(error != undefined) {
            console.log(error);
            res.writeHead(404);
            res.end();
        } else {
            res.writeHead(200, {'Content-Type': 'text/html'});
            res.end(data);
        }
    });
});

/////////////////////////////////////////////////////////////////////////////////
/////////////////RESTFUL APIS/////////////////////////////////////////////////////
//LOGIN///////////////////////////////////////////////////////////////////////////
app.route('/login')
    .post(function login_post(req,res){
        try{
        //query는 따로 구현하면 됨.
        //var query = require('./query.js');
        query.login([
            req.body.id
            ,salted(req.body.pw)],
            function loginAPI(ret){
            if(ret.fail==0){
                req.session.privilege=ret.result.privilege;
                req.session.uid=ret.result.id;
                req.session.name=ret.result.name;
            }
            res.send(ret);
        });
        } catch(error){
        }
    });

app.get('/logout', function logout_get(req,res){
    req.session.destroy(function logout_destroy(err){
        if(err!==null){
            RESULT.GENERAL_FAIL.result="";
            res.send(RESULT.GENERAL_FAIL);
        } else {
            RESULT.GENERAL_SUCCESS.result="";
            res.send(RESULT.GENERAL_SUCCESS);
        }
    });
});

module.exports = app;

2. https://github.com/hotehrud/router_mvc

 

※ Redis의 채널을 통한 데이터 조작 (PUB/SBU를 이용한 대화 서버)

var net = require('net'); 
var redis = require('redis'); 
var server = net.createServer(function(socket) { 
  var subscriber; // 구독자 설정 
  var publisher;  // 발행자 설정 
    subscriber = redis.createClient(6379, '127.0.0.1');  // 각 사용자에 대한 구독자 설정 
    subscriber.subscribe('main_chat_room'); // 구독자의 채널 설정 

    subscriber.on('message', function(channel, message) { // 메시지 받았을 경우, 메시지 보여준다 
      socket.write('Channel ' + channel + ': ' + message);  
    }); 

    publisher = redis.createClient(6379, '127.0.0.1'); // 발행자 설정 

  socket.on('data', function(data) { 
    publisher.publish('main_chat_room', data); // 메시지 입력시, 해당 채널로 발행 
  }); 

  socket.on('end', function() {  // 접속 종료시 
    subscriber.unsubscribe('main_chat_room');  // 구독자는 채널 해제 
    subscriber.end();  // 구독자 설정 해제 
    publisher.end();  // 발행자 설정 해제 
  }); 
}); 
server.listen(3000);  // 접속 포트 설정

 

 

 


참고사이트

https://mygumi.tistory.com/91

https://m.blog.naver.com/PostView.nhn?blogId=ksh81850&logNo=220368320794&proxyReferer=https%3A%2F%2Fwww.google.co.kr%2F

https://www.zerocho.com/category/NodeJS/post/5a3238b714c5f9001b16c430

https://bcho.tistory.com/1098

http://blog.securekim.com/2017/10/nodejs-redis-session.html

'do > nodejs' 카테고리의 다른 글

express login  (0) 2019.04.09
Request, Response  (0) 2019.04.07
dotenv  (0) 2019.04.05
Swagger  (1) 2019.04.05
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
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
글 보관함