Redis 정의

Redis(Remote Dictionary Server)는 Key-Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈소스 기반의 비관계형 데이터베이스 관리 시스템(DBMS)이지만 DBMS보다는 빠른 캐시의 성격으로 대표되며, NoSQL 중 하나에 속한다.

데이터베이스, 캐시, 메시지 브로커로 사용되며 인메모리 데이터 구조를 가진 저장소이다.

Redis는 기본적으로 영속성을 위한 데이터베이스는 아니지만, 백업을 통한 영속성을 지원한다.

Redis 활용처로는 Session Store, Cache, 분당 호출 수를 제한하는 등의 Limit Rater, Job Queue 등이 있다.

 

Key-Value Store

Redis는 Key-value 구조로 자바의 Map과 같은 구조이다.

Key-Value Store의 장단점은 아래와 같다.

장점 단점
- 단순성에서 오는 쉬운 구현과 사용성
- Hash를 이용해 값을 바로 읽으므로 빠른 속도를 보장한다.
- Hash를 이용하기 때문에 추가 연산이 필요없다.
- 분산 환경에서의 수평적 확장성이 보장된다.
- Key를 통해서만 값을 읽을 수 있다.
- 범위 검색등의 복잡한 질의가 불가능하다.

 

 

NoSQL의 종류

NoSQL의 데이터 모델은 Key-Value, Document, Wide-column, Graph로 분류할 수 있다.

 

  • Key-Value
    • Redis, Memcached, Riak, DynamoDB
  • Document
    • MongoDB, CouchDB
  • Wide-column
    • Cassandra, HBase, Google Big Table
  • Graph
    • Neo4j, OrientDB, AgensGraph

 

Data Type

Redis의 value는 여러 데이터 타입이 존재한다.

  1. Strings
  2. Lists
  3. Sets
  4. Hashes
  5. SortedSets
  6. Bitmaps
  7. HyperLogLog

 

Strings

가장 기본적인 데이터 타입이며 바이트 배열을 저장하는데 binary-safe로 처리된다.

binary-safe는 모든 문자를 표현할 수 있다는 의미로 제외되는 문자 없이 모든 문자를 저장할 수 있다.

C 언어의 경우 변수에 문자열을 넣을 때 마지막에 null 문자를 넣는다. null 문자열 자체는 문자 코드가 존재하는데 이 문자코드는 문자열의 끝을 의미하기 때문에 문자열로 표현하지 않는다.

그러나 binary-safe는 이런 제외되는 문자열 없이 모든 문자를 저장할 수 있는 것이다.

 

주요 명령어로는 set, get, incr, decr, mset, mget이 있다.

 

 

 

set은 특정 키에 문자열 값을 저장하는 명령어다. set [keyName] [value] 구조로 저장할 수 있다.

get은 특정 키에 저장된 문자열 값을 가져오는 명령어다. get [keyName] 구조로 되어있다.

 

Redis에는 MySQL의 auto_increment나 Oracle의 Sequence같이 자동 증감을 처리할 수 있다.

위 기능들은 간단한 이해를 위한 예시일 뿐이고 차이가 있는데 MySQL과 Oracle은 데이터가 삽입되는 시점에 특정 컬럼 값을 증가시켜주기 위함이지만 Redis에서는 특정 Key의 value를 Integer로 취급하고 1씩 증감한다는 점이다.

INCR [keyName] 혹은 DECR [keyName] 구조로 사용한다.

INCR은 1 증가, DECR은 1 감소시킨다.

이 두 명령어는 원자성이 보장되기 때문에 여러 요청이 몰리더라도 중복되거나 누락되는일 없이 정상 처리된다.

 

mset, mget은 한번에 여러 키에 대한 값을 저장하거나 가져오는 명령어다.

mset keyName1 value1 keyName2 value2 ... 와 같은 구조다.

mget keyName1 keyName2 ... 와 같은 구조로 여러 데이터를 가져올 수 있다.

mset firstKey firstValue secondKey secondValue 이 명령어를 수행한 뒤

keys * 명령어를 통해 모든 key를 출력해보면 firstKey, secondKey가 출력되는 것을 확인할 수 있으며

mget firstKey secondKey 이 명령어를 실행하게 되면 firstValue, secondValue가 출력되는 것을 볼 수 있다.

한번에 여러 Key와 그 값을 저장할 수 있다라는 개념이지 배열 형태로 저장하는 것이 아니다.

 

 

Lists

Linked-List 형태의 자료구조로 인덱스 접근은 느리지만, 데이터의 추가 및 삭제가 빠르다는 장점이 있다.

Lists는 Queue와 Stack의 역할로도 사용할 수 있다.

 

주요 명령어로는 lpush, rpush, llen, lrange, lpop, rpop이 있다.

 

lpush와 rpush는 리스트의 왼쪽(head) 혹은 오른쪽(tail)에 새로운 값을 추가하는 명령어다.

lpush listName value

rpush listName value

 

llen은 리스트에 들어있는 아이템 개수를 반환한다.

llen listName

 

lrange는 리스트의 특정 범위를 반환한다.

lrange listName 시작 인덱스 끝 인덱스

인덱스 범위를 0 -1로 설정하게 되면 0번 인덱스부터 가장 끝 요소까지 모두를 출력한다.

이때 인덱스의 경우 zero index 방식이다.

 

lpop은 리스트의 왼쪽(head)에서 값을 삭제하고 반환한다.

lpop listName

즉, Queue와 같이 가장 먼저 들어간 head 값을 추출하고 반환하게 된다.

 

rpop은 리스트의 오른쪽(tail)에서 값을 삭제하고 반환한다.

rpop listName

lpop과 반대로 Stack처럼 가장 나중에 들어간 값을 추출하고 반환한다.

 

 

Sets

순서가 없는 유니크한 값의 집합이다. 유니크한 집합인 만큼 중복이 존재할 수 없다.

검색이 빠르고 개별 접근을 위한 인덱스는 존재하지 않는다.

sets 타입을 통한 교집합, 합집합 등의 집합 연산이 가능하다.

 

주요 명령어로는 sadd, srem, scard, smembers, sismember가 있다.

 

sadd는 Set에 데이터를 추가하는 명령어다.

sadd setName value

 

srem은 set에서 데이터를 삭제하는 명령어다.

srem setName value

 

scard는 set에 저장된 아이템의 개수를 반환한다.

scard setName

 

smembers는 set에 저장된 아이템들을 반환한다.

smembers setName

 

sismember는 특정 값이 set에 들어있는지를 반환한다.

sismember setName value

 

위 명령어들 중 scard, smembers를 제외한 나머지 명령어들은 결과가 0, 1로 반환된다.

0은 실패, 1은 성공이다.

이미 들어가있는 데이터를 또 넣으려고 한다거나, 존재하지 않는 데이터를 삭제하고자 하는 경우에도 오류가 발생하는 것이 아닌 0이 출력된다.

 

 

Hashes

Hashes는 하나의 Key 하위에 여러개의 field-value 쌍을 저장하는 구조다.

여러필드를 가진 객체를 저장하는 것으로 HINCRBY 명령어를 통해 카운터로 활용이 가능하다.

 

하나의 Key 하위에 여러개의 field-value 쌍을 가지는 구조라는 것은 마치 아래 JSON과 같은 구조라고 볼 수 있다.

{
    "user1": {
        "name": "coco",
        "age": 9
    }
}

 

Redis는 JSON을 그대로 value에 넣을 수 있는데 Hashes와 JSON Value의 차이점은 단일 데이터 조회에서 발생한다.

위와 같은 구조의 데이터를 저장한뒤 name만 조회해야 하는 상황이라고 가정했을 때, JSON이 그대로 value에 담기는 경우 user1의 모든 데이터를 조회해 가져온 뒤 name만 따로 뺄 수 있도록 파싱해야 한다.

하지만, Hashes로 저장하게 되면 조회 시점에서부터 name만 조회해 파싱하는 과정이 사라지게 된다.

 

항상 해당 데이터가 전부 필요하다고 보장된다면 JSON을 그대로 담는 방법도 좋을 수 있겠으나, 그렇지 않은 경우에는 Hashes가 더 유용할 수 있다.

또한, 하나의 value에 대해 increase, decrease도 사용할 수 있기 때문에 이런 기능이 필요하다면 Hashes를 선택하는 것이 유리하다.

 

주요 명령어로는 hset, hget, hmget, hincrby, hdel이 있다.

 

hset은 한개 또는 다수의 필드에 값을 저장하는 명령어다.

hset key field value field value
위 코드를 예시로 user1은 key가 된다. 그리고 name은 field 또는 sub-key라고 불리며, 그 값은 value이다.

그럼 예시대로 저장하기 위한 명령어는 hset user1 name bear age 10  이런 명령어가 된다.

 

hget은 특정 필드의 값을 반환하는 명령어다.

hget key field

hget user1 name 명령어를 입력하면 user1의 name 필드 값인 coco가 반환된다.

 

hmget은 한개 이상의 필드값을 반환하는 명령어다.

hmget key field field

# 명령어 결과
hmget user1 name age

1) "coco"
2) "9"

 

 

hincrby는 특정 필드의 값을 Integer로 취급하고 지정한 수 만큼 증가시킨다.

hincrby key field 증가값

# 명령어 결과
hincrby user1 name 1

(error) ERR hash value is not an integer


hincrby user1 age 1

(integer) 10


hmget user1 name age

1) "coco"
2) "10"

위와 같이 Integer로 변환이 불가능한 경우에는 오류가 발생하며, 변환할 수 있는 값인 경우에만 증가된다.

Hashes에서는 hincrby를 통해 값을 증가시키는 것은 가능하지만 감소시키는 decrease는 수행할 수 없다.

 

 

hdel은 한개 이상의 필드를 삭제하는 명령어다.

hdel key field field

삭제된 필드를 조회하면 (nil)이 출력된다. 애초에 저장하지 않았던 필드를 조회하는 경우에도 (nil)이 출력되기 때문에 존재하지 않는 필드를 조회하는 경우에는 (nil)이 출력된다고 보면 된다.

그렇기 때문에 name, age를 모두 제거하고 hmget으로 조회하더라도 둘다 (nil)이 나오게 된다.

그럼 user1이라는 key에 존재하는 모든 필드가 제거되었는데 이 key는 남는건가? 싶을 수 있는데 keys * 명령어를 통해 조회해보면 해당 key 역시 삭제되어 존재하지 않는 것을 확인할 수 있다.

 

그리고 hdel user1 name을 통해 name 필드만 제거한 뒤 다시 hset user1 name coco 명령어를 통해 필드를 저장하면 동일하게 user1 key안에 name 필드가 저장된다.

 

 

SortedSet

Set과 유사한 유니크한 값의 집합이지만, 각 값은 연관된 정수형의 score를 갖고 정렬되어 있다는 점이 차이점이다.

SortedSet은 정렬된 상태이기 때문에 빠르게 최소, 최대값을 구할 수 있어 순위계산이나 리더보드 구현등에 활용이 된다.

 

주요 명령어는 zadd, zrange, zrank, zrevrank, zrem, zincrby가 있다.

 

 

zadd는 한개 또는 다수의 값을 추가 또는 업데이트 하는 명령어다.

zadd key score value score value ... 와 같은 구조다.

 

zrange는 특정 범위의 값을 오름차순 정렬로 반환한다.

zrange key 시작인덱스 끝인덱스 [withscores]

Lists와 마찬가지로 zero index 방식이다.

withscores 옵션을 같이 입력하게 되면 score와 함께 출력한다.

# 값 추가
zadd rank 10 coco 20 mozzi

# 조회
zrange rank 0 -1

1) "coco"
2) "mozzi"


zrange rank 0 -1 withscores

1) "coco"
2) "10"
3) "mozzi"
4) "20"

 

그럼 여기까지 잠깐 정리하면 score는 정렬되는 기준이 된다.

그렇기 떄문에 삭제 명령어를 통해 coco를 제거한 뒤 다시 10의 score로 추가하게 되면 동일하게 coco가 먼저 출력되게 된다.

 

주의사항으로는 score는 중복될 수 있으며, 같은 값인 경우에는 value 기준 사전순으로 정렬된다.

zadd rank 10 coco
zadd rank 10 mozzi
zadd rank 10 youn
zadd rank 10 jung

위와 같이 값을 추가하는 경우 coco -> jung -> mozzi -> youn 순서로 정렬된다.

 

 

zrank는 특정 값의 위치(순위)를 반환하는데 이때 오름차순 기준으로 위치를 반환한다.

zrank key value

이때도 zero index로 가장 첫 값의 위치는 0이다.

 

zrevrank는 zrank와 마찬가지로 특정 값의 위치를 반환하는데 내림차순 기준으로 위치를 반환한다.

zrevrank key value

그래서 가장 첫 값을 조회하게 되면 0이 아닌 총 개수 - 1개의 결과를 반환한다.

 

 

zrem은 한개 이상의 값을 삭제하는 명령어다.

zrem key value

 

 

zincrby는 특정 값의 순위를 지정한 값만큼 증가시키는 명령어다.

zincrby key increase-score value

위 명령어 예시에서 increase-score에 작성한 값만큼 score 값이 증가하게 되는데 이때도 증가한 값이 중복되는 값이라면 value 기준 사전순으로 정렬된다.

zadd rank 10 coco 20 mozzi 30 jung 30 youn

zrange rank 0 -1

1) "coco"
2) "mozzi"
3) "jung"
4) "youn"


zincrby rank 10 mozzi

1) "coco"
2) "jung"
3) "mozzi"
4) "youn"

 

 

 

Bitmaps

비트 벡터를 사용해 N개의 Set을 공간 효율적으로 저장한다.

하나의 비트맵이 가지는 공간은 4,294,967,295(2^32 - 1)인데 0과 1로만 이루어진 비트 벡터를 사용함으로써 간결하게 표현할 수 있는 데이터들의 저장 공간을 효율적으로 사용하는 것이다.

예시로, 방문 여부를 확인해야 할 때, visit이라는 key값의 bitmap을 활용하고 비트맵의 인덱스를 사용자 번호로 사용한다면 0은 미방문, 1은 방문으로 체크할 수 있게 된다.

그럼 하나의 비트맵만으로 42억명의 방문 이력을 처리할 수 있기 때문에 효율적이다.

 

주요 명령어로는 setbit, getbit, bitcount, bitop가 있다.

 

 

setbit는 비트맵의 특정 offset 값을 변경한다.

setbit key offset 변경값(0, 1)

 

getbit는 비트맵의 특정 offset 값을 반환한다.

getbit key offset

 

bitcount는 비트맵에서 set(1) 상태인 비트의 개수를 반환한다.

bitcount key

 

bitop는 비트맵들간의 비트 연산을 수행하고 결과를 비트맵에 저장한다.

bitop 수행할연산 결과비트맵key key1 key2

여기서 연산에는 and, or, xor, not을 사용할 수 있다.

 

# visit 이라는 key 값의 bitmap 생성 및 10번 인덱스의 값을 1로 변환
setbit visit 10 1
(integer) 0

setbit visit 9 1
(integer) 0


getbit visit 10
(integer) 1

getbit visit 11
(integer) 0


# 범위를 벗어난(43억번째 인덱스) 값 변경. getbit도 동일한 오류 발생
setbit 4300000000 1
(error) ERR bit offset is not an integer or out of range


# 새로운 visit2 bitmap 생성
setbit visit2 10 1
(integer) 0


# visit과 visit2를 비트연산하고 결과는 visitResult 라는 비트맵을 생성해 저장
bitop and visitResult visit visit2
(integer) 2


keys *
1) "visitResult"
2) "visit2"
3) "visit"


getbit visitResult 10
(integer) 1

bitcount visitResult
(integer) 1

 

결과에 대해 정리.

setbit에 대한 결과는 수정되기 전의 값이다.

즉, setbit visit 10 1을 최초 수행했을 때는 최초 설정값이 0이기 때문에 0이 출력되는거고 이후 setbit visit 10 1 또는 setbit visit 10 0을 수행하는 경우 이전 값인 1이 출력된다.

 

범위를 벗어난 값을 조회 또는 수정하려고 한다면 out of range 오류가 발생한다.

 

bitop 명령어의 결과는 생성된 비트맵의 크기를 나타내는데 byte 단위 결과다.

8bit = 1byte이기 때문에 10(index 11)까지만 값을 변경했으므로 2byte의 크기를 갖게 되므로 2를 반환하는 것이다.

그럼 여기서 알 수 있는것은 bitmaps는 크기가 최초 생성시부터 2^32 - 1인 512MB로 생성되는 것이 아니라는 것을 알 수 있다.

 

최대 크기가 512MB인거지 최초 생성시부터 512MB갖고 생성되지 않는다.

위 코드처럼 setbit visit 10 1을 통해 visit이라는 bitmap을 생성하게 되면 zero index 구조이기 때문에 11의 크기가 필요하다.

하지만 byte 단위로 생성되기 때문에 16bit은 2byte의 크기로 최초 생성되게 된다.

이후 setbit 명령어를 통해 더 큰 수의 index를 수정하는 경우 그때마다 byte 단위로 크기가 커지게 되는 것이다.

그리고 그 최대치는 512MB인 2^31 - 1까지 허용이 된다.

 

 

HyperLogLog

HyperLogLog는 유니크한 값의 개수를 효율적으로 얻을 수 있다.

확률적 자료구조이기 때문에 오차가 있으며, 매우 큰 데이터를 다룰 때 사용이 된다.

18,446,744,073,709,551,616(2^64, 1844경)개의 유니크 값을 계산하는 것이 가능하며 12KB의 메모리를 사용해 0.81%의 오차율을 허용한다.

보통 값들을 넣은 뒤 그 값들이 유니크한 값으로 나눴을 때 총 몇개인지를 알아내는 용도로 사용되고, bitmap의 bitcount와 비슷한 용도이지만 효율성이 더 높다.

 

만약 HyperLogLog가 아닌 Set을 사용한다면 데이터가 수백만개만 되더라도 몇 MB의 메모리를 사용하게 된다.

하지만 HyperLogLog를 사용하면 약 1800경 개의 데이터를 담더라도 고작 12KB만 사용하게 된다.

너무 대용량의 데이터이기 때문에 확률적으로 어느정도 오차율을 허용하고 성능을 높인다.

너무 많은 양의 데이터를 사용하는 경우는 99% 이상의 정확도라면 로직을 처리하기에 충분한 경우가 많다고 한다.

HyperLogLog는 0.81%의 오차율을 허용하기 때문에 이에 적합하고 많이 사용된다고 한다.

 

용도는 아무래도 count에 관련되다보니 주로 add, count 두가지 명령어를 사용한다.

특징으로는 값을 넣을 때 내부에 데이터를 저장하지 않는다는 점이 있다.

HyperLogLog라는 나름대로의 확률적 자료구조를 내부구현 형태로 갖고 있기 때문에 add로 저장한다고 해서 실제로 내부에 저장되지는 않는다.

 

 

주요 명령어로는 pfadd, pfcount, pfmerge가 있다.

 

pfadd는 HyperLogLog에 값을 추가하는 명령어다.

pfadd key value value value ...

 

pfcount는 HyperLogLog에 입력된 값들의 cardinality를 반환한다.

pfcount key

 

pfmerge는 다수의 HyperLogLog를 반환한다.

pfmerge 결과key key1 key2

 

# visit이라는 HyperLogLog 생성
pfadd visit coco mozzi youn jung
(integer) 1

pfcount visit
(integer) 4

# visit2라는 HyperLogLog 생성. youn만 visit과 동일하게.
pfadd visit2 coco2 mozzi2 youn jung2
(integer) 1

pfcount visit2
(integer) 4


# visit과 visit2를 합집합으로 합쳐서 visitResult라는 HyperLogLog 생성
pfmerge visitResult visit visit2
OK

# 중복은 제거되기 때문에 중복된 youn 하나가 제거되고 7개만 남는다.
pfcount visitResult
(integer) 7

 

HyperLogLog의 설명처럼 대용량의 데이터를 넣어서 테스트 해볼수가 없어서 간단하게 명령어 테스트만 수행해봤다.

유니크한 값의 개수를 효율적으로 얻기 위한 방법이기 때문에 set과 마찬가지로 중복은 허용하지 않으며, 중복되는 값을 의도적으로 넣더라도 오류가 발생하지 않는다.

 

 

 

Reference

 

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

이 포스팅은 레디스 공식 문서를 보고 지식을 정리하기 위해 쓴 글입니다. Strings Redis String 유형은 Redis 키와 연결할 수 있는 가장 간단한 유형의 값이다. Memcached에서의 유일한 데이터 타입이자, R

yeongunheo.tistory.com

 

+ Recent posts