[Redis] Redis 학습 및 데이터 타입 실습1

2022. 2. 26. 21:59TIL💡/Database

Redis는 가장 인기 있는 인메모리(In-memory) Key-Value 저장소이다.

Redis 기초 설명

String, Hash, List, Set, Sorted Set, Bitmap, HyperLogLog와 같은 강력한 데이터 타입 때문에 데이터 구조 서버라고도 불린다.

 

Redis는 기본적으로 메모리에 저장하기 때문에 읽기와 쓰기 명령이 매우 빠르다.

Redis는 디스크에도 데이터를 저장할 수 있다.

 

방법

  • Snapshot - 저장된 데이터를 바이너리 스냅샷으로 생성
  • Journaling - 시간에 걸쳐 실행된 모든 커맨드를 순서대로 저장해 사람이 읽을 수 있는 파일로 생성

이를 통해 데이터의 영속성을 달성할 수 있다.

 

추가적으로 Redis는 Key Expiration, Transaction, Publish/Subscribe 기능을 설정할 수 있다.

또한 Redis가 새로운 커맨드를 추가 생성할 수 있도록 Lua 스크립트 기능을 제공한다.

 

Redis = REmote DIctionary Server

 

Hello Redis(CLI 예제)

redis-server

레디스의 실제 데이터 저장소

클러스터 모드 또는 독립실행형(standalone) 모드로 실행 가능

redis-cli

모든 레디스 커맨드를 실행할 수 있는 CLI

 

기본적으로 레디스는 6379 포트를 바인드하고, 독립실행형 모드로 실행한다.

커맨드

  • SET
  • GET
  • HELP: 레디스 커맨드의 문법을 배우기에 유용
  • KEYS: 패턴과 일치하는 저장된 모든 키를 리턴 ex) KEYS p*

책에는 node-redis v3 예시로 나와있어서 node-redis의 v4의 Github을 직접 보면서 시도하였습니다. 이는 위의 커맨드를 node 클라이언트로 실행한 것입니다.

import { createClient } from 'redis';

(async () => {
  const client = createClient();

  client.on('error', (err) => console.log('Redis Client Error', err));
  client.connect();
  await client.set('key', '은지');
  const value = await client.get('key');
  console.log(value);
  client.quit();
})();

 

레디스 데이터 타입

💚 문자열

- 문자열은 많은 커맨드를 가지며 여러 목적으로 사용되기 때문에 레디스에서 가장 다양한 데이터 타입

- 정수(Integer), 부동소수점(Float), 텍스트 문자열, 비트맵 값이 기반이고 연관 커맨들를 사용함으로써 동작

- 문자열 값은 텍스트 또는 바이너리 데이터의 512MB를 초과 불가능

 

사용 예시

캐시 매커니즘

HTML페이지와 API 응답에서 이미지, 비디오까지 어떠한 텍스트 또는 바이너리 데이터라도 캐시 가능

SET, GET, MSET, MGET, 커맨드를 이용해 간단한 캐시 시스템 구현 가능

 

자동 만료되는 캐시

키의 만료를 자동으로 지원하는 문자열은 SETEX, EXPIRE, EXPIREAT 커맨드를 이용해 튼튼한 캐시 시스템을 만들 수 있다. 데이터베이스 질의 실행이 오래 걸리고, 일정 시간 동안 캐시돼야 할 때 매우 유용하다.

따라서 너무 자주 질의가 실행되는 것을 피할 수 있고, 애플리케이션 성능 향상에도 도움을 준다.

 

개수 계산(count)

문자열과 INCR, INCRBY 커맨드를 이용해 쉽게 구현 가능한 개수 계산이다. 페이지 뷰, 비디오 뷰, 좋아요 같은 개수 계산이 좋은 예시이다.

문자열은 DECR, DECRBY, INCRFLOATBY와 같은 개수 계산 커맨드도 제공한다.

 

redis-cli를 활용한 문자열 예제

 

MSET 커맨드는 한 번에 다중  키의 값을 저장한다.

MGET 커맨드는 여러 개의 키값을 얻을 수 있고 키 이름은 공백으로 구분된다.

EXPIRE 커맨드는 주어진 키에 대한 만료 시간(초 단위)을 추가한다.

만료 시간이 지나면 키는 자동으로 레디스에서 지워진다.

키 만료 커맨드가 성공적으로 설정되며 1을 리턴하고, 키가 존재하지 않거나 설정할 수 없다면 0을 리턴한다.

 

TTL(Time To Live) 커맨드는 다음 중 하나로 리턴한다.

- 양의 정수: 주어진 키가 얼마나 생존할 수 있는지 초로 보여준다.

- -2: 키가 만료되거나 존재하지 않는 경우

- -1: 키가 존재하지만 만료 시간을 저장하지 않은 경우

INCR, INCRBY 커맨드는 매우 유사한 기능을 가진다.

- INCR 커맨드는 하나씩 키값을 증가

- INCRBY 커맨드는 주어진 숫자만큼 키 값을 증가시키고 증가된 값을 리턴

DECR, DECRBY 커맨드는 키 값을 감소시킨다는 것이 두 커맨드 간의 유일한 차이점이다.

 

INCRBYFLOAT 커맨드는 부동소수점을 받아 키 값을 증가시킨 후, 새롭게 변경된 값을 리턴

INCRBY, DECRBY, INCRBYFLOAT 커맨드는 양수 또는 음수를 받는다.

 

지금까지 언급한 커맨드는 원자적(atomic) 커맨드로서, 키-값을 증가시키거나 감소시키고 하나의 명령으로 새 값을 리턴하는 커맨드

따라서 서로 다른 두 개의 클라이언트가 동일한 커맨드를 동시에 실행해도 동일한 값을 얻을 수 없다.

 커맨드 간에 어떠한 경합 조건(race condition)도 존재하지 않기 때문이다.

 

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

노드를 이용해  문자열로 투표 시스템 개발하기

좋아요, 싫어요를 투표할 수 있는 노드 기능을 구현한다.

import { createClient } from 'redis';

const client = createClient();

const upVote = async (id) => {
  var key = "article:" + id + ":votes";
  await client.incr(key);
}

const downVote = async (id) => {
  var key = "article:" + id + ":votes";
  await client.decr(key);
}

const showResults = async (id) => {
  var headlineKey = "article:" + id + ":headline";
  var voteKey = "article:" + id + ":votes";
  var headValue = await client.get(headlineKey);
  var voteValue = await client.get(voteKey);
  console.log("The article " + headValue + " has " + voteValue);
}

(async () => {
  
  client.on('error', (err) => console.log('Redis Client Error', err));
  client.connect();
  await upVote(12345);
  await upVote(12345);
  await upVote(12345);
  await upVote(10001);
  await upVote(10001);
  await downVote(10001);
  await upVote(60056);

  await showResults(12345);
  await showResults(10001);
  await showResults(60056);

  client.quit();
})();

그런데 책에 나와있는대로 mget이 작동하지 않아서 결국 일일이 get을 수행하였다.

그리고 반드시 비동기 작업을 잊지 않아야 한다.

 

💚리스트 

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

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

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

- 레디스의 리스트는 연결리스트(Linked List)라서 리스트의 처음 또는 끝에서의 엘리먼트의 추가 및 삭제는 항상 O(1)이다. 리스트에서 엘리먼트에 접근하는 작업은 O(N)이지만 첫 번째 또는 마지막 엘리먼트에는 항상 일정 시간으로 접근한다.

 

사용 예시

✨ 이벤트 큐

리스트는 Resque, Celery, Logstash를 포함한 많은 툴에서 사용

 

✨ 최근 사용자 글 저장하기

트위터는 사용자의 최근 트윗을 저장할 때 리스트를 사용

 

redis-cli 실습

레디스의 리스트는 연결 리스트기 때문에, 리스트의 처음과 끝에 데이터를 추가할 수 있는 커맨드가 존재한다.

LPUSH 커맨드는 리스트의 처음에 데이터를 추가하고, RPUSH 커맨드는 리스트의 끝에 데이터를 추가한다.

LLEN 커맨드는 리스트의 길이를 리턴

LINDEX 커맨드는 주어진 인덱스의 엘리먼트를 리턴

리스트의 엘리먼트는 항상 왼쪽에서 오른쪽으로 접근

→ 인덱스 0은 첫 번째 엘리먼트를, 인덱스 1은 두 번째 엘리먼트를 가리키며, 나머지는 그 뒤의 값으로 접근한다.

리스트의 끝 부분에 접근하기 위해 음수 인덱스를 사용할 수 있다.

-1은 마지막 엘리먼트를, -2는 끝에서 두 번째 엘리먼트를 가리키며, 다른 엘리먼트도 이렇게 음수로 접근할 수 있다.

LRANGE 커맨드는 시작과 끝 인덱스를 포함시켜 주어진 인덱스 범위에 있는 모든 엘리먼트 값을 배열로 리턴한다.

LPOP 커맨드는 리스트의 첫 번째 엘리먼트를 삭제하고 리턴한다.

RPOP 커맨드는 리스트의 마지막 엘리먼트를 삭제하고 리턴한다.

BRPOP 커맨드는 RPOP의 블로킹 버전으로, 레디스 리스트의 마지막 엘리먼트를 삭제한다.

리스트가 비어있으면 제거될 엘리먼트가 들어올 때까지 기다린다. 리스트가 비어있으면 큐에 엘리먼트가 추가되자마자 엘리먼트 처리를 확실하게 할 수 있는 polling 같은 것을 구현해야하기 때문에 RPOP은 적합하지 않다.

리스트가 비어있더라도 BRPOP의 장점을 잘 활용한다면 걱정할 필요는 없다.

 

하지만 여전히 신뢰성 문제가 발생한다. 왜냐하면 실패가 발생할 때 추적하거나 재시도할 수 있는 방법이 없다.

이런 신뢰성 문제를 해결할 수 있는 좋은 방법은 큐를 추가로 사용하는 것이다.

큐에서 얻은 각 엘리먼트를 추가된 큐에 넣는다.

제대로 작동한다면, 추가된 큐에서 엘리먼트를 제거한다.

재시도하거나 실패 알림을 생성하기 위해 막힌 엘리먼트를 위한 추가 큐를 모니터링할 수 있다.

RPOPPUSH 커맨드는 이런 상황에 매우 적합하다.

RPOPPUSH 커맨드는 큐에서 RPOP 커맨드를 실행하고, 추가된 큐에서 LPUSH 커맨드를 실행한 후, 마지막으로 엘리먼트를 리턴한다.

RPOPPUSH 커맨드는 이 모든 작업을 한 번에 실행하는 원자적 커맨드다.

 

💚 해시

해시는 필드를 값으로 매핑할 수 있기 때문에, 객체를 저장하는 데 훌륭한 데이터 구조다.

해시는 메모리를 효율적으로 쓸 수 있고, 데이터를 빨리 찾을 수 있게 최적화돼 있다.

해시에서 필드 이름과 값은 문자열이다. → 따라서 해시는 문자열을 문자열로 매핑한다.

 

앞선 문자열 예제에서, 기사 헤드라인과 투표를 보여주는 두 개의 분리된 키를 사용했다. 이런 경우네느 두 개의 필드가 동일한 객체 속하기 때문에 해시를 사용하는 것이 훨씬 의미가 있다.

 

그 밖의 해시의 큰 장점은 메모리 최적화다. 최적화는 hash-max-ziplist-entries와 hash-max-ziplist-value 설정을 기반으로 한다.

해시는 내부적으로 ziplist 또는 hash table이 될 수 있다.

 

📌 Ziplist

- 메모리 효율화에 목적을 둔 양쪽으로 연결된 리스트

- 정수를 일련의 문자로 저장하지 않고 실제 정수의 값으로 저장

- 메모리 최적화돼 있다 할지라도 일정한 시간 내로 검색이 수행되지는 않는다.

 

📌 Hash Table

- 일정한 시간 내로 검색은 되지만 메모리 최적화가 이루어지지 않는다.

 

인스타그램은 3억 건의 미디어 ID로 사용자 ID를 역참조를 해야 해서, 문자열과 해시를 이용한 레디스 프로토타입의 벤치마크 테스트를 진행하기로 결정했다.

문자열을 이용한 솔루션은 미디어 ID당 하나의 키를 사용하고, 약 21GB의 메모리를 사용했다.
해시를 이용한 솔루션은 일부 설정을 수저해 약 5GB를 사용했다.

참고

https://instagram-engineering.com/storing-hundreds-of-millions-of-simple-key-value-pairs-in-redis-1091ae80f74c

 

Storing hundreds of millions of simple key-value pairs in Redis

When transitioning systems, sometimes you have to build a little scaffolding. At Instagram, we recently had to do just that: for legacy…

instagram-engineering.com

redis-cli 실습

HSET 커맨드는 주어진 키의 필드에 값을 저장한다. 문법은 HSET key field value다.

HMSET 커맨드는 공백으로 구분된 다중 필드 값을 키에 저장한다.

HSETHMSET은 필드가 존재하지 않으면 필드를 생성하고, 필드가 이미 존재한다면 필드의 값을 덮어쓴다.

 

HINCRBY 커맨드는 주어진 정수만큼 필드를 증가시킨다.

HINCRBYFLOAT INCRBY INCRBYFLOAT와 비슷한다.

 

HGET 커맨드는 해시에서 필드를 읽는다.

HMGET 커맨드는 한 번에 다중 필드를 읽는다.

HDEL 커맨드는 해시에서 필드를 삭제한다.

 

HGETALL 커맨드는 해시에서 모든 필드/값의 쌍으로 이루어진 배열을 리턴

HKEYS와 HVALS 커맨드로 각각 해시의 필드 이름 또는 필드 값만 얻을 수 있다.

 

해시에 많은 필드가 존재하고 메모리를 많이 사용한다면 HGETALL 커맨드가 문제를 일으킬 수 있다.
HGETALL 커맨드는 모든 해시 데이터를 네트워크를 통해 전달해야할 필요가 있기 때문이다.
이러한 경우에는 HSCAN 커맨드가 좋은 대안이 될 수 있다.

HSCAN은 한 번에 모든 필드를 리턴하지 않는다. 커서와 해시 필드의 값을 한 번에 리턴한다.
해시에서 모든 필드를 얻으려면 리턴된 커서의 값이 0이 될때까지 HSCAN 커맨드를 실행해야 한다.

 

 

'TIL💡 > Database' 카테고리의 다른 글

[Redis] 다양한 커맨드와 기능 소개  (0) 2022.02.27
[Redis] 데이터타입 실습2  (0) 2022.02.27
[SQL Server] Transaction  (0) 2022.02.12
[SQL Server] Lock  (0) 2022.02.08
SQL Server로 Index 실습  (0) 2022.01.30