메뉴

문서정보

목차

Redis 데이터 스트럭처

MySQL같은 RDBMS는 데이터 스트럭처라는 개념이 없다. 데이터베이스를 이루는 여러 개의 테이블이 있으며, 이 테이블을 (join등으로 엮어서) 필요한 데이터를 추출할 뿐이다. RDBMS를 사용하는 서비스에서 데이터 스트럭처는 "애플리케이션 영역"에서 처리 한다.

반면 Redis는 List, Set, Sorted set, Hash, HyperLogLogs와 같은 애플리케이션 영역에서나 다룰만한 데이터 스트럭처를 제공한다. 애플리케이션에서 해야 할 일들을 Redis에 맡길 수 있음을 의미한다. Redis에서 key는 string이고, 데이터 스트럭처는 value에 저장한다. 아래 Redis가 지원하는 데이터 스트럭처를 정리했다. 이들 모든 데이터 스트럭처를 다룰 것이다. 이들 자료구조 외에 GeoHash, PUB/SUB과 같은 기능을 제공하는데, 데이트 스트럭처라고 보기는 힘들어서 따로 다루려 한다.

자료구조외에 앞으로 자주 사용할 기본적인 관리 명령 몇 개도 살펴보겠다.

Redis Strings

Redis의 가장 기본이 되는 데이터스트럭처다. 기본적으로 Redis의 String은 연속된 바이트의 조합으로 python이나 ruby언어에서의 string이 아닌 C/C++ 언어에서의 char 배열과 비슷하다. 따라서 String 뿐만 아니라 바이트로 표현할 수 있는 정보, 예를 들어 JPEG 이미지나 serialized된 루비 객체들도 저장 할 수 있다. String value의 최대 크기는 512M 이다.

바이트를 저장하는 만큼 일반적인 string 뿐만 아니라 int나 float형도 저장을 할 수 있다. 명령도 다양해서 string과 관련해서 무려 24개의 명령을 제공한다. string 관련 지원 명령의 목록은 redis.io에서 확인 할 수 있다. 여기에서는 핵심적인 몇 개 명령어들을 살펴볼 것이다.

SET & GET

SET 명령어로 key에 string 값을 설정 할 수 있다. key에 문자열을 저장하는 느낌으로 가장 쉽게 사용 할 수 있는 명령이다. user:info:yundream key에 yundream@gmail.com 을 저장해 보자.
SET user:info:yundream yundream@gmail.com
GET 명령으로 key의 값을 읽을 수 있다.
GET user:info:yundream
"yundream@gmail.com"
Key는 데이터베이스에서 유일해야 한다. key가 이미 존재할 경우 value를 업데이트 한다.
SET user:info:yundream yundream@naver.com
OK

GET user:info:yundream
"yundream@naver.com"
이외에 몇 개의 유용한 옵션들이 있다. 이미 key가 있을 경우 value를 업데이트 한다고 했는데, 무시하고 싶을 때가 있다. NX 옵션을 설정하고 명령을 내리면, 동일한 key가 있을 때 value를 업데이트하지 않는다.
SET user:info:yundream yundream@yahoo.com NX
(nil)

EX와 PX는 만료시간이 있는 세션(session)유지에 유용하게 사용 할 수 있다. 유저가 사이트에 방문하면 세션을 만드는데, 적당한 시간에 세션을 정리해줘야 한다. EX와 PX 옵션을 이용해서 일정 시간 후에 key가 삭제되도록 할 수 있다. 60초의 만료시간을 설정했다.
SET user:info:yundream yundream@yahoo.com EX 60
OK
TTL key 명령으로 key의 남은 만료시간을 확인 할 수 있다.
TTL user:info:yundream 
(integer) 52

String 명령어 리스트

Redis는 각 데이터 스트럭처를 관리하기 위한 명령어들을 제공한다. Redis는 string 관련 20개가 넘는 명령어들을 제공한다. 명령어들 모두를 정리했다. 이 중 자주 사용되는 명령어들만 자세히 설명하겠다.
명령 사용법 설명
DECR key 값을 1씩 감소
DECRBY key decrement decrement 만큼 감소
DEL key [key...] key를 삭제
GET key key의 값을 조회
GETSET key value key를 조회하고 새로운 값을 저장
INCR key 값을 1씩 증가
INCRBY key increment increment만큼 값을 증가
MGET key [key...] 여러 key를 한번에 조회
SET key value key에 값을 저장
SETNX key value key가 없을 경우 값을 저장
MSET key value [key value...] 여러 key를 저장
MSETNX key value [key value...] key가 없을 경우 저장
APPEND key value 데이터를 추가(append) 한다.
SETEX key seconds value 데이터 만료시간을 설정한다.
GETBIT key offset offset 이후의 비트 값 조회
SETBIT key offset value offset 이후에 비트 값 설정
STRLEN key 데이터의 바이트 수를 반환한다.
GETRANGE key start end start end 범위의 데이터를 반환
BITCOUNT key [start end] 비트 1을 센다.
BITOP key operation destkey key [key...] 비트연산(AND, OR, XOR, NOT) 실행
INCRBYFLOAT key increment increment만큼 실수 증가
PSETEX key milliseconds value 밀리초단위로 만료시간 설정
BITPOS key bit 지정한 bit의 위치를 구한다

INCR 과 DECR을 이용한 Rate Limit 구현

INCR은 value를 1씩 증가한다. 인터넷 서비스에서 페이지와 아이템에 대한 방문 카운트를 저장할 때 유용하게 사용 할 수 있다. "item:x0001" 아이템에 대한 카운터를 해보자. key가 없다면, value가 0인 key를 만들고 여기에 1을 더한다.
INCR item:x0001
(integer) 1
INCR item:x0001
(integer) 2
GET item:x0001
DECR은 value를 1씩 감소한다. key가 없다면, value가 0인 key를 만들고 여기에서 1을 뺀다. -1이 될 것이다.
DECR item:x0002
(integer) -1

INCR은 카운팅외에, 유저별 API 호출 제한 등에 사용 할 수 있다. 보통 호출 제한은 두 가지 방식으로 이루어진다.
  1. 한달 동안 호출할 수 있는 총량
  2. 특정시간(예를 들어 1분)동안 호출 할 수 있는 API 제한. 한돌 동안의 호출량을 초과하지 않더라도 특정시간에 대량의 API가 호출 되면, Rate Limite를 걸어야 할 수 있다.
yundream 유저가 201809월에 사용한 API는 아래와 같이 count하면 된다.
INCR api:yundream:201809
(integer) 1
INCR api:yundream:201809
(integer) 2
API 리미트를 설정하고 관리하고 싶다면 아래의 사항들을 코드에 녹여내야 할 것이다.
  1. API counter는 일정 시간이 지나면 지워져야 한다. 전달 API 사용량은 Redis에서 조회하는 정책으로 하면, 2달 정도 유지하면 될 것이다.
  2. API를 호출전에 INCR 명령어를 실행해서 카운트를 늘린다. INCR 명령어는 현재 value를 반환하는데, 이 값이 한달 사용량을 초과 하면, API 호출 실패 메시지(한달 사용량을 초과했습니다.)를 리턴한다.
  3. 실 서비스에서라면 Limit의 90%정도에 도달 했을 때, 경고메일을 전송하는 장치를 만들어야 할 것이다.
짧은시간 대량의 요청을 막기 위한 장치도 시간이 분단위로 조정된다는 것 말고는 특이 사항은 없다.

BIT

Redis는 string를 바이트로 다룬다고 했다. 이런 이유로 Redis string은 BitField와 XOR, AND, OR, NOT과 같은 비트연산도 지원한다. 비트연산을 어디에 써먹을 수 있을 것인지 느낌이 오지 않을 수 있을 것 같아서, Redis string에서의 비트 관련 명령어들에 설명과 이들 명령어를 어디에 응용할 수 있는지 살펴보기로 했다.

SETBIT로 key에 BIT를 설정 할 수 있다. 사용법은 아래와 같다.
SETBIT key offset value
offset 만큼 bit단위로 이동한 다음 value를 설정한다. offset은 0부터 시작한다. offset 0, 즉 0번째 위치에 1을 설정하고 값을 읽어보자.
SETBIT mykey 0 1
(integer) 0

GET mykey
"\x80"
16진수로 80이 출력됐다. 왜 이 값이 출력됐는지를 확인하기 위해서 현재 상태를 그림으로 묘사했다.

 SETBIT

0번째 비트를 1로 설정했기 때문에, "10000000"이고 이걸 16진수로 출력하다보니 "\x80"이 된거다. 여기에서 7번째 비트를 1로 설정하면 어떤 값이 나올지 예상 할 수 있을 것이다.
SETBIT mykey 7 1
(integer) 0

GET mykey
"\x81"
GETBIT를 이용해서 offset 만큼위치의 bit 값을 읽을 수 있다.
GETBIT mykey 7
(integer) 1

BITCOUNT를 이용하면 몇개의 비트가 1로 설정됐는지를 확인 할 수 있다.
BITCOUNT key [start end]
start와 end를 생략하면 key 전체에서 1로 설정된 비트를 계산한다.
BITCOUNT mykey 0 0
(integer) 2
start가 0 이고 end가 0이니, 처음 1바이트에 1로 세팅된 비트를 카운팅한다.

SETBIT를 이용하면 유니크방문자를 계산 할 수 있다. 유저식별번호가 int 형이라고 가정할 때, 유저가 방문을 하면 유저식별번호 만큼 offset한 위치를 1로 설정한다. 그리고 BITCOUNT를 하면 특정시간동안 방문한 유니크유저를 구할 수 있다. 아래와 같이 Key를 만들 수 있을 것이다. 2018년 8월 26일 12021, 10035 유저가 방문했다면, 아래와 같이 SETBIT을 한다.
SETBIT visitor:d:20180826  12021 1 
SETBIT visitor:d:20180826  10035 1 
이렇게 하면, 공간을 극도로 아낄 수 있다. 천만명 유저가 방문하는 사이트라면, 1.2메가바이트 정도의 공간으로 유니크한 일 방문자수를 계산할 수 있다.

Redis에서 저장 할 수 있는 string의 최대크기는 512메가바이트 이므로 2^32 만큼의 정보를 저장 할 수 있다.

BITFIELD

Bit field는 컴퓨터 프로그래밍에서 사용하는 데이터 구조다. 일련의 비트들을 저장하기 위해서 일정한 크기의 메모리 공간을 할당하고, 여기에 고정된 크기의 데이터를 차례로 저장을 한다.

Redis의 BITFIELD는 Bit field의 구현이다. 입력해야 하는 데이터의 크기가 고정이되며, 인접한 위치에 연속적으로 배치 할 수 있을 경우 사용 한다. 대표적인 예가 타임시리즈(Time series)데이터의 저장이다.

타임시리즈 데이터의 대표적인 예는 모니터링 정보의 저장이다. 예를들어 cpu 사용량을 모니터링 할 경우, 5분단위로 0~100 사이의 데이터를 연속해서 저장할 거다. 사용방법은 아래와 같다.
BITFIELD key [GET type offset] [GET type offset] ...
BITFIELD key [SET type offset value] [SET type offset value] ...
type는 i8(int8), i16(int16), u8(unsigned int8), u16(unsigned int16)등으로 정할 수 있다.

5분단위로 cpu 사용율 데이터를 저장한다고 가정해보자. cpu 사용율은 0~100일테니 u8(0~255)로 충분할거다. 2018년 8월 29일 00:00, 00:05 데이터를 저장했다.
BITFIELD cpu:usage:20180829 SET u8 #0 12 SET u8 #1 25
이런식으로 저장하면 된다. cpu의 하루치 모니터링 정보를 저장하기 위해서 사용한 메모리 공간은 288 바이트 정도다. 데이터를 꺼내보자.
BITFIELD cpu:usage:20180829 GET u8 #0 GET u8 #1
1) (integer) 12
2) (integer) 25
GETRANGE로도 데이터를 읽을 수 있다.
GETRANGE cpu:usage:20180829 0 1
"\x0c\x19"
16진수로 표현되기 때문에, 변환을 해야 겠지만 많은 데이터(이를테면 하루치 전부)를 가져올 때 편하게 사용 할 수 있다. STRLEN을 value의 길이를 계산해보자.
STRLEN cpu:usage:20180829
(integer) 2

DEL

key를 삭제 할 수 있다. key와 함께 value도 삭제된다.
DEL user:info:yundream
(integer) 1

FLUSHALL

FLUSHALL 명령으로 데이터베이스에 있는 모든 key들을 삭제할 수 있다.

KEYS

KYES 명령으로 데이터베이스에서 특정 패턴을 가지는 KEY를 가져올 수 있다. glob-style 패턴을 사용 할 수 있다.
KEYS pattern  
KEYS 명령의 시간복잡도는 O(N)으로 데이터베이스에 있는 모든 key들을 스캔해서 일치하는 key를 찾는다. Key가 많을 경우 많은 시간이 걸릴 수 있다. 또한 Redis의 one thread 운용이라는 특성 때문에, 이 시간동안 다른 작업이 블럭된다. 그러니 특별한 관리목적이 아닌 한 애플리케이션에 사용하면 된다. 대신 SCAN의 사용을 권장한다.

SCAN

패턴으로 일치하는 key를 찾기 위해서는 모든 key에 대해서 패턴비교연산을 수행하는 수 밖에 없다. 이런 작업에서 시간을 줄이기 위해서 사용하는 기술이 paging이다. 모든 데이터를 전부 가져오는 대신에, 일정 갯수만 가져오는 방식이다. 현재 어디까지 가져왔는지를 알고 있다면, 증분해서 반복을 하면서 데이터를 가져올 수 있다.

SCAN 명령을 이용해서 증분반복을 통해서 key를 가져올 수 있다.
SCAN cursor [MATCH pattern] [COUNT count]
테스트를 위해서 10개의 key를 입력했다. MSET을 이용하면 하나의 명령으로 여러 개의 키를 입력할 수 있다.
MSET key:1 1 key:2 2 key:3 3 ....

먼저 KEYS로 전체 key를 모두 가져왔다.
KEYS key*
 1) "key:6"
 2) "key:7"
 3) "key:10"
 4) "key:9"
 5) "key:3"
 6) "key:2"
 7) "key:4"
 8) "key:5"
 9) "key:8"
10) "key:1"

위 명령을 SCAN으로 다시 작성했다.
> scan 0 MATCH key*
1) "3"
2) 1) "key:3"
   2) "key:7"
   3) "key:8"
   4) "key:1"
   5) "key:6"
   6) "key:2"
   7) "key:4"
   8) "key:5"
두 개의 반환 값이 있다. 3은 현재 cursor다. 다음번 반복요청을 하고 싶다면, cursor를 3으로 설정하고 명령을 내리면 된다.
scan 3 MATCH key*
1) "0"
2) 1) "key:10"
   2) "key:9"
cursor가 0이다. 더 이상 가져올 key가 없음을 의미한다.

count를 설정해보자.
SCAN 0 MATCH key* COUNT 5
1) "14"
2) 1) "key:3"
   2) "key:7"
SCAN 14 MATCH key* COUNT 5
1) "3"
2) 1) "key:8"
   2) "key:1"
   3) "key:6"
   4) "key:2"
   5) "key:4"
   6) "key:5"
SCAN 3 MATCH key* COUNT 5
1) "0"
2) 1) "key:10"
   2) "key:9"
SCAN은 string에서 key를 찾기 위해서 사용하는 명령이고, 다른 데이터 스트럭처에 대해서도 그에 맞는 SCAN 변형이 존재한다. 이들은 각 데이터 스트럭처를 다루면서 살펴보도록 하겠다.

Redis와 Persistent 스토어

앞서예제로 다루었던 Rate Limit 아이디어를 Redis로 만드는 건 어려운일이 아니다. 하지만 Redis는 persistent하지 않아서 persistent하게 데이터를 저장 할 수 있는 시스템을 준비해야 하기 때문에, 실제 구성에서는 신경써야 할 것들이 꽤 있다. 아래 그림은 이러한 구성 중 하나를 묘사하고 있다.

 REDIS 읽기와 쓰기의 분리

유저가 API를 호출하면, 호출로그를 Message Queue(Kafka같은)으로 보낸다. Message Queue에 연결돼 있는 메시지 처리기(보통 컨슈머 프로세스라고 부르는)가 메시지를 읽어서 REDIS 카운트를 하고, RDBMS에도 기록한다. 만약 REDIS에 문제가 생겨서 모든 데이터가 날라가더라도 RDBMS에 있는 데이터로 복원 할 수 있을 것이다.

구성은 애플리케이션마다 달라질 수 있겠으나, persistent한 스토어를 가지는 이 패턴은 거의 모든 경우에 들어가야 한다. 죽 Redis는 "보조" 역할이어야 하며, 그 자체가 "주"가 되어서는 안된다. 예를들어 요즘에는 대량의 유저 데이터를 "빅데이터 플랫폼"에서 분석 한 다음 서비스 할 수 있도록 데이터베이스에 올리는 등의 작업을 많이 하는데, 이 경우에도 우선은 RDBMS혹은 MongoDB, DynamoDB등으로 올리고 난 다음, Redis는 캐시 혹은 2차연산 결과를 서비스하기 위한 목적으로 사용 한다.

복습

  1. 웹 서비스에서 유저세션 정보를 저장해야 한다. 어떻게 저장할지 계획을 세우고 redis-cli로 테스트를 해보자.
  2. SCAN 작동 방식이 궁금하지 않은가 ?
« Redis 시작 목차 Redis Data structure - LIST »