[Redis Documentation #3] 데이터타입과 추상화

https://redis.io/topics/data-types-intro

GARIMOO
garimoo
29 min readMar 2, 2020

--

An Introduction to Redis data types and abstractions

레디스는 일반적인 key-value 타입의 저장소보다 발전된 여러 종류의 값을 지원하는 데이터 구조 서버이다. 기존 key-value 저장소가 문자열 키와 문자열 값만을 연결했지만, 레디스는 보다 복잡한 데이터 구조를 저장할 수 있다. 레디스에서 지원하는 데이터타입은 아래와 같다.

  • (Binary-safe) Strings
  • Lists: 삽입 순서에 따라 정렬된 문자열의 집합. 기본적인 linked list 형태
  • Sets: 고유하고 정렬되지 않은 문자열의 집합
  • Sorted Sets: Sets와 비슷하지만 모든 값은 스코어로 정렬됨
  • Sets와 달리 다양한 방법으로 활용 가능 (ex. 상위10개의 값, 하위 10개의 값)
  • Hashes: value와 연관된 필드로 구성된 맵. 모든 필드와 값은 모두 문자열로 구성
  • Bit arrays (Bitmaps)
  • HyperLogLogs: 집합의 카디널리티를 추정하기 위해 사용되는 확률적 데이터 구조.
  • Streams: 추상 로그 데이터 유형을 제공하는 append-only 자료구조

Redis Keys

레디스의 키는 Binary Safe 하다. ‘abc’와 같은 문자열부터 JPEG 파일까지 모든 이진 시퀀스를 키로 사용할 수 있다는 뜻이다. 빈 문자열 또한 키로써 유효하다.

  • 길이가 매우 긴 키를 사용하는 것은 권장하지 않는다. 1024바이트 크기의 키는 메모리도 많이 차지할 뿐 아니라, 키 조회시 비용이 많이 들 수 있다. 길이가 긴 키값을 저장해야 한다면 해시를 사용하는 것이 메모리와 대역폭 관점에서 더 좋은 방법이다.
  • 길이가 짧다고 무조건 좋은 것도 아니다. 예를 들어 'user:1000:followers' 의 키를 'u1000flw' 로 줄이는 건 불필요하다. 전자의 방식이 가독성이 훨씬 좋고, 후자와 비교했을 때 메모리를 유의미하게 많이 사용하는 것이 아니기 떄문이다. 키가 짧을수록 메모리를 적게 사용하긴 하지만, 그 둘의 밸런스를 찾는 것이 중요하다.
  • 스키마를 사용하는 것이 좋다. 예를 들어 “user:1000” 처럼 “object-type:id” 형태를 사용해라. “comment:reply.to” 또는 “comment:reply-to”와 같이 . 이나 - 부호 또는 : 를 사용해서 구분하는 것을 권장한다.
  • 허용되는 최대 키 크기는 512MB이다.

Redis Strings

레디스의 String은 키와 연결할 수 있는 가장 간단한 유형의 값이다. 레디스의 키가 문자열이므로 이 구조는 문자열을 다른 문자열에 매핑하는 것이라고 보면 된다. HTML fragment나 페이지 캐싱 등에 자주 사용된다.

위의 예제에서 볼 수 있듯이 SET이나 GET 커맨드를 사용해서 String 값을 설정하고 조회할 수 있다. SET 커맨드는 해당 키에 String이 아닌 값이 연결되어 있다면 새로 입력한 값으로 대체한다.

String 타입에는 모든 종류의 문자열 (이진 데이터 포함) 을 저장할 수 있다. 따라서 JPEG 이미지를 저장할 수도 있다. 최대값은 512MB이다.

키가 이미 존재하거나, 존재하지 않을 때에만 데이터를 저장하게 하는 옵션도 있다.

String은 레디스의 가장 기본적인 타입이고, 이를 이용한 여러 기능이 존재한다. 예를 들어 다음 커맨드는 모두 atomic하게 동작한다.

INCR 명령어는 String을 정수로 파싱하고, 이를 1씩 증가시킨 값으로 변경한다. 유사한 커맨드로는 INCRBY, DECR, DECRBY 등이 있다. 내부적으로는 항상 같은 방식으로 동작한다.

INCR 명령어가 atomic하다는 것은 무엇을 의미할까? 동일한 키에 대해 INCR를 수행하는 여러 클라이언트가 경쟁 상태(Race Condition)을 발생시킬 일이 없음을 뜻한다. 예를 들어 클라이언트 A가 와 B가 동시에 10을 읽고 값을 동시에 11로 증가시켜 저장하는 상황은 발생하지 않는다는 것이다. 레디스에서 이 예제 상황이 발생하면 결과값은 항상 12이고, READ-INCREMENT-SET 기능은 atomic하게, 모두 다른 클라이언트가 명령어를 수행하지 않을 때 발생한다.

이외에도 String은 다양한 커맨드를 제공한다. 예를 들어 GETSET 은 키를 새 값으로 변경하고 이전값을 반환한다. 예를 들어 웹사이트가 새 방문자를 받을 때마다 INCR를 이용하여 키를 증가시키는 시스템이 있는 경우에 이 명령어를 사용한다면, 증분을 계속 하면서 한시간에 한번씩 정보를 수집할 수 있다. 키에 새 값 0을 할당하고 이전 값을 다시 읽도록 키를 가져오는 커맨드도 있다.

단일 명령으로 여러 키의 값을 설정하거나 검색할 수도 있는데, 이는 대기 시간 단축에 유용하다. MSETMGET 커맨드를 이용하면 된다.

Altering and querying the key space

키 자체에 대한 조회를 할 수 있는 커맨드도 존재하고, 이는 특정 타입에 국한되지 않는다.

예를 들어 EXISTS 커맨드는 해당 키가 DB에 있는지 확인해서 0이나 1을 반환한다. DEL 커맨드는 값에 관계없이 키를 삭제한다.

예제에서 볼 수 있듯이, DEL이 1과 0 중 어떤 값을 반환했는지에 따라 해당 키가 지워졌는지(이미 존재했는지), 지워지지 않았는지 (그런 이름의 key가 존재하지 않았는지) 확인할 수 있다.

키 관련 명령이 많이 있지만, 위의 두 명령은 지정된 키에 저장된 값의 종류를 반환하는 TYPE과 함께 필수적인 커맨드이다.

Redis expires: keys with limited time to live

더 복잡한 자료구조로 넘어가기 전에, 타입과 상관 없이 작동하는 expire 기능에 대해 알아보자. 일정 시간 이후 키가 삭제되도록 모든 키에 timeout을 설정할 수 있다. 사용 시간이 경과하면 사용자가 키에 대해 DEL 명령어를 호출한 것처럼 키가 자동으로 삭제된다.

  • 초나 밀리초를 이용해서 만료 시간을 설정할 수 있다.
  • 하지만 만료되는 시간은 항상 1밀리초 단위로 발생한다.
  • 만료에 대한 정보는 리플리카 노드를 생성할 때에 복제되고, RDB나 AOF 파일로 저장될 때에도 포함된다.

만료시간 설정은 간단하다.

두번째로 키를 GET 한 시간은 5초가 지난 이후이므로 해당 키는 삭제되었다. 위의 예에서는 만료를 설정하기 위해 EXPIRE 커맨드를 사용했고, 기존에 만료시간이 설정되어 있는 키에도 다른 만료시간을 설정하기 위해 해당 커맨드를 사용할 수 있다. PERSIST 커맨드를 이용하면 만료 설정을 제거하고 키를 영구적으로 유지하도록 설정할 수 있다. 다음의 예제처럼 키를 생성 할 때 바로 만료시간 설정을 지정하는 옵션도 존재한다.

위의 예제는 key에 100을 저장했고, 만료 시간을 10초로 설정했다. TTL 커맨드로 키의 남은 시간을 확인할 수 있다.

밀리초 단위로 설정하고 확인하려면 PEXPIRE, PTTL 명령어를 사용하면 된다.

Redis Lists

레디스의 리스트는 linked list 로 만들어졌다. 즉 리스트안에 수백만 개의 아이템이 있더라도 목록의 head와 tail에 값을 추가할 때 동일한 시간이 소요된다. LPUSH 커맨드를 사용해서 새로운 아이템을 10개의 아이템을 가지고 있는 리스트의 head에 추가하는 속도는 목록에 1천만개의 아이템을 추가하는 것과 같다.

데이터베이스에서는 아이템을 매우 긴 리스트에 빠른 방법으로 추가할 수 있어야 하기 때문에 레디스의 리스트는 linked list로 구현되었다. 또 다른 장점은 리스트의 일정 범위를 일정 시간에 반환할 수 있다는 것이다.

하지만 인덱스로 리스트에 접근할 때에는 그다지 빠르지 않다. 인덱스를 이용해서 아이템에 빠르게 접근해야 할 경우가 많다면 sorted set을 사용하는 것을 권장한다.

First steps with Redis Lists

LPUSH 커맨드는 리스트의 가장 왼쪽에(head) 새로운 아이템을 추가하고, RPUSH 커맨드는 리스트의 가장 오른쪽에(tail) 새로운 아이템을 추가한다. LRANGE 커맨드는 리스트의 범위로 아이템을 추출한다.

LRANGE를 사용할 때에는 두 개의 인덱스가 필요하다. 인덱스는 음수로도 사용이 가능한데, 거꾸로 생각하면 된다. 그러므로 -1이 마지막 아이템이고, -2는 마지막에서 두번째 아이템이다.

위의 예제에서 RPUSH는 리스트의 오른쪽에 요소를 입력했지만 마지막의 LPUSH는 리스트의 제일 왼쪽에 요소를 입력한 것을 알 수 있다.

모든 커맨드는 여러 아이템을 한번에 처리할 수 있다.

리스트에서 중요한 것은 저장된 요소를 pop하는 기능이며, 목록에서 아이템을 반환하고 동시에 삭제한다. 목록의 양쪽에서 아이템을 push할 수 있듯이 양쪽에서 pop할 수 있다.

세개의 요소를 push하고 세번 pop 했으니 이 리스트는 비어있고, 더이상 pop 할 아이템은 남아있지 않다. 이 상태의 리스트에서 다시 pop을 시도하면 아래와 같은 결과를 얻을 수 있다.

레디스는 NULL 값을 반환하여 리스트에 아무 값도 들어있지 않음을 리턴한다.

Common use cases for lists

리스트는 여러 작업에 유용하지만, 대표적인 두 가지 사용 사례는 다음과 같다.

  • 사용자가 SNS에 게시한 최신의 포스트에 대해 생각해보자.
  • 프로세스간의 통신 방법에서, 소비자-생산자 패턴은 생산자가 아이템을 만들어서 리스트에 넣으면, 소비자가 꺼내와서 액션을 수행하는 식으로 동작된다. 레디스에는 이 사례를 좀 더 효율적이고 안정적으로 만들 수 있도록 하는 방법이 존재한다.

예를 들어 루비의 유명한 라이브러리인 resque와 sidekiq 는 레디스의 리스트를 이용하여 백그라운드 작업을 구현한다.

트위터는 레디스의 리스트를 사용하여 사용자의 최신 트윗을 가져온다.(링크)

일반적인 유스케이스를 단계적으로 설명하기 위해, 홈페이지에 SNS에 게시한 최신 사진이 표시되고, 이 액세스 속도를 높히고 싶을 때를 생각해보자.

  • 사용자가 새 사진을 게시할 때마다 LPUSH를 사용해서 ID를 목록에 추가한다.
  • 사용자가 홈페이지를 방문할 때 최신 10개의 항목을 얻기 위해 LRANGE 0 9 를 사용한다.

Capped lists

많은 사례에서 우리는 SNS 업데이트, 로그 또는 기타 모든 항목을 저장하기 위해 리스트를 사용한다.

레디스에서는 리스트의 최신 N개 아이템만 기억하도록 LTRIM 커맨드를 사용해서 가장 오래된 항목을 삭제할 수 있는 기능을 제공한다.

LTRIM 커맨드의 사용법은 두개의 인덱스를 사용해서 범위를 표시한다는 점에서 LRANGE와 유사하지만, 해당 범위를 벗어난 모든 요소는 제거된다는 점에서 다르다.

위의 LTRIM 커맨드는 리스트에서 인덱스 0부터 2까지의 아이템만 남기게 되는데. 이를 통해 매우 간단하지만 유용한 패턴으로 사용 가능하다. 새로운 아이템을 추가하고 범위를 초과하는 아이템을 제거하기 위해 push와 trim을 함께 사용하면 된다.

위 명령어의 조합은 새로운 아이템을 추가하고 1000개의 최신값만 리스트에 남겨둔다. LRANGE를 사용하면 오래된 데이터를 기억할 필요 없이 상위 항목에 액세스 할 수 있다.

Blocking operations on lists

리스트는 큐를 구현하기에 적합한 기능을 가지고 있고, 일반적으로 프로세스간 통신 시스템의 구성 요소로 blocking 기능을 사용할 수 있다.

예를 들어서, 한 프로세스가 큐에 아이템을 넣고, 다른 프로세스를 사용해서 해당 아이템을 꺼낸다고 생각할 때 이는 일반적인 생산자-소비자 패턴이고, 다음과 같은 간단한 방법으로 구현할 수 있다.

  • 생산자는 LPUSH 커맨드로 리스트에 아이템을 넣는다.
  • 리스트에서 아이템을 꺼내려면 RPOP을 사용한다.

하지만 어떤 상황에서 리스트가 비어있다면 RPOP은 NULL을 반환한다. 이 경우 소비자가 RPOP을 다시 시도해야 한다. 이것을 polling이라 하며, 이는 다음과 같은 몇 가지 단점이 있기 때문에 좋은 방법이 아니다.

  • 불필요한 작업이 계속될 수 있다.
  • 소비자는 NULL을 수신하면 대기시간 이후 재시도하므로 지연시간이 증가한다.

그래서 레디스는 목록이 비어있을 경우 잠시동안 응답을 막을 수 있는 Bloking 커맨드로 BRPOP, BLPOP을 제공한다. 즉, 새로운 요소가 리스트에 추가되거나 사용자가 지정한 시간 초과에 도달했을 때에만 소비자에게 응답한다. 예를 들어 다음과 같이 사용할 수 있다.

이는 리스트에 아이템이 없을 경우 일단 대기하지만, 5초 후에도 데이터가 들어오지 않았을 경우에 응답하라는 것을 의미한다. 0을 timeout으로 설정한 경우 아이템이 생기기까지 영원히 기다릴 수 있으며, 동시에 여러 리스트에서 대기하고 첫번째 리스트가 아이템을 수신할 때 알림을 받을 수 있도록 여러 목록을 지정할 수도 있다.

BRPOP을 사용하기 위해 다음과 같은 사항을 참고하자.

  • 클라이언트 대기 목록에는 순서가 있다. 리스트에 아이템이 들어오면 먼저 블록된 클라이언트가 먼저 가져간다.
  • 리턴된 값은 RPOP과는 다르다. BRPOP과 BLPOP은 여러 목록에서 리스트를 기다리도록 할 수 있기 때문에 키의 이름도 필요해서 2-요소 배열이다.
  • BRPOP list1 list 2 0: list1과 list2 중 먼저 값이 들어오는 것에서 데이터를 받겠다.
  • 결과값: list1 에서 c를 받았다.

목록과 차단 방법에 대해서는 다음 사항을 알아야 한다.

  • RPOPLPUSH를 사용해서 원형큐를 만들 수 있다.
  • BRPOPLPUSH라는 명령의 차단 변형도 존재한다.

Automatic creation and removal of keys

지금까지 예제에서는 아이템을 푸시하기 전에 빈 리스트를 생성하거나, 더이상 아이템이 없는 빈 리스트를 제거하는 과정이 없었다. 아이템이 없을 때 키를 삭제하거나 키가 존재하지 않는 경우 빈 리스트를 생성하는건 레디스의 내부 기능이다. 예를 들어 LPUSH를 사용하여 키에 아이템을 추가한다고 하자.

이 내용은 리스트에만 국한된 건 아니고 multiple elements (스트림, 셋, 정렬된 셋 등)에 모두 관련된 내용이다.

기본적으로 레디스 내부적으로 아래 세가지 규칙으로 데이터를 생성한다.

1. 아이템을 추가할 때 키가 없다면 추가 전에 빈 데이터타입을 먼저 생성한다.

하지만 아래 예제처럼 키가 이미 존재하는 경우에는 타입에 맞지 않는 커맨드를 실행할 수 없다.

2. 제거하는 아이템이 키에 남은 마지막 아이템이었다면 아이템 삭제 이후 키도 자동으로 삭제한다. (스트림 타입은 이 규칙에 대해 유일한 예외)

리스트에서 모든 값이 pop되면 해당 key는 자동적으로 삭제된다.

3. 존재하지 않는 키에 대해 LLEN(리스트의 길이를 반환하는 커맨드)과 같은 읽기 전용 명령이나 아이템을 삭제하는 것과 같은 명령어를 사용하면 비어있는 데이터타입처럼 결과값을 반환한다.

Redis Hashes

Redis의 Hash는 field-value 쌍을 사용한 일반적인 해시를 구현한다.

Hash는 객체를 표현할 때 편리하다. 필드의 갯수에는 제한이 없으므로 여러 방법으로 사용이 가능하다.

HMSET 명령은 Hash의 여러 필드를 한번에 설정하고, HGET은 단일 필드를 검색한다. HMGETHGET과 비슷하지만 값의 배열을 리턴한다.

HINCRBY 와 같이 개별 아이템에 대한 조작 커맨드도 있다.

작은 해시(작은 값을 가진 몇가지의 요소를 포함)는 특별한 방식으로 인코딩되어 메모리를 효율적으로 운영할 수 있다.

Redis Sets

Set은 정렬되지 않은 문자열의 모음이다. SADD커맨드는 Set에 새 아이템을 추가한다. 주어진 아이템이 이미 존재하는지 테스트하거나, 교차, 합집합, 차집합 등의 기능을 수행할 수 있다.

위의 예제에서 세가지 아이템을 Set에 넣고 모든 요소를 반환한 결과에서 데이터가 정렬되어 있지 않음을 확인할 수 있다.

멤버십을 확인하는 기능을 셋에 이미 존재하는지를 확인하는 간단한 커맨드로 쉽게 구현할 수 있다.

3은 셋의 구성원이지만 30은 아니다.

셋은 객체 간의 관계를 표현할 때 좋다. 셋을 이용하여 쉽게 태그를 구현할 수 있다.

이 문제를 모델링하는 가장 간단한 방법은 태그를 지정할 모든 객체에 대해 설정하는 것이다. 셋에는 객체와 관련된 태그의 ID를 포함한다.

예를 들어 뉴스 기사에 태그를 지정할 때, 기사ID 1000에 태그ID 1,2,5,77이 태그된 경우, 셋에서 이런 태그ID를 뉴스 아이템과 연관지을 수 있다.

주어진 태그로 태그가 지정된 모든 뉴스 목록을 얻을 수도 있다.

이 객체에 대한 모든 태그를 얻는 것은 간단하다.

올바른 레디스의 명령을 이용하여 쉽게 구현할 수 있는 기능이 있다. 예를 들어 1,2,10,27 태그가 함께 있는 모든 객체 목록을 원할 수도 있다. SINTER 커맨드를 사용해서 이 작업을 수행할 수 있다. 이 커맨드는 서로 다른 셋간의 교집합을 수행한다.

교집합 뿐만 아니라 합집합, 차집합, 랜덤추출 등의 기능을 사용할 수 있다.

SPOP은 아이템을 추출하는 명령어이고, 특정 문제를 모델링 하는 데에 편하다. 예를 들어 웹 기반 포커 게임을 구현하기 위한 덱을 셋으로 표현할 수 있다. 아래는 ©lubs, (D)iamonds, (H)earts, (S)pades 로 간략하게 표현한 예이다.

이제 각 플레이어에게 5장의 카드씩을 제공해야 한다. SPOP 명령어는 랜덤으로 아이템을 뽑아서 클라이언트로 반환하므로 이 경우에 완벽한 작업이다.

다음 판에 다시 sadd를 할 필요 없이 이 덱을 그대로 쓰기 위해 지금 상태으 복사본을 만들어둘 수 있다. 이는 일반적으로 여러 셋간의 합집합을 수행하고 결과를 다른 셋에 저장하는 SUNIONSTORE를 사용하면 된다. 그러나 셋 하나의 합집합으므로 다음과 같은 명령어를 사용하면 된다.

이제 첫번째 플레이어에게 5장의 카드를 제공할 준비가 되었다.

이제는 셋 내의 아이템 수를 알아보는 명령어를 사용하기에 좋은 타이밍이다. 이를 집합의 이론에서 카디널리티라고 부르는 경우가 많기 때문에, 커맨드의 이름이 SCARD이다.

셋에서 아이템을 제거하지 않고 임의의 요소를 가져와야 할 경우에는 SRANDMEMBER 커맨드를 사용하면 된다. 이 때 반복되는 아이템과 반복되지 않는 아이템을 모두 반환하는 기능도 존재한다.

Redis Sorted sets

정렬된 셋은 해시와 셋의 혼합과 비슷한 타입이다. 셋과 마찬가지로 정렬된 셋은 반복되지 않는 고유의 문자열 요소로 구성되므로, 어떤 의미에서는 정렬된 셋도 셋이다.

하지만 셋은 순서가 없지만 정렬된 셋의 모든 요소는 score라는 값과 연결된다. 이 때는 모든 요소가 값과 연결되므로 해시와 유사하다고 볼 수도 있다.

그리고 정렬된 셋의 아이템은 순서대로 가져올 수 있다. (요청시 정렬되지 않을 수 있음. 순서는 정렬된 셋을 나타내는 데 사용되는 데이터 구조의 고유성)

아이템은 다음의 규칙에 의해 정렬된다.

  • A와 B의 점수가 다를 때 A.score > B.score 라면 A>B
  • A와 B의 점수가 같을 때 A 문자열이 사전순으로 B보다 빠를 경우 A>B. 정렬된 집합에는 고유 요소만 있으므로 A와 B문자열은 같을 수 없다.

선택된 해커 이름을 정렬된 셋으로 추가한 간단한 예를 확인해보자. 생년월일이 score 이다.

ZADD이 SADD 와 비슷해보이지만, 아이템 앞에 score를 추가로 입력해야 한다는 점에서 다르다. ZADD도 마찬가지로 가변적이어서 여러개의 score-value 쌍을 한번에 추가할 수 있다.

정렬된 셋을 사용하면 실제로 해킹된 해커 목록이 정렬되어 있기 때문에 해커 목록을 반환하는 것은 쉽다.

NOTE: 정렬된 셋은 스킵 목록과 해시테이블을 모두 포함하는 이중 포트 데이터 구조를 통해 구현되므로 아이템을 추가할 때마다 O(log(N)) 연산을 수행한다. 그러나 정렬된 요소를 요청할 때에는 이미 정렬되어 있으므로 정렬 작업을 수행할 필요가 없다.

NOTE: 0과 -1은 각각 인덱스 0에서 마지막 인덱스를 뜻한다. (-1은 LRANGE 명령의 경우와 동일하게 작동한다.)

만약 이 순서를 반대로, 어린 순서대로 호출하고 싶을 때에는 어떻게 해야 할까? 이 때에는 ZRANGE 대신 ZREVRANGE를 사용하면 된다.

WITHSCORE 옵션을 사용해서 score도 함께 반환할 수 있다.

Operating on ranges

정렬된 셋은 더 강력한 특징을 가지고 있다. 바로 범위로 아이템을 조작할 수 있는 것이다. 예를 들어 1950년까지 태어난 모든 해커들을 호출하고 싶을 때 ZRANGEBYSCORE 커맨드를 사용하면 된다.

레디스에서 음의 무한대(-inf) 부터 1950사이의 score를 가진 모든 요소를 반환하도록 했다.(마지막 요소도 포함된다)

또한 다양한 아이템을 범위로 삭제할 수 있다. 이 셋에서 1940년부터 1960년 사이에 태어난 모든 해커를 삭제할 수 있다.

ZREMRANGEBYSCORE은 (커맨드명은 별로지만) 제거된 요소의 수를 반환할 때 유용하게 사용된다.

또 정렬된 셋에서 유용하게 사용될 수 있는 커맨드는 RANK를 가져오는 기능이다. 정렬된 셋에서 해당 아이템의 포지션을 뽑아낼 수 있다.

ZREVRANK 커맨드는 모든 요소를 내림차순으로 정렬해서 순위를 얻기 위해 사용할 수 있다.

Lexicographical scores(사전순 비교)

정렬된 셋의 아이템이 모두 같은 스코어로 입력되었다고 가정하고 사전순으로 새로 범위를 조회할 수 있는 기능이 존재한다.

이 기능을 사용하는 주요 커맨드는 ZRANGEBYLEX, ZREVRANGEBYLEX, ZREMRANGEBYLEX, ZLEXCOUNT이다.

예를 들어 유명한 해커 목록을 다시 추가하지만, 이번에는 모든 아이템을 스코어 0점으로 적용한다.

정렬된 셋의 정렬 규칙에 의해, 모두 사전순으로 정렬된 상태이다.

ZRANGEBYLEX를 사용해서 사전순으로 아이템을 뽑아올 수 있다.

모든 범위는 첫번째 문자에 의해 포함되거나 제외될 수 있으며, 무한대와 음수 무한대는 +와 — 문자열을 사용한다.

이 기능은 정렬된 셋을 일반 인덱스로 사용할 수 있다는 점에서 유용하게 사용될 수 있다.

Updating the score: leader boards

정렬된 셋의 점수는 언제든지 업데이트 가능하다. 정렬된셋에 이미 포함된 요소에 대해 ZADD를 호출하면 O(log(N)) 의 시간 복잡도로 점수(와 위치)가 업데이트된다. 따라서 정렬된 셋은 많은 업데이트가 있을 때에도 적합하다.

이 특성으로 인해 일반적인 사용 사례는 리더보드이다. 일반적인 응용 프로그램에서 상위 N명의 사용자와 리더보드의 사용자 순위를 표시하기 위해 높은 점수로 정렬된 사용자를 가져와 순위를 정하는 기능이다.

Bitmaps

비트맵은 실제 데이터 유형이 아니라 문자열 유형에 정의된 일련의 비트 지향 연산이다. 문자열은 binary-safe 한 blob이고 최대 길이는 512MB이므로 최대 232개의 다른 비트를 설정할 수 있다.

비트 연산은 한개의 비트를 1이나 0으로 설정하는 상수시간의 연산과, 범위 내에서 비트의 갯수를 세는것과 같은 그룹 단위의 연산 이렇게 크게 두 종류의 연산으로 나눌 수 있다.

비트맵의 가장 큰 장점 중 하나는 정보를 저장할 때 공간을 크게 절약할 수 있다는 것입니다. 예를 들어, 사용자 ID가 증분되어 저장될 때 시스템의 512MB만 사용해서 40억 명의 단일 비트 정보를 기억할 수 있다.

비트는 SETBIT 및 GETBIT 커맨드를 이용하여 설정, 조회한다.

SETBIT 커맨드는 첫 번째 인수로 비트 인덱스를 지정하고, 두 번째 인수로 설정할 값(1이나 0)을 사용한다. 지정한 인덱스가 현재 문자열 길이를 벗어났다면 커맨드가 자동으로 문자열 길이를 늘린다.

GETBIT은 지정된 인덱스에서 비트 값을 반환한다. 범위를 벗어난 비트는 항상 0으로 간주된다.

비트 그룹에서 작동하는 커맨드는 세가지가 있다.

  • BITOP은 다른 문자열간에 비트 단위 연산을 수행한다. AND, OR, XOR,
    NOT을 사용할 수 있다.
  • BITCOUNT는 1로 설정된 비트 개수를 반환한다.
  • BOTPOS는 지정된 값이 0이거나 1인 첫 번째 비트를 찾는다.

BITPOS와 BITCOUNT는 문자열의 전체 길이에 대해서도 적용 가능하지만, 일부 바이트 범위를 지정해서 실행할 수 있다. 다음은 BITCOUNT의 간단한 예제이다.

비트맵의 일반적인 사용 사례는 다음과 같다.

  • 모든 종류의 실시간 분석
  • 오브젝트 ID와 관련된 bool 정보를 고성능이지만 적은 공간을 사용해서 저장할 때

예를 들어, 웹사이트 사용자의 가장 긴 일일 방문 횟수를 알고 싶다고 가정해보자. 웹사이트를 만든 날은 0으로 설정하고 사용자가 웹사이트를 방문할 때마다 SETBIT으로 비트를 설정한다. 비트 인덱스는 현재 유닉스 시간에서 초기 오프셋을 빼고, 하루의 초 수로 나눈다. (3600*24)

이렇게 하면 각 사용자마다 매일 방문 정보가 포함된 작은 문자열이 생긴다. BITCOUNT를 사용하면 특정 사용자가 웹사이트를 방문한 일수를 쉽게 얻을 수 있으며, BITOPS 호출을 몇번 하거나 비트맵 클라이언트를 가져와서 분석하면 가장 긴 방문 횟수를 쉽게 계산할 수 있게 된다.

HyperLogLogs

HyperLogLog는 고유한 것을 계산하기 위해 사용되는 확률적 데이터 구조이다. (기술적으로 집합의 카디널리티를 추정하기 위함) 일반적으로 고유 항목을 계산하려면 계산하려는 항목 수에 비례하는 메모리가 필요하다. 여러번 계산하지 않도록 과거의 항목을 기억해야 하기 때문이다. 그러나 메모리를 정밀하게 교환하는 알고리즘이 있고, 이를 레디스로 구현했을 때의 오차는 1% 미만으로 측정되었다. 이 알고리즘의 장점은 더이상 계산된 항목 수에 비례하여 메모리 양을 늘릴 필요가 없고, 일정한 양의 메모리를 사용할 수 있다는 것이다. 데이터의 크기는 최악의 경우가 12KB이고, 아이템이 별로 없는 HyperLogLog의 경우 더 작아질 수 있다.

레디스에서 HLL은 기술적으로 다른 데이터지만 레디스 스트링으로 인코딩되어, GET 사용시 HLL을 정렬되어 호출하고, SET 사용시는 재정렬되어 다시 서버에 저장된다.

개념적으로 HLL API는 셋을 사용해서 동일한 작업을 수행하는 것과 같다. 모든 아이템을 셋으로 SADD하고 SCARD를 사용하여 셋 내의 아이템 수를 확인한다. SADD는 기존 요소를 다시 추가하지 않기 때문에 고유하다.

데이터 구조에는 실제 아이템을 포함하지 않는 상태만 포함되므로 실제로 HLL에 항목을 추가하지는 않지만 API는 동일하다.

새로운 아이템을 만날 때마다 PFADD를 사용하여 카운트에 추가한다. 지금까지 PFADD로 추가된 고유한 요소의 근사값을 검색할 때에는 PFCOUNT를 사용한다.

이 데이터구조의 사용 예로는 매일 사용자가 검색한 고유한 쿼리를 계산하는 것 등이 있을 수 있다.

--

--