Programming

[Redis] 레디스 데이터 타입 정리

이 포스팅은 레디스 공식 문서를 보고 지식을 정리하기 위해 쓴 글입니다.

Strings

Redis String 유형은 Redis 키와 연결할 수 있는 가장 간단한 유형의 값이다. Memcached에서의 유일한 데이터 타입이자, Redis에서도 자연스러운 값이다.

기본적으로 SET, GET을 이용해 문자열 값을 설정하고 검색할 수 있다.

> set mykey somevalue
OK
> get mykey
"somevalue"

SET는 키가 문자열이 아닌 값과 연결되어 있더라도 키가 이미 존재하는 경우 키에 이미 저장된 기존 값을 대체한다.

SET 명령에는 추가 인수로 제공되는 여러 옵션이 있다. 예를 들어, 키가 이미 존재하는 경우 실패하도록 요청하거나, 키가 이미 존재하는 경우에만 성공하도록 요청할 수 있다.

> set mykey newval nx # 키가 이미 존재하는 경우 실패한다.
(nil)
> set mykey newval xx # 키가 이미 존재하는 경우에만 성공한다.
OK

비록 문자열들이 레디스의 기본값일지라도, 그것들로 수행할 수 있는 흥미로운 연산들이 있다. 예를 들어, 하나는 atomic increment이다.

> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

INCR 명령은 1)문자열 값을 정수로 구문 분석하고, 2)1씩 증분한 다음, 3)마지막으로 얻은 값을 새 값으로 설정한다. 비슷한 명령어로는 INCRBY, DECR and DECRBY가 있다.

INCR 명령은 atomic하다. 즉, 여러 클라이언트가 동시에 같은 키에 INCR 명령어를 입력한다고 해서 경쟁 상태에 들어가지 않는다.

 

또다른 명령어로는 GETSET이 있다. GETSET 명령은 키를 새 값으로 설정하고 결과로 이전 값을 반환한다. 웹 사이트가 새 방문자를 받을 때마다 INCR을 사용하여 Redis 키를 증가시키는 시스템이 있는 경우 이 명령을 사용할 수 있다. 만약 한시간마다 새 방문자를 측정하려는 상황이라면, GETSET 명령어로 “0”을 설정하고 매번 값을 받아올 수 있다.

> get counter
"152"
> GETSET counter 160
"152"
> get counter
"160"

한번에 여러 값을 설정하고 가져오는 기능도 있다. MSET과 MGET을 이용하면 된다.

> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

Altering and querying the key space

특정 타입에 속하지 않는 명령어도 있다.

EXISTS 명령은 지정된 키가 데이터베이스에 있는지 여부에 대한 결과값으로 1 또는 0을 반환한다. DEL 명령은 값과 상관없이 키 및 관련 값을 삭제한다.

> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

DEL 명령어를 통해 key가 제거되었는지의 여부도 확인할 수 있다.(1: 제거됨, 0:제거되지않음;값이 원래 없었음)

TYPE 명령어는 키의 타입을 알려준다.

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none

Key expiration

Key expiration을 통해 키의 유효시간을 설정할 수 있다. 유효 시간(TTL, Time To Live)이 지나면 키는 자동으로 파기된다.

이 기능의 특징은 다음과 같다.

  • 초 또는 밀리초 단위로 설정할 수 있다.
  • 만료시간 단위는 항상 1 밀리초이다.
  • expiration에 대한 정보는 항상 디스크에 복제되고 저장된다. 만료시간을 특정 날짜로 저장하기때문에 레디스 서버가 중지되더라도 시간은 계속 흐른다.
> set key some-value
OK
> expire key 5 # 5초로 TTL을 설정한다.
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)

EXPIRE 이외에도 키 생성과 함께 만료시간을 설정할 수도 있다.

> set key 100 ex 10 # 10초로 TTL을 설정한다.
OK
> ttl key
(integer) 9

PEXPIRE와 PTTL 명령어로 밀리초 단위로 만료시간을 설정하고 확인할 수 있다.

  • EXPIRE : 초 단위
  • PEXPIRE : 밀리초 단위

List

레디스의 list는 Linked list이다. 이말은 곧 LPUSH 명령어로 리스트의 맨앞에 10개를 추가하는 속도와 1000개를 추가하는 속도가 같다는 의미이다. 대신, 인덱스를 통해 탐색할 때는 ArrayList 보다 느리다는 단점이 존재한다.

 

데이터베이스에서 긴 리스트에 요소를 추가하는 것은 매우 중요하기에 레디스의 List는 LinkedList로 구현되었다.

만약 많은 요소들 중 중간 요소에 빠르게 접근하는 것이 중요하다면 List보다는 Sorted Sets 자료형을 사용하는 것이 더 좋다.

First steps with Redis Lists

LPUSH 명령어는 리스트의 왼쪽(맨앞)에, RPUSH 명령어는 리스트의 오른쪽(맨뒤)에 요소를 삽입한다. LRANGE 명령어는 리스트의 요소를 추출한다.

> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1 # 첫번째(0)부터 마지막번째 요소(-1)까지 출력하라.
1) "first"
2) "A"
3) "B"

LRANGE 명령어는 2개의 인덱스를 갖고 있는데 시작 인덱스와 끝 인덱스를 의미한다. -1은 마지막, -2는 끝에서 2번째를 의미한다.

한번에 여러 요소를 삽입할 수도 있다.

> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"

RPOP, LPOP은 요소를 pop하는 기능이다. 즉, 요소를 출력함과 동시 제거하는 기능이다.

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"

Common use cases for lists

List 자료형은 아래 2가지 케이스에서 유용하게 사용할 수 있다.

  • 사용자가 SNS에 가장 최근에 올린 포스트를 기억해야 할 때
  • 생산자 - 소비자 패턴(생산자는 작업을 넣고, 소비자는 작업을 처리하는 구조)에서 유용하게 사용할 수 있다. 레디스는 이를 위한 List 명령어를 제공한다.

트위터에서 가장 최근 트윗을 가져올 때 레디스를 이용한다. 참고

사용자가 사진을 업로드하면 photo_id를 LPUSH를 통해 리스트에 넣고, 여러 사용자들이 메인페이지에 접속할 때마다 LRANGE 0 9 명령어를 통해 최근 10개의 트윗을 가져온다.

Capped lists

SNS 업데이트, 로그을 포함해 어떤 데이터든지 최신 데이터를 저장하기 위해 List를 사용한다. 레디스의 LTRIM 명령어를 사용하면 최신 N개의 항목만 기억하고 가장 오래된 항목은 모두 폐기하면서 목록을 제한된 컬렉션으로 사용할 수 있다.

 

LTRIM 명령어는 LRANGE 명령어와 비슷하나, 특정 범위의 요소를 보여주는 대신 특정 범위의 요소만을 제외한 모든 요소를 제거한다는 특징이 있다.

> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2 # 0번쨰 인덱스부터 2번째 인덱스까지를 제외한 모든 요소를 제거한다.
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

이를 통해 매우 간단하지만 유용한 패턴이 가능해진다. 즉, List push 작업 + List trim 작업을 함께 수행하여 새 요소를 추가하고 특정 범위를 넘어가는 요소를 제거하는 방식이다.

LPUSH mylist <some element>
LTRIM mylist 0 999

위의 조합은 새 요소를 추가하고 1000개의 최신 요소만 목록으로 가져온다. 여기에 LRANGE를 사용하면 오래된 데이터를 기억할 필요 없이 상위 항목의 데이터만을 가져올 수 있다. LRANGE 명령어의 시간복잡도가 O(N)이기에 제한된 데이터만을 가져오는 것은 성능상 매우 중요하다. (LinkedList의 시간복잡도 참고.)

Blocking operations on lists

List는 큐를 구현하기에 적합한 기능을 갖고 있다.

한 프로세스는 요소를 push하고, 또다른 프로세스는 요소를 사용하는 상황이라 가정해보자. 이렇게 일반적인 생산자 - 소비자 패턴에서 다음 방식으로 이를 구현할 수 있다.

  • 요소를 삽입하기 위해 생산자는 LPUSH 명령어를 호출한다.
  • 요소를 사용하기 위해 소바자는 RPOP 명령어를 호출한다.

때때로 리스트가 비어있어 RPOP 명령어가 null을 반환할 수 있다. 이 경우 소비자는 잠시 기다렸다가 RPOP을 사용하여 다시 시도해야 한다. 이것을 폴링(polling)이라 부르는데, 몇가지 단점 때문에 좋은 선택지는 아니다.

  1. 레디스와 클라이언트가 불필요한 명령어를 처리하도록 강제한다.(즉, 계속 명령어를 호출하고 null을 반환하는 것 자체가 양측에게 부담이 된다.)
  2. 소비자가 null을 받고 난 뒤, 일정시간 대기하므로 지연시간이 발생한다. 지연시간을 줄이기 위해서는 자주 RPOP 명령어를 호출해야 하는데, 이건 1번문제를 증폭시키는 효과를 낳는다.

그래서 Redis는 목록이 비어 있을 경우 블로킹할 수 있는 BRPOP 및 BLPOP이라는 명령어를 제공한다. 새 요소가 List에 추가되거나 지정한 시간이 다 됐을때만, 호출한 클라이언트에게 결과값이 돌아간다.

> brpop tasks 5 # 새 요소가 추가될 때까지 5초간 기다린 후, 사용할 요소가 없으면 null을 반환한다.
1) "tasks"
2) "do_something"

0을 timeout으로 설정하면 시간제한 없이 영원히 기다릴 수 있다. 또한, 여러 List에서 동시에 대기하고 그중 첫 번째 요소가 추가되면 반환값을 받도록 설정할 수도 있다.

 

BRPOP은 다음 특징들을 갖고 있다.

  • 클라이언트는 순서대로 제공된다. 대기하고 있는 첫 번째 클라이언트는 다른 클라이언트에 의해 요소가 푸시될 때 먼저 제공된다.
  • RPOP과 비교했을 때 반환값이 다르다. BRPOP과 BLPOP은 여러 목록에서 요소를 기다리는 것을 차단할 수 있기 때문에, 키의 이름과 값을 포함하는 2요소 배열이다.
  • 시간초과되면 null을 반환한다.

블로킹에 대해 좀 더 알아보려면 다음 키워드를 공부하면 좋다.

  • LMOVE를 사용하여 더 안전한 대기열 또는 회전 대기열을 구축할 수 있다.
  • BLMOVE라고 불리는 명령어도 있다.

Automatic creation and removal of keys

지금까지는 빈 List를 만들거나 삭제하지 않았다. List가 비어있을때 이 List를 삭제하거나, 요소를 추가하기 전에 새로운 List를 만드는 것은 레디스의 역할이었기 떄문이다. 이러한 특성은 List를 포함한 모든 레디스 데이터 타입에 적용된다.

Hashes

레디스의 Hash 자료형은 자바의 HashMap과 동일하게 작동한다.

> hset user:1000 username antirez birthyear 1977 verified 1
(integer) 3
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"

HSET 명령어가 한번에 여러 필드를 설정할 수 있는 것에 반해, HGET 명령어는 한번에 하나의 필드값만을 가져올 수 있다. HMGET는 HGET와 유사하지만 다음과 같은 값의 배열을 반환한다.

> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)

HINCRBY와 같이 단일 필드에서도 작업을 수행할 수 있는 명령도 있다.

> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997

hash 자료형에 대한 명령어는 여기에서 찾아볼 수 있다.

Sets

레디스의 Set 자료형은 정렬되지 않은 문자열 집합을 의미한다. SADD 명령어는 set에 새로운 요소를 삽입한다. 뿐만 아니라 주어진 요소가 이미 존재하는 경우, 여러 집합 간의 교집합, 합집합 또는 차이 등과 같은 집합에 대한 여러 다른 연산을 수행할 수도 있다.

> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

위의 결과값을 보면 요소를 삽입한 순서대로 반환되지 않았음을 확인할 수 있다. 기본적으로 레디스의 Set은 값을 자유롭게 반환한다.

SISMEMBER 명령어를 통해 이미 요소가 존재하는지 확인할 수 있다.

> sismember myset 3 # Set에 값이 존재하는 경우
(integer) 1
> sismember myset 30 # Set에 값이 존재하지 않는 경우
(integer) 0

집합은 객체 간의 관계를 표현하는 데 유용하다. 예를 들어, 태그를 구현하기 위해 Set을 쉽게 사용할 수 있다.

이 문제를 모델링하는 간단한 방법은 태그를 지정할 모든 개체에 대해 집합을 갖는 것이다. 집합에는 개체와 연결된 태그의 ID가 포함된다.

 

하나의 예로 뉴스 기사에 태그를 다는 상황을 들 수 있다. 기사 ID 1000에 태그 1, 2, 5 및 77이 지정된 경우 집합은 다음 태그 ID를 뉴스 항목과 연결할 수 있다.

> sadd news:1000:tags 1 2 5 77
(integer) 4

또는, 반대로 주어진 태그에 뉴스기사를 연결할 수도 있다.

> sadd tag:1:news 1000
(integer) 1
> sadd tag:2:news 1000
(integer) 1
> sadd tag:5:news 1000
(integer) 1
> sadd tag:77:news 1000
(integer) 1

SMEMBERS 명령어로 뉴스 기사에 달린 모든 태그를 확인할 수 있다.

> smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2

SINTER 명령어로 각 태그에 달린 뉴스기사의 교집합을 찾을 수도 있다.

> sinter tag:1:news tag:2:news tag:5:news tag:7:news
1) "1000"

교집합 외에도 합집합, 차집합, 랜덤 요소 추출 등을 수행할 수 있다.

예를 들어, 52장의 포커 카드 중 플레이어들에게 각 5장씩 랜덤으로 패를 나눠준다고 할 때 SPOP 명령어를 이용할 수 있다. 그러나 이 경우 매번 나눠준 패만큼의 카드를 덱에 채워넣어야 하기 때문에 적절한 명령어가 아니다.

 

SUNIONSTORE 명령어를 통해 집합을 복사할 수 있다.

> sunionstore game:1:deck deck
(integer) 52

드디어 첫 번째 선수에게 5장의 카드를 제공할 준비가 되었다.

> spop game:1:deck
"C6"
> spop game:1:deck
"CQ"
> spop game:1:deck
"D1"
> spop game:1:deck
"CJ"
> spop game:1:deck
"SJ"

SCARD 명령어로 덱에 남아 있는 카드의 개수를 확인할 수 있다.

scard game:1:deck
(integer) 47

Set에서 요소를 제거하지 않고 랜덤 요소만 가져와야 할 경우, 작업에 적합한 SRAND MEMBER 명령어도 있다. 또한, 반복 요소와 반복되지 않는 요소를 모두 반환할 수 있는 기능도 있다.

Sorted sets

Sorted set은 정렬된 집합을 말한다. sorted set 내 모든 요소는 부동소수점이라 불리우는 score와 연관된다. (모든 요소가 값에 매핑되기 때문에 Hash와 유사하다 말할 수 있다.)

Sorted set은 스코어를 기준으로 정렬된다. 만약 스코어가 같다면 사전순서대로 정렬된다.

> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1

ZADD 명령어는 SADD와 유사하지만 추가할 요소 앞에 스코어를 사용한다. 또한, ZADD는 여러 개의 Score-Key 쌍을 자유롭게 지정할 수 있다.

 

ZRANGE 명령어로 정렬된 값을 확인할 수 있다.

> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"

만약 역순으로 정렬하고 싶다면 ZREVRANGE 명령어를 사용하면 된다.

> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"

WITSCORES 명령어를 사용해 스코어를 반환할 수도 있다.

> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"

Operating on ranges

Sorted set은 범위 내에서 동작할 수 있다. 만약 1950년대 이전에 태어난 모든 사람을 조회한다면 ZRANGEBYSCORE 명령어를 사용하면 된다.

> zrangebyscore hackers -inf 1950 # score가 1950 이하인 모든 Key 조회
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"

요소의 범위를 제거할 수도 있다. ZREMRANGEBYSCORE 명령어로 1940년에서 1960년 사이에 태어난 모든 사람을 분류된 집합에서 제거해보자.

> zremrangebyscore hackers 1940 1960
(integer) 4

> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Yukihiro Matsumoto"
5) "Linus Torvalds"

또 다른 매우 유용한 명령어는 get-rank이다. 정렬된 집합에서 요소의 위치가 무엇인지 물어볼 수 있다.

> zrank hackers "Hedy Lamarr"
(integer) 1

ZREVRANK 명령어를 통해 뒤에서 몇 등인지 알 수도 있다.

> ZREVRANK hackers "Alan Turing"
(integer) 4