상세 컨텐츠

본문 제목

[Redis] 기본 데이터 타입 정리

IT/프로그래밍

by James Lee. 2019. 3. 10. 21:29

본문

그동안 기존 프로젝트에 적용되어 있는 레디스를 "사용"만 해보다가 "공부"해보려고 한다.

공부의 구체적 목표는 레디스의 데이터 타입을 모두 파악하고, 레디스를 사용하는 어플리케이션의 성능을 개선하며, 레디스 안티 패턴(사용하면 안 되는 커맨드 들)을 이해하는 것이다.

이 포스팅은 에이콘 출판사의 Redis 핵심정리를 읽고 기본 데이터 타입인 문자열, 리스트, 해시를 정리한 내용이다.

Hello World

Hello world가 반이라고 했던가, 우선 간단하게 노드를 이용하여 Hello World 예제를 띄워본다.

var redis = require("redis");
var client = redis.createClient();

client.set("my_key", "Hello World using Node.js and Redis");
client.get("my_key", redis.print);

client.quit();

실행 결과

Reply: Hello World using Node.js and Redis

이제 레디스에서 사용하는 기본 데이터 타입에 대해 알아보자.

문자열

레디스에서 가장 다양한 데이터 타입

텍스트 문자열 외에도 정수(Integer) 또는 부동소수점(Float), 비트맵 값이 기반이고 연관 커맨드를 사용함으로써 동작한다.

문자열은 텍스트 (HTML, JSON, HTML 도 저장 가능), 숫자형(Integer, Float) 뿐 아니라 바이너리 데이터(이미지, 비디오, 오디오 파일)까지 저장 가능하다.

문자열 값은 512MB를 초과할 수 없다.

문자열의 사용 예시

  • 캐시 매커니즘 : 어떠한 타입의 데이터라도 캐시할 수 있다.
    • 커맨드 : SET, GET, MSET, MGET
  • 자동 만료되는 캐시 : 데이터베이스 질의 실행이 오래 걸리고, 일정 시간 동안 캐시되어야 할 때 매우 유용하다. 너무 자주 질의가 실행되는 것을 피할 수 있고, 애플리케이션 성능 향상에도 유용하다.
    • 커맨드 : SETEX, EXPIRE, EXPIREAT
  • 개수 계산 : 페이지 뷰, 비디오 뷰, 좋아요 같은 개수 계산이 좋은 예시다.
    • 커맨드 : INCR, INCRBY, DECR, DECRBY, INCRFLOATBY

문자열을 이용한 투표 시스템 만들기

var redis = require("redis");
var client = redis.createClient();

function upVote(id) {
    var key = "article:" + id + ":votes";
    client.incr(key);
}

function downVote(id) {
    var key = "article:" + id + ":votes";
    client.decr(key);
}

function showResults(id) {
    var headlineKey = "article:" + id + ":headline";
    var voteKey = "article:" + id + ":votes";

    //콜백을 전달할 수 있는 mget api 
    client.mget([headlineKey, voteKey], function (err, replies) {
        console.log("The article " + replies[0] + "has " + replies[1] + " votes");
    });
}


// 12345 아티클 3번 투표 (+3)
upVote(12345);
upVote(12345);
upVote(12345);

// 10001 아티클 3번 투표 (+2, -1)
upVote(10001);
upVote(10001);
downVote(10001);

//  60056 아티클 1번 투표 (+1)
upVote(60056);

//결과 보여주기
showResults(12345);
showResults(10001);
showResults(60056);

client.quit();

실행 결과

The article Google Wants to Turn Your Clothes Into a Computerhas 3 votes
The article For Millennials, the End of the TV Viewing Partyhas 1 votes
The article Alicia Vikander, Who Portrayed Denmark's Queen, Is Screen Royaltyhas 1 votes

리스트

리스트는 간단한 콜렉션, 스택, 큐와 같이 동작할 수 있기 때문에 레디스에서는 매우 유연한 데이터 타입이다.

많은 이벤트 시스템은 레디스의 리스트를 큐로 사용하는데, 리스트 커맨드가 원자적인 특성을 갖고 있어, 병렬 시스템이 큐에서 엘리먼트를 얻어낼 때 중복으로 얻지 않도록 보장해준다.

레디스의 리스트에는 블로킹 커맨드가 존재한다. 즉, 클라이언트가 비어있는 리스트에 블로킹 커맨드를 실행할 때, 클라이언트는 리스트에 새로운 엘리먼트가 추가될 때까지 기다린다는 의미다.

레디스의 리스트는 연결 리스트이다.

  • 처음 또는 끝에서의 접근/추가/삭제 O(1)
  • 처음과 끝이 아닌 경우는 O(N)

리스트의 각 엘리먼트가 list-max-ziplist-value(바이트 값) 설정 값보다 작고, 엘리먼트 개수가 list-max-ziplist-entries 설정 값보다 작으면, 리스트는 encode 될 수 있고, 메모리를 최적화 할 수 있다.

리스트를 사용하는 실제 예시

  • 이벤트 큐 : 레스큐, 셀러리, 로그스태시 등등의 툴에서 사용
  • 최근 사용자 글 저장하기 : 트위터는 사용자의 최근 트윗을 저장할 때 리스트를 사용

커맨드

  • LPUSH, RPUSH
  • LLEN
  • LINDEX
  • LRANGE
  • LPOP, RPOP
  • BRPOP, BLPOP
    • RPOP, LPOP의 블로킹 버전
  • RPOPLPUSH

참고 - Atomic

레디스는 항상 한 번에 하나의 커맨드를 실행하는 싱글 스레드 기반으로 동작한다. '원자적(Atomic)'이란 다중 클라이언트가 동시에 동일한 키를 작업하는 커맨드를 수행할 때, 경합 조건이 결코 발생하지 않는다는 것을 의미한다.

Queue를 이용한 생산자/소비자 시스템

consumer-worker.js

var redis = require("redis");
var client = redis.createClient();
var queue = require("./queue");
var logsQueue = new queue.Queue("logs", client);

function logMessages() {
    logsQueue.pop(function (err, replies) {
        var queueName = replies[0];
        var message = replies[1];

        console.log("[consumer] Got log: " + message);

        logsQueue.size(function (err,size) {
            console.log(size + " logs left");
        });

        logMessages();
    });
}

logMessages();

실행 결과

Created 5 logs

producer-worker.js

var redis = require("redis");
var client = redis.createClient();
var queue = require("./queue");
var logsQueue = new queue.Queue("logs", client);
var MAX = 5;

for (var i = 0; i< MAX; i++){
    logsQueue.push("Hello world #" + i);
}

console.log("Created " + MAX + " logs");

client.quit();

실행 결과

[consumer] Got log: Hello world #0
4 logs left
[consumer] Got log: Hello world #1
3 logs left
[consumer] Got log: Hello world #2
2 logs left
[consumer] Got log: Hello world #3
1 logs left
[consumer] Got log: Hello world #4
0 logs left

해시

해시는 필드를 값으로 매핑 할 수 있기 때문에 객체를 저장하기 적합한 구조

메모리를 효율적으로 사용하며, 데이터를 빨리 찾을 수 있게 최적화되어 있음

필드 이름과 값은 문자열

해시의 큰 장점은 메모리 최적화다.

아래는 인스타그램의 문자열과 해시 성능 벤치마크 사례이다.

인스타그램은 3억 건의 미디어 ID로 사용자 ID를 역참조를 해야 해서, 문자열과 해시를 이용한 레디스 프로토타입의 벤치마크 테스트를 진행하기로 결정했다.
문자열을 이용한 솔루션은 미디어 ID당 하나의 키를 사용하고, 약 21GB의 메모리를 사용했다.
해시를 이용한 솔루션은 일부 설정을 수정해 약 5GB의 메모리를 사용했다.

21GB → 5GB

주의 : HGETALL

해시에 많은 필드가 존재하고 메모리를 많이 사용한다면, HGETALL 커맨드가 문제를 일으킬 수 있다. HGETALL 커맨드는 모든 해시 데이터를 네트워크를 통해 전달해야 할 필요가 있기 때문에 레디스를 느리게 할 수 있다.
이러한 경우에는 한 번에 모든 필드를 리턴하지 않고, 커서와 해시 필드의 값을 리턴하는 HSCAN 커맨드가 대안이 될 수 있다.

Hash를 이용한 투표 시스템

var redis = require("redis");
var client = redis.createClient();

function saveLink(id, author, title, link) {
    client.hmset("link:" + id, "author", author, "title", title, "link", link, "score",0);
}

function upVote(id) {
    client.hincrby("link:" + id, "score", 1);
}

function downVote(id) {
    client.hincrby("link:" + id, "score", -1);
}

function showDetails(id) {
    client.hgetall("link:" + id, function (err, replies) {
        console.log("Title:", replies["title"]);
        console.log("Author:", replies["author"]);
        console.log("Link:", replies["link"]);
        console.log("Score:", replies["score"]);
    });
}


saveLink(123, "dayvson", "Maxwell Dayvson's Github page", "http://github.com/dayvson");

upVote(123);
upVote(123);

saveLink(456, "hltbra", "Hugo Tavare's Github Page", "https://github.com/hltbra");
upVote(456);
upVote(456);
downVote(456);

showDetails(123);
showDetails(456);

client.quit();

실행 결과

Title: Maxwell Dayvson's Github page
Author: dayvson
Link: http://github.com/dayvson
Score: 2
Title: Hugo Tavare's Github Page
Author: hltbra
Link: https://github.com/hltbra
Score: 1

그 외

keys : 패턴과 일치하는 저장된 모든 키 리턴

keys p*
> "philosoper"


관련글 더보기

댓글 영역