0. 들어가기 전
이전에 Redis에 대해서 간단하게 정리해봤었습니다.
https://ksh-coding.tistory.com/127
해당 글 이후에 여러 기능에서 Redis를 사용했고, 잘 동작하는 것을 확인할 수 있었습니다.
그러나, Redis를 사용할 때 추상화된 Spring Data Redis를 사용하면서 어떤 자료구조로 저장되는지,
어떤 자료구조가 해당 기능, 데이터에 적합한지, Redis에 어떤 자료 구조들이 존재하는지에 대해 모르고 사용했었습니다.
그래서 이번 기회에 Redis에 다양한 자료구조들을 살펴보고 해당 자료구조는 어떤 데이터, 기능에 적합한지도 함께 알아보도록 하겠습니다.
Redis의 공식 문서에 나와 있는 자료구조 종류는 다음과 같습니다.
- Strings
- Lists
- Sets
- Hashes
- Sorted sets
- Geospatial
- Bitmaps
- Streams
- Bitfields
- Probabilistic
- Time series
이렇게 다양한 자료구조들이 존재하지만 결론적으로, Bold 처리한 7가지 자료 구조들만 이번에 알아보도록 하겠습니다.
Probabilistic과 Time series는 Extension Data Type으로 정의되고 있기 때문에 이번 포스팅에서 제외했습니다.
또한 Streams와 Bitfields는 이해하기에 조금 힘들어서 이번에는 제외했습니다.
나중에 사용하게 되는 일이 생긴다면 하나의 포스팅으로 다뤄서 알아봐야겠습니다!
Redis의 Key-Value 구조
먼저, 자료구조들을 알아보기 전에 공통적으로 적용되는 Redis의 Key-Value 구조를 이해해야 합니다.
처음에 Redis가 Key-Value 기반의 저장소라는 것을 인지하고 다양한 자료구조들이 있다는 점을 생각했을 때
다양한 자료 구조들이 Key-Value 기반으로 Key, Value에 모두 적절히 구성된다고 생각했습니다.
하지만, Redis의 자료구조는 이러한 구조가 아니라 Value에 해당하는 Object로 자료구조를 사용하는 것이었습니다.
Redis의 Key는 무조건 String 문자열이고, 이에 해당하는 Value에 다양한 자료구조를 저장하는 느낌으로 이해하고 가봅시다.
사용하는 예제들은 대부분 Redis의 공식문서에서 인용 & 디벨롭해서 사용하도록 하겠습니다.
1. Strings
Redis 공식 문서에서 Strings 자료 구조는 다음과 같이 언급하고 있습니다.
Redis strings store sequences of bytes, including text, serialized objects, and binary arrays.
As such, strings are the simplest type of value you can associate with a Redis key.
- strings 자료구조는 텍스트, 직렬화된 객체, 바이너리 배열을 포함한 sequences of bytes를 저장합니다.
- 따라서, strings 자료구조는 Redis key에 연결할 수 있는 가장 간단한 자료구조입니다.
1-1. Strings 기본 커맨드
기본적으로 Strings 자료구조는 SET, GET 커맨드를 통해 값을 저장하고 가져올 수 있습니다.
> SET bike:1 Deimos
OK
> GET bike:1
"Deimos"
- SET 커맨드를 통해 'bike:1'를 Key로, Strings 자료구조인 'Deimos'를 Value로 저장
- GET 커맨드를 통해 'bike:1' Key에 해당하는 Value 가져오기
1-2. Strings 활용 예시
Example 1. 방문자 수 조회
- 웹 사이트의 방문자 수를 저장하는 상황
redis> INCR visitorCount
(integer) 1
redis> INCR visitorCount
(integer) 2
redis> INCR visitorCount
(integer) 3
redis> GET visitorCount
"3"
- 웹 사이트의 방문자 수를 Strings 자료구조로 Value로 저장
- 방문 시마다 INCR 커맨드를 사용하여 숫자 증가
- INCR 커맨드는 Atomic하게 동작하여 동시성과 관련한 데이터 정합성 문제가 발생하지 않습니다.
- 따라서, 빈번하게 1씩 증가하는 데이터에 INCR 커맨드를 사용해서 Strings 자료구조로 저장하는 것으로 활용할 수 있습니다.
Example 2. 시간별 방문자 수 조회
예시 상황은 Example 1과 거의 동일하지만, 추가 요구사항으로 다음과 같은 요구사항이 있습니다.
- 웹 사이트의 방문자 수는 1시간 단위로 집계
- 1시간마다 방문자 수를 다시 0으로 초기화
이 상황에서 GETSET 커맨드를 사용해서 다음과 같이 구현할 수 있습니다.
redis> INCR visitorCount
(integer) 1
redis> INCR visitorCount
(integer) 2
redis> INCR visitorCount
(integer) 3
redis> GETSET visitorCount "0"
"3"
- 방문자 수 INCR 커맨드로 방문 시마다 1씩 증가
- 1시간이 되면 GETSET 커맨드로 방문자 수를 0으로 초기화하고, 가져오는 값이 이전 시간까지 방문한 방문자 수
Example 3. 월별 동선 데이터 캐싱
- 월별로 조회되는 동선 데이터를 캐싱해야 하는 상황
이 상황에서 Strings 자료구조로 String으로 직렬화된 JSON 객체를 저장하여 캐싱할 수 있습니다.
redis> SET addRoute:2024-06 {2024년 6월에 생성된 동선 데이터를 직렬화한 객체}
OK
redis> GET addRoute:2024-06
{2024년 6월에 생성된 동선 데이터를 직렬화한 객체}
2. Lists
Redis 공식 문서에서 Lists 자료 구조는 다음과 같이 언급하고 있습니다.
- Redis lists are implemented via Linked Lists.
- Redis Lists are implemented with linked lists because for a database system it is crucial to be able to add elements to a very long list in a very fast way.
- Redis의 Lists 자료구조는 LinkedList로 구현된다.
- 데이터 삽입은 O(1)로 상당히 빠르다.
- 인덱스를 통한 데이터 조회는 O(N)으로 느리다.
- DB 시스템에서는 빠르게 요소들을 추가하는 것이 중요하기 때문에 Linked List로 구현
2-1. Lists 기본 커맨드
기본적으로 Lists 자료구조는 다음과 같은 커맨드가 있습니다.
- LPUSH, RPUSH : 리스트의 head/tail에 요소 저장
- LPOP, RPOP : 리스트의 head/tail의 요소 제거하고 반환
- LLEN : 리스트의 길이 반환
- LMOVE : 다른 리스트로 요소를 이동
- LRANGE : 범위에 해당하는 요소들 반환
- LTRIM : 범위에 해당하는 요소만 남도록 리스트 조절
- BLPOP, BRPOP : LPOP, RPOP과 동일하게 리스트의 head/tail 요소를 제거하고 반환, 하지만 만약 리스트가 비어있으면 타임아웃 시간동안 리스트에 PUSH될 때까지 대기
- BLMOVE : LMOVE와 동일하게 다른 리스트로 요소를 이동, 하지만 만약 리스트가 비어있으면 타임아웃 시간 동안 리스트에 PUSH될 때 까지 대기
> LPUSH bikes:repairs bike:1
(integer) 1
> LPUSH bikes:repairs bike:2
(integer) 2
> RPOP bikes:repairs
"bike:1"
> RPOP bikes:repairs
"bike:2"
2-2. Lists 활용 예시
Example 1. 1년(단위 기간) 동안의 일별 방문자 수 통계
- 웹 사이트의 방문자 수를 저장하는데, 최근 1년 단위의 그래프로 일별로 방문자 수가 얼마나 있었는지 조회하려는 상황
- 오늘 기준 최근 1년 단위의 방문자 수이므로, 데이터는 365개만 존재하면 된다.
이 경우에 LTRIM과 LRANGE 커맨드를 통해 손쉽게 구현할 수 있습니다.
- 우선 하루의 끝에 스케줄링을 통해 방문자 수를 RPUSH를 통해 저장합니다.
- 이때, RPUSH 이후에 LTRIM을 통해 데이터 개수를 365개로 유지합니다.
- 조회 시에는 LRANGE를 통해 1년 간의 데이터를 조회합니다.
redis> RPUSH recently:visitor:year 10
(integer) 1
redis> LTRIM recently:visitor:year 0 364
"OK"
redis> RPUSH recently:visitor:year 30
(integer) 2
redis> LTRIM recently:visitor:year 0 364
"OK"
... (365개 저장 & TRIM)
redis> LRANGE recently:visitor:year 0 -1
1) "10"
2) "30"
...
이런 식으로, List에 방문자 수를 하루마다 일별로 저장하고
LTRIM을 통해 1년 데이터가 쌓인 후 다음 데이터가 쌓여도 최신 1년의 데이터를 유지할 수 있습니다.
Example 2. 백그라운드 작업을 위한 작업 큐 구현
Lists 자료구조를 활용해서 pub-sub 패턴을 구현한 Message Queue 형태로 사용할 수 있습니다.
따라서 애플리케이션에서 비동기적으로 백그라운드 작업을 처리하는 곳에 활용할 수 있습니다.
기본적으로 PUSH, POP을 통해 pub-sub 패턴을 구현합니다.
이때 메시지를 소비할 때 BLPOP, BRPOP을 활용하여 메시지가 없으면 대기하면서 효율적으로 활용할 수 있습니다.
redis> RPUSH back:ground:job "1" (백그라운드 작업 ID)
(integer) 1
redis> BRPOP back:ground:job
"OK"
3. Sets
Redis 공식 문서에서 Sets 자료 구조는 다음과 같이 언급하고 있습니다.
A Redis set is an unordered collection of unique strings (members).
- Set은 정렬되지 않은 Unique한 요소의 집합이다.
3-1. Sets 기본 커맨드
기본적인 Sets 자료구조는 다음과 같은 커맨드가 있습니다.
- SADD : Set에 새로운 멤버를 추가
- SREM : Set에서 특정한 멤버를 제거
- SISMEMBER : 특정 멤버가 Set에 있는지 판단 (있으면 1, 없으면 0 반환)
- SINTER : 여러 Set에 공통적으로 속하는 멤버 반환
- SCARD : Set의 크기 반환
> SADD bikes:racing:france bike:1
(integer) 1
> SADD bikes:racing:france bike:1
(integer) 0
> SADD bikes:racing:france bike:2 bike:3
(integer) 2
> SADD bikes:racing:usa bike:1 bike:4
(integer) 2
3-2. Sets 활용 예시
Example 1. 블로그에 접근하는 고유 IP 추적
- 블로그에 방문하는 고유한 사용자가 몇 명인지 통계를 내고 싶은 상황
- 이때, 아이디가 여러개거나 블로그를 10번 접근해도 1명의 사용자로 통계 내고 싶은 상황
이때는 블로그 접근 시마다 사용자의 고유 IP를 저장하여 통계를 낼 수 있습니다.
redis> SADD blog:visitor:IP "xxx.xxx.xxx"
(integer) 1
redis> SADD blog:visitor:IP "yyy.yyy.yyy"
(integer) 1
redis> SCARD blog:visitor:IP
(integer) 2
4. Hashes
Redis 공식 문서에서 Hashes 자료 구조는 다음과 같이 언급하고 있습니다.
Redis hashes are record types structured as collections of field-value pairs.
- Hashes는 field-value 쌍의 집합으로 이루어진 record 타입입니다.
아래의 예시를 보면 Hashes를 이해할 수 있습니다.
> HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972
(integer) 4
> HGET bike:1 model
"Deimos"
> HGET bike:1 price
"4972"
> HGETALL bike:1
1) "model"
2) "Deimos"
3) "brand"
4) "Ergonom"
5) "type"
6) "Enduro bikes"
7) "price"
8) "4972"
- bike:1 이라는 Key에 Hashes 자료구조를 사용한 값이 저장되어 있습니다.
- bike:1은 4개의 field-value 쌍을 가집니다.
- model - Deimos
- brand - Ergonom
- type - 'Enduro bikes'
- price - 4972
이처럼 여러 개의 field-value 쌍의 집합을 Hashes라고 합니다.
4-1. Hashes 기본 커맨드
기본적으로 Hashes 자료구조는 다음과 같은 커맨드가 있습니다.
- HSET : Hash에 한 개 이상의 필드 값들을 설정
- HGET : 하나의 주어진 필드에 해당하는 값을 반환
- HMGET : 한 개 이상의 주어진 필드에 해당하는 값들을 반환 (Multiple)
- HINCRBY : 주어진 Integer 필드의 값을 +- 변경
> HINCRBY bike:1:stats rides 1
(integer) 1
> HINCRBY bike:1:stats rides 1
(integer) 2
> HINCRBY bike:1:stats rides 1
(integer) 3
> HINCRBY bike:1:stats crashes 1
(integer) 1
> HINCRBY bike:1:stats owners 1
(integer) 1
> HGET bike:1:stats rides
"3"
> HMGET bike:1:stats owners crashes
1) "1"
2) "1"
4-2. Hashes 활용 예시
Example 1. 객체 저장
hashes는 하나의 Key에 다양한 field-value 쌍들을 저장할 수 있어서 객체를 구조적으로 저장하는 데에 적합한 자료구조입니다.
예를 들어, '닉네임'과 '로그인 ID'를 가지는 멤버 객체를 저장할 때 다음과 같이 Hashes에 구조적으로 저장할 수 있습니다.
redis> HSET member nickname "seongha" loginId "sh123"
(integer) 2
redis> HGET member nickname
"seongha"
redis> HGET member loginId
"sh123"
Example 2. 실시간으로 업데이트되는 여러 카운터 저장
- 태그들이 선택되면 태그 선택 횟수가 1씩 증가하고, 태그들의 선택 횟수를 저장해야하는 상황
이 경우에 Strings 자료 구조로 INCR 커맨드를 사용하여 태그마다 각각 카운터를 저장할 수도 있지만,
태그가 많아지는 경우 키의 개수가 엄청나게 많아질 수 있습니다.
따라서, 하나의 key에 태그별로 태그 이름-카운트 쌍을 지어서 구조적으로 저장할 수 있습니다.
redis> HSET tagCounter "데이트" 0 "맛집" 0
(integer) 2
redis> HINCRBY tagCounter "데이트" 1
(integer) 1
redis> HINCRBY tagCounter "맛집" 1
(integer) 1
redis> HGET tagCounter "데이트"
"1"
redis> HGET tagCounter "맛집"
"1"
5. Sorted Sets
Redis 공식 문서에서 Sorted Sets 자료 구조는 다음과 같이 언급하고 있습니다.
A Redis sorted set is a collection of unique strings (members) ordered by an associated score.
When more than one string has the same score, the strings are ordered lexicographically.
- Sorted Set은 연관된 score로 정렬된 고유한 멤버들의 집합이다.
- 한 가지 이상의 같은 score를 가진 멤버가 있을 때는, 멤버는 사전순으로 정렬된다.
추가적으로, Sorted Sets에 대해 다음과 같이 언급합니다.
You can think of sorted sets as a mix between a Set and a Hash.
However while elements inside sets are not ordered, every element in a sorted set is associated with a floating point value, called the 'score' (this is why the type is also similar to a hash, since every element is mapped to a value).
- Sorted Set에 대해 Set 자료구조와 Hash 자료 구조가 섞인 자료구조라고 생각할 수 있다.
- Sorted Set은 Set과 달리 'score'로 정렬되어 있는 자료구조이다.
- 해당 score가 각 요소마다 매핑되어 있기 때문에 Hash 자료 구조와 유사하다.
이렇게 Set과 Hash 자료 구조와 비교하며 Sorted Set을 이해하기 쉽게 설명하고 있습니다.
> ZADD racer_scores 10 "Norem"
(integer) 1
> ZADD racer_scores 12 "Castilla"
(integer) 1
> ZADD racer_scores 8 "Sam-Bodden" 10 "Royce" 6 "Ford" 14 "Prickett"
(integer) 4
5-1. Sorted Sets 기본 커맨드
기본적으로 Sorted Sets 자료구조는 다음과 같은 커맨드가 있습니다.
- ZADD : 새로운 멤버와 멤버의 score 추가, 이미 존재하는 멤버라면 score update
- ZRANGE : 주어진 범위의 오름차순으로 정렬된 멤버들 반환
- ZREVRANGE : 주어진 범위의 내림차순으로 정렬된 멤버들 반환
- ZRANK : 오름차순으로 정렬된 Sorted Set에서 주어진 멤버의 순위 반환
- ZREVRANK : 내림차순으로 정렬된 Sorted Set에서 주어진 멤버의 순위 반환
5-2. Sorted Sets 활용 예시
Example 1. 게임 랭킹 구현
- 게임이 진행되고 점수에 따른 랭킹(순위)을 보여줘야 하는 상황
redis> ZADD game 222 player1 111 player2 333 player3
(integer) 3
redis> ZREVRANGE game 0 -1
1) "player3"
2) "player1"
3) "player2"
- ZADD를 통해 플레이어의 점수들을 Sorted Set에 저장
- ZREVRANGE를 통해 점수 높은 순으로 플레이어 반환
Example 2. 오래 접속하지 않은 사용자 개인 정보 삭제
- 서비스에서 1년동안 접속하지 않은 사용자는 개인 정보를 삭제해야한다.
redis> ZADD member:last:access 1723010400(유닉스시간 m/s) member1
(integer) 1
redis> ZRANGEBYSCORE member:last:access -inf xxx(오늘으로부터 1년 전 유닉스 시간)
"member1"
- 사용자 접근 기록 저장 시에 접근한 유닉스 시간 저장
- 오래 접속하지 않은 사용자의 개인 정보를 삭제할 때, ZRANGEBYSCORE를 통해 1년 이상 접속하지 않은 멤버 조회
6. Geospatial
Redis 공식 문서에서 Geospatial 자료 구조는 다음과 같이 언급하고 있습니다.
Redis geospatial indexes let you store coordinates and search for them.
This data structure is useful for finding nearby points within a given radius or bounding box.
- Geospatial Index를 사용하면 좌표를 저장하고 검색할 수 있습니다.
- 해당 자료구조는 지정된 반경, 경계에서 가까운 지점을 찾는 용도로 유용합니다.
6-1. Geospatial 기본 커맨드
기본적으로 Geospatial 자료구조는 다음과 같은 커맨드가 있습니다.
- GEOADD : 지정된 Geospatial Index에 위치(경도, 위도 순)를 추가
- GEOSEARCH : 주어진 반경, 경계 내의 위치들을 반환
> GEOADD bikes:rentable -122.27652 37.805186 station:1
(integer) 1
> GEOADD bikes:rentable -122.2674626 37.8062344 station:2
(integer) 1
> GEOADD bikes:rentable -122.2469854 37.8104049 station:3
(integer) 1
6-2. Geospatial 활용 예시
Example 1. 현재 위치에서 반경 내의 자전거 대여소 찾기
- 자전거 대여 앱에서 사용자의 현재 위치를 기반으로 반경 5km 내의 자전거 대여소 가까운 순으로 찾기
redis> GEOADD bikes:rentable -122.27652 37.805186 station:1
(integer) 1
redis> GEOADD bikes:rentable -122.2674626 37.8062344 station:2
(integer) 1
redis> GEOADD bikes:rentable -122.2469854 37.8104049 station:3
(integer) 1
redis> GEOSEARCH bikes:rentable FROMLONLAT -122.2612767 37.7936847 BYRADIUS 5 km ASC
1) "station:2"
2) "station:1"
3) "station:3"
- GEOADD를 통해 위치(경도, 위도)와 멤버(요소) 저장
- GEOSEARCH를 통해 현재 위치를 기반으로 반경 5km 내의 자전거 대여소 가까운 순으로 찾기
- FROMLONLAT : 주어진 경도, 위도를 기반으로 검색
- BYRADIUS 5Km : 반경 5km로 검색
- ASC : 검색 결과를 주어진 위치에서 가까운 순에서 먼 순으로 정렬
7. Bitmaps
Redis 공식 문서에서 Bitmaps 자료 구조는 다음과 같이 언급하고 있습니다.
Bitmaps are not an actual data type, but a set of bit-oriented operations defined on the String type which is treated like a bit vector.
Since strings are binary safe blobs and their maximum length is 512 MB, they are suitable to set up to 2^32 different bits.
- Bitmap은 실제 자료 구조가 아니라 문자열을 비트 벡터처럼 다룰 수 있도록 하는 자료구조이다.
- String은 binary safe하고 최대 길이가 512MB이기 때문에, 2의 32승 비트까지 설정할 수 있다.
추가적으로, Bitmaps의 이점에 대해 다음과 같이 언급합니다.
One of the biggest advantages of bitmaps is that they often provide extreme space savings when storing information.
For example in a system where different users are represented by incremental user IDs, it is possible to remember a single bit information of 4 billion users using just 512 MB of memory.
- Bitmap의 가장 큰 장점 중 하나는 정보를 저장할 때 공간을 매우 크게 절약할 수 있다는 점이다.
- 예를 들면 사용자가 Auto Increment 되는 ID를 사용할 때 512MB의 메모리를 통해 최대 40억명의 단일 정보를 저장할 수 있다.
이처럼, Bitmap은 대용량의 단일 정보들을 저장하는 데 이점이 있는 것을 알 수 있습니다.
7-1. Bitmaps 기본 커맨드
기본적으로 Bitmaps 자료구조는 다음과 같은 커맨드가 있습니다.
- SETBIT : 주어진 offset(문자열)의 비트를 0 또는 1로 설정
- GETBIT : 주어진 offset(문자열)의 비트를 반환
7-2. Bitmaps 활용 예시
Example 1. 웹 사이트에서 사용자의 일일 최장 방문 일수 구하기
- 서비스 오픈 일자인 2024-04-01부터 사용자가 몇 일 연속 웹사이트에 방문했는지를 구해야한다.
* offset : (현재 유닉스 시간 - 2024-04-01의 유닉스 시간) / 하루의 초 수(3600 * 24)
redis> SETBIT member:1 128 1
redis> BITCOUNT member:1
(Integer) 10
- BITCOUNT를 통해 멤버의 총 방문 일 수를 구할 수 있다.
- 이후에, 클라이언트 측으로 비트맵을 가져와서 연속으로 1이 반복되는 최장 개수를 구하면 최장 방문 일수를 구할 수 있다.
Reference
https://redis.io/docs/latest/develop/data-types/
'DB > Redis' 카테고리의 다른 글
[Redis] EC2 환경에서 Docker에 Redis 설치 후 실행하기 (0) | 2024.01.15 |
---|---|
[DB] Redis란? (0) | 2023.12.17 |