Redis 사용해보기
2019-12-20 19:59:02

redis란?

  • key-value 형식의 NoSQL 데이터 베이스
  • 일반적인 DB는 디스크에 저장을 하는데 redis는 메모리에 저장을 한다.

사용하면서 느꼈었던 점

  • 데이터베이스를 다양하게 다뤄보진 않았지만 내가 다뤄본 거 중에는 읽고/쓰기가 제일 빨랐음
  • 다양한 데이터 타입이 존재 한다.

설치

Mac의 경우 brew를 통해 설치

1
2
3
brew install redis
brew services start redis
redis-server -v # 버전 확인

실행할 폴더에서 터미널을 열고 다음의 명령어를 입력 한다.

1
2
npm init -y
npm i redis -S # node.js redis client

redis 데이터 타입

데이터 타입의 명령어들이 엄청 많기 때문에 전부 쓰기는 불가능 해서 최대한 써 본 위주로 작성

공통

redis에 연결하기 위한 코드

1
2
3
const redis = require('redis');

const redisClient = redis.createClient(6379, 'localhost');

strings

key - value를 저장할 수 있는 데이터 타입이다.

모든 데이터(숫자, 문자, JSON, 기타 등등)를 쓸 수 있으나 읽을 때는 데이터 타입이

string으로 되어 있다 그래서 타입 이름이 string인가..? 크기는 512MB까지 가능

set, mset

  • set은 하나의 key와 value를 저장을 한다.
  • mset은 여러 개의 key, value를 저장할 때 쓴다.
1
2
3
4
5
6
7
8
9
redisClient.set('key', 1, (err, result) => {
if (err) console.log(err);
console.log(result); // OK
});

redisClient.mset(['a', 1, 'b', 2], (err, result) => { // 열 안에 [key, value ...]
if (err) console.log(err);
console.log(result); // OK
});

get, mget

  • get은 key를 통해 value를 읽는다.
  • mget은 여러 개의 key로 value를 읽을 때 사용 한다.
1
2
3
4
5
6
7
8
9
redisClient.get('key', (err, result) => {
if (err) console.log(err);
console.log(result, typeof result); // value, string
});

redisClient.mget(['a', 'b'], (err, result) => {
if (err) console.log(err);
console.log(result); // ['1', '2']
});

del

key를 삭제 한다.

1
2
3
4
redisClient.del('key', (err, result) => {
if (err) console.log(err);
console.log(result); // 1
});

incrby

해당 숫자 만큼 증,감소가 진행 된다 이 녀석은 원자성을 지원 한다.

예로 0인 상태에서 A에서는 숫자를 2를 증가 시키고 B에서는 4를 증가 하라는 게 비슷한 시기에

일어났을 때 덮어쓰지 않고 하나 하나씩 처리하여 총 6이 된다.

그리고 이런 증,감소는 리턴 타입은 숫자로 받는다.

1
2
3
4
redisClient.incrby('key', 2, (err, result) => {
if (err) console.log(err);
console.log(result, typeof result); // 2, number
});

setex

지정한 시간이 지나면 key가 삭제 된다 만료가 필요한 캐시나 세션에 좋은 타입이다.

단위는 초

1
2
3
4
redisClient.setex('Session:UserA', 10, 'abc', (err, result) => {
if (err) console.log(err);
console.log(result); // OK
});

lists

list는 자료구조에 나오는 스택, 큐와 같다고 생각 하면 된다.

리스트에 데이터가 하나도 없을 시 자동으로 삭제가 되고 리스트가 보유할 수 있는 데이터의 개수는

최대 232 - 1 이다.

lpush

스택, 큐 처럼 마지막에 삽입 되고 돌아오는 결과 값은 현재 리스트에 포함된 값의 개수

1
2
3
4
5
6
7
8
9
redisClient.lpush('MyList', 'a', (err, result) => {
if (err) console.log(err);
console.log(result); // 1
});

redisClient.lpush('MyList', ['c', 'd'], (err, result) => {
if (err) console.log(err);
console.log(result); // 3
});

rpop

먼저 들어온 데이터를 꺼내고 삭제, 큐를 생각하면 되겠다.

1
2
3
4
redisClient.rpop('MyList', (err, result) => {
if (err) console.log(err);
console.log(result); // 'a'
});

lpop

마지막에 들어온 데이터를 꺼내고 삭제, 스택을 생각하면 된다.

1
2
3
4
redisClient.lpop('MyList', (err, result) => {
if (err) console.log(err);
console.log(result); // 'd'
});

lrange

인덱스로 범위를 지정해서 리스트를 조회 할 수 있다.

0, -1로 하면 전체 조회를 하고 인덱스는 제일 마지막 데이터가 0부터 시작 한다.

1
2
3
4
redisClient.lrange('MyList', 0, -1, (err, result) => {
if (err) console.log(err);
console.log(result); // 'c'
});

sets

sets은 순서가 없고 똑같은 데이터가 없는 구조이며 하나의 set이 가질 수 있는 데이터의

최대 개수는 232 - 1 이다.

sadd

하나 또는 여러 개의 데이터를 삽입할 수 있고 성공 시 리턴은 삽입한 개수다.

중복된 데이터가 있는 경우에는 0을 받는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
redisClient.sadd('MySet', 'a', (err, result) => {
if (err) console.log(err);
console.log(result); // 1
redisClient.sadd('MySet', 'a', (err, result) => {
if (err) console.log(err);
console.log(result); // 0
});
});

redisClient.sadd('MySet', ['b', 'c', 'd'], (err, result) => {
if (err) console.log(err);
console.log(result); // 3
});

srem

하나 또는 여러 개의 데이터를 삭제할 수 있고 성공 시 삭제된 개수를 리턴 받는다.

1
2
3
4
5
6
7
8
9
redisClient.srem('MySet', 'a', (err, result) => {
if (err) console.log(err);
console.log(result); // 1
});

redisClient.srem('MySet', ['b', 'c'], (err, result) => {
if (err) console.log(err);
console.log(result); // 2
});

smembers

sets에 속한 데이터들을 모두 볼 수 있다.

1
2
3
4
redisClient.smembers('MySet', (err, result) => {
if (err) console.log(err);
console.log(result); // ['d']
});

sunion

합집합 처럼 여러 sets들의 데이터들을 합쳐서 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
redisClient.sunion(['MySet', 'MySet2'], (err, result) => {
if (err) console.log(err);
console.log(result);
/*
[
'1', 'b', '2',
'c', 'd', '4',
'3'
]
*/
});

srandmember

sets의 데이터들 중 랜덤으로 하나 가지고 온다.

1
2
3
4
redisClient.srandmember('MySet2', (err, result) => {
if (err) console.log(err);
console.log(result); // 1, 2, 3, 4 중 아무거나
});

sscan

데이터가 엄청 큰 sets에서 smembers를 호출 하는 경우 처리 하는 동안 다른 요청에 응답 하질

않으므로 그럴 경우 sscan을 사용하면 된다고 한다.

1
2
3
4
5
6
7
8
const sscan = (cursor, arr, cb) => {
redisClient.sscan('MySet3', cursor, (err, data) => {
const nextCursor = Number(data[0]);
const result = arr.concat(data[1]);
if (nextCursor === 0) return cb(null, result);
return sscan(nextCursor, result);
});
};

1000000건이 있는 sets에 재귀로 짜서 돌려봤는데 겁나 느리네…

sorted sets

정렬된 sets는 점수로 정렬된, 똑같은 데이터가 없는 구조이다.

쉽게 이해 하자면 하나의 테이블(=key)에 key(=member), value(=score)가 세트로

저장이 되는데 value는 숫자만 되고 정렬을 할 수 있다는 의미 한 가지 유의할 점은 정렬 시

같은 score가 있는 경우에는 이름순으로 정렬 된다.

zadd

하나의 member, score를 저장을 하고 성공 시 1을 리턴 하고 중복된 데이터는 0을 리턴 한다.

1
2
3
4
redisClient.zadd('MySt', 100, 'a', (err, result) => {
if (err) console.log(err);
console.log(result);
});

zincrby

하나의 member의 score를 원하는 값만큼 증,감소 할 수 있고 리턴은 증,감소된 값이다.

1
2
3
redisClient.zincrby('MySt', 30, 'a', (err, result) => {
console.log(result); // 130
});

zrange, zrevrange

zrange는 지정한 index의 범위만큼 score가 낮은 순으로 정렬된 member들을

조회 할 수 있고 0, -1은 모두 조회이다.

zrevrange는 zrange와 반대로 score가 높은 순으로 정렬된 member들을 조회 할 수 있다.

withscores를 추가하면 score도 결과에 나온다.

1
2
3
4
5
6
7
redisClient.zrange('MySt', 0, -1, (err, result) => {
console.log(result); // [a, b]
});

redisClient.zrevrange('MySt', 0, -1, 'withscores', (err, result) => {
console.log(result); // [ 'b', '200', 'a', '130' ]
});

zrank, zrevrank

둘 다 member의 index를 리턴 한다.

차이점은 zrank는 score가 작은 순으로 0, zrevrank는 score가 큰 순으로 0

1
2
3
4
5
6
7
redisClient.zrank('MySt', 'a', (err, result) => {
console.log(result); // 0
});

redisClient.zrevrank('MySt', 'a', (err, result) => {
console.log(result); // 1
});

zscore

member의 score를 리턴 한다.

1
2
3
redisClient.zscore('MySt', 'a', (err, result) => {
console.log(result); // 130, 타입은 문자형이다 숫자 아님..
});

zrem

데이터를 삭제 한다.

1
2
3
redisClient.zrem('MySt', 'a', (err, result) => {
console.log(result); // 1
});

zscan

zscan도 sscan과 비슷한 용도

1
2
3
redisClient.zscan('MySt', 0, (err, result) => {
console.log(result);
});

hashes

hashes는 하나의 key에 여러개 field, value를 저장 할 수 있는 구조

hset, hmset

  • hset은 하나의 field, value를 삽입
  • hmset은 여러개의 field, value를 삽입
1
2
3
4
5
6
7
redisClient.hset('MyHash', 'a', 1, (err, result) => {
console.log(result); // 1
});

redisClient.hmset('MyHash', ['b', 1, 'c', 2], (err, result) => {
console.log(result); // OK
});

hget, hmget

  • hget은 하나의 field로 조회 해서 value를 가지고 온다.
  • hmget은 여러 개의 field로 조회를 해서 여러 value를 가지고 온다.
1
2
3
4
5
6
7
redisClient.hget('MyHash', 'a', (err, result) => {
console.log(result); // 1
});

redisClient.hmget('MyHash', ['b', 'c'], (err, result) => {
console.log(result); // ['1', '2']
});

hdel

필드를 삭제 한다.

1
2
3
redisClient.hdel('MyHash', 'c', (err, result) => {
console.log(result); // 1
});

hgetall

해당 key의 모든 field, value를 가지고 온다.

1
2
3
redisClient.hgetall('MyHash', (err, result) => {
console.log(result); // { a: '1', b: '1' }
});

hscan

scan 시리즈들은 모두 동일

1
2
3
redisClient.hscan('MyHash', 0, (err, result) => {
console.log(result); // [ '0', [ 'a', '1', 'b', '1' ] ]
});

hyperloglog

hyperloglog는 해당 key의 데이터 개수를 구하기 위한 처음 보는 구조다.

장점은 하나의 key당 아주 작은 메모리(최대12KB)를 사용하고 단점은 이게 100% 정확도를

보장하지 않는다고 한다 표준오차는 0.81%인데(그럼 99%는 맞는다는 소리네..) 사용 예는

해당 사이트나 게임에 고유 방문자 수 같은 케이스

pfadd

키에 하나의 데이터 또는 여러 데이터를 삽입할 수 있다.

1
2
3
4
5
6
7
redisClient.pfadd('visit:2020-01-01', ['user1', 'user2', 'user3', 'user4'], (err, result) => {
console.log(result);
});

redisClient.pfadd('visit:2020-01-02', 'user4', (err, result) => {
console.log(result);
});

pfcount

하나의 키 또는 여러 키의 개수를 받을 수 있다.

1
2
3
4
5
6
7
redisClient.pfcount('visit:2020-01-02', (err, result) => {
console.log(result); // 4
});

redisClient.pfcount(['visit:2020-01-02', 'visit:2020-01-01'], (err, result) => {
console.log(result); // user4가 공통으로 들어가있기 때문에 4
});

그 외 커맨드

pub/sub

publish(발송) / subscribe(구독)의 약자

메세지를 주고 받는 기능, 주의해야 할 점은 구독중인 경우 다른 redis의 명령어를 사용 불가

1
2
3
4
5
6
7
8
9
10
11
12
// publish.js

const redis = require('redis');

const redisClient = redis.createClient();

const channel = 'MyChannel';

redisClient.publish(channel, 'Hi!', (err, result) => {
if (err) console.log(err);
console.log(result);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// subscribe.js

const redis = require('redis');

const redisClient = redis.createClient();

const channel = 'MyChannel';

redisClient.subscribe(channel);

redisClient.on('message', (channel, message) => {
console.log('channel', channel, 'message', message);
});

// redisClient.get('qewqw', (e, d) => {
// console.log(e); // 구독중이므로 에러가 뜬다.
// });

subscribe.js를 실행을 하면 채널(MyChannel)을 구독 중이고

publish.js를 실행을 하게 되면 내가 작성한 메세지가 채널(MyChannel)에 보내게 되고

subscribe.js는 구독중이므로 publish.js에서 보낸 메세지를 계속 출력을 한다.

트랜잭션

redis에서 순차적 실행할 수 있는 커맨드를 지원을 한다.

그러나 중간에 에러가 나도 롤백이 안 되고 다음으로 넘어감…

1
2
3
4
5
6
7
8
9
10
const multi = redisClient.multi();

multi.set('tr', 1);
multi.setbit('bbb', 'bb', 2); // 에러 나는 부분
multi.incrby('tr', 2);

multi.exec((err, result) => {
if (err) console.log(err); // 에러가 찍힘
console.log(result);
});

그러나 실제로 들어 가서 보면 tr이 3이 되어 있음

watch

watch를 선언한 key를 트랜잭션을 쓸려고 했을 때 외부에서 값을 변경하게 되는 경우

트랜잭션을 실행하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
redisClient.watch('tt');

const multi = redisClient.multi();

multi.incrby('tt', 100);

redisClient.incrby('tt', 10); // multi를 실행 하기 전에 10을 올림

multi.exec((err, result) => {
console.log(result);
});

실제 tt의 value를 보면 10으로 되어 있다.

참고