Back-End/Redis

Redis 자료구조 활용 사례

gilbert9172 2024. 12. 18. 22:24

 

❐ Description


Redis의 자료구조를 적절히 활용해 애플리케이션 성능을 향상 시키며,

동시에 개발의 단순성과 편의성을 증대할 수 있는 방법에 대해서 알아보자.

+ ) 실무에서 발생할 수 있는 요구 사항을 생각해서 직접 예제를 만들고 실습까지 해보자.

 

 

 

❐ sorted set을 이용한 사례


ChatGPT에게 예제 데이터를 만들어 달라고 했다.

Redis의 자료구조 sorted set으로 리더보드 예시를 만들꺼야.
2024년 12월 1일 부터 18일까지, 예제 데이터를 만들어 줘.

template : ZADD daily-score:<yyyymmdd> <point> user:<id>
각 요일마다 데이터수는 10개 이상이고 같은 날에 user의 id는 중복될 수 없어.(다른 날은 상관 없어)

 

1. 실시간 리더보드 & 랭킹 합산

sorted set은 저장된 데이터를 오름차순으로 반환하는데, 이를 응용하면 실시간 리더보드를 쉽게 구현할 수 있다.

REV 커맨드를 추가하면 내림차순으로 데이터를 반환한다.
127.0.0.1:6379> ZRANGE daily-score:20241211 0 -1 WITHSCORES
더보기
user:475
29
user:284
110
user:195
163
user:344
445
user:19
531
user:325
561
user:252
736
user:375
744
user:115
808
user:471
873

 

 

또한, Redis에서는 랭킹 합산과 같은 연산은 `ZUNIONSTORE` 커맨드를 사용해 간단하게 구현할 수 있다.

ZUNIONSTORE <생성할 키 이름><합산할 키 갯수><합산할 키>...<합산할 키>

 

🧩 단순 합산 랭킹 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

ChatGPT가 만들어준 예시 중, 7일(12월1일 ~ 7일)의 랭킹을 합산해봤다.

7일치 데이터 합산
127.0.0.1:6379> ZUNIONSTORE weekly-score:12-1 7 daily-score:20241201 
daily-score:20241202 daily-score:20241203 daily-score:20241204 
daily-score:20241205 daily-score:20241206 daily-score:20241207
12.01 ~ 12.07 기간동안 1등 ~ 10등 조회
127.0.0.1:6379> ZRANGE weekly-score:12-1 0 9 WITHSCORES REV
더보기

user:53의 데이터 검증 : (788 + 740) = 1528

user:52
1528
user:383
1201
user:485
1071
user:484
1013
user:174
999
user:331
935
user:298
899
user:384
897
user:190
877
user:147
865

 

 

🧩가중치 합산 랭킹 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

 

가중치를 부여하기 위해선 `weights` 커맨드를 사용하면 된다.

ZUNIONSTORE weekly-score:12-mon 3 
daily-score:20241202 daily-score:20241209 daily-score:20241216 
WEIGHTS 1 2 1
ZRANGE weekly-score:12-mon 0 9 REV WITHSCORES
더보기
✅ user:106 데이터 검증 
12월 9일 점수 = 989점  
가중치 = 2
결과 = 989 * 2 = 1978
user:106
1978
user:361
1910
user:295
1910
user:82
1637
user:455
1510
user:332
1430
user:395
1172
user:30
954
user:484
937
user:67
922

 

 

2. 최근 검색 기록

실습을 위해 ChatGPT를 활용해서 예제를 직접 만들어 봤다.

127.0.0.1:6379> ZADD search-keyword:123 20241206200753 핸드폰
127.0.0.1:6379> ZADD search-keyword:123 20241206041701 노트북
127.0.0.1:6379> ZADD search-keyword:123 20241201090857 청소기
127.0.0.1:6379> ZADD search-keyword:123 20241210021952 에어팟
127.0.0.1:6379> ZADD search-keyword:123 20241217121701 책상
127.0.0.1:6379> ZADD search-keyword:123 20241211112843 의자
127.0.0.1:6379> ZADD search-keyword:123 20241207213755 모니터
127.0.0.1:6379> ZADD search-keyword:123 20241209044331 키보드
127.0.0.1:6379> ZADD search-keyword:123 20241212233922 헤드셋
127.0.0.1:6379> ZADD search-keyword:123 20241204081511 스피커

 

 

🧩 ZRANGE ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

 

최근 검색 기록을 조회해보면 다음과 같다.

127.0.0.1:6379> ZRANGE search-keyword:123 0 4 REV
책상
헤드셋
의자
에어팟
키보드

 

여기서 사용자가 에어팟을 추가로 다시 검색했다고 가정 해보자.

ZADD search-keyword:123 20241217173413 에어팟

 

그리고 다시 최근 검색 기록을 조회해보면 `에어팟`이 가장 상위에 위치한 것을 확인할 수 있다.

127.0.0.1:6379> ZRANGE search-keyword:123 0 4 REV
에어팟
책상
헤드셋
의자
키보드

 

최대로 보관할 수 있는 검색 기록이 5개라면 어떻게 될까?

ZADD search-keyword:123 20241217203413 아이폰

 

 

🧩 ZREMRANGEBYRANK ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

 

물론 `ZRANGE`를 사용해서 갯수를 필터링 해줄 수 있지만, 불필요하게 많은 데이터가 저장이 될 것이다.

이럴 때는 ZREMRANGEBYRANK 커맨드를 사용하면 불필요한 데이터 저장을 방지할 수 있다.

 

최근 검색 기록 5개 초과
127.0.0.1:6379> ZRANGE search-keyword:123 0 -1
청소기
노트북
핸드폰
에어팟
책상
아이폰
sorted set의 minus-index를 사용해서 가장 오래된 검색 기록 제거
127.0.0.1:6379> ZREMRANGEBYRANK search-keyword:123 -6 -6
1
결과적으로 O(n)으로 항상 5개의 검색결과 유지
127.0.0.1:6379> ZRANGE search-keyword:123 0 -1
노트북
핸드폰
에어팟
책상
아이폰

 

 

3. 태그  기능

만약 내가 채용공고 사이트를 운영하는 사람이라면, 사용자의 기술 스택을 Redis의 tag 기능을 사용해서

구현해볼 수 있을 것 같다. 

 

🧩 사용자를 기준으로 태그 추가 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

127.0.0.1:6379> SADD user:123:tags springboot jpa redis
127.0.0.1:6379> SADD user:124:tags react javascript
127.0.0.1:6379> SADD user:125:tags redis express node.js

127.0.0.1:6379> SMEMBERS user:125:tags
redis
express
node.js

 

 

🧩 태그를 기준으로 사용자 추가 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

127.0.0.1:6379> SADD tag:springboot:user 100
127.0.0.1:6379> SADD tag:springboot:user 200

127.0.0.1:6379> SMEMBERS tag:springboot:user
100
200

 

 

🤔 현재 가입한 사용자가 가장 많이 보유한 스킬셋은? ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

위와 같은 기능을 구현해야 한다면, sorted set을 응용하면 될 것으로 생각된다. 

사용자 태그 선택
SADD tag:springboot:user 100
태그 popularity 갱신
ZINCRBY tag:popularity 1 springboot

 

 

4. 랜덤 데이터 추출

"이벤트 응모 결과 추첨" 같은 요구사항을 해결할 수 있다.

 

 

 

 

 

❐ 다양한 카운팅 방법


1. 좋아요 처리하기

set 자료구조를 사용하면 RDB의 부하를 방지할 수 있다.

 

 

2. 읽지 않은 메시지 카운팅하기

이 부분이 굉장히 흥미로웠다. 기존 wewoot 서비스에서 채팅은 sendbird를 사용했었고, 사용자가 읽지 않은

메시지의 수를 RDB로 관리했었다. 그러다보니 코드가 복잡해졌던 기억이 난다.

 

다음과 같이 Redis를 사용하면 복잡한 로직을 단순화할 수 있을 것 같다.

  • A ➔ B 메시지 발송 (B가 읽지 않은 메시자 카운트 +1)
  • B가 메시지를 읽는다. (B가 읽지 않은 메시자 카운트 -1)
  • B ➔ A 메시지 발송 (A가 읽지 않은 메시자 카운트 +1)
user 123과 124가 읽지 않은 채팅 데이터 셋팅
127.0.0.1:6379> HSET user:123:chat user:124 10 user:125 3
127.0.0.1:6379> HSET user:124:chat user:123 0
User123 : 메시지 읽고, User124에게 메시지 전송
# 메시지 읽고
127.0.0.1:6379> HSET user:123:chat user:124 0
127.0.0.1:6379> HGETALL user:123:chat
user:124
0

# 메시지 전송
127.0.0.1:6379> HINCRBY user:124:chat user:123 1

# User124 읽지 않은 메시지 카운트 증가
127.0.0.1:6379> HGETALL user:124:chat
user:123
1

 

 

3. DAU 구하기

책에서는 사용자의 PK가 0부터 순차적으로 증가하는 것을 바탕으로 설명하고 있는 것 같다.

그러나 PK 값을 다른 규칙을 적용해서 사용하면 BitMap을 사용해서 DAU를 구할 수 있을까?

➔ 아마도 Hashing을 통해 PK 값을 변경해줘야 할 것같다.

 

4. hyperloglog를 이용한 애플리케이션 미터링

필요할 때 다시 읽어보기.

 

 

 

 

 

❐ Geospatial Index를 이용한 위치 기반 애플리케이션 개발


TODO : 내 위치 기준 1km 반경에 있는 스탬프 장소 목록

 

geo set은 위치 공간 관리에 특화된 데이터 구조로, 각 위치 데이터는 경도와 위도의 쌍으로 저장된다.

이 데이터는 내부적으로 sorted set 구조로 저장된다.

GEOADD nowon 127.063205 37.656324 7
127.0.0.1:6379> GEOPOS nowon 23
127.05789059400558472
37.65012329205499952

 

lat, log 기준으로 반지름 500m 이내 장소
127.0.0.1:6379> GEOSEARCH nowon fromlonlat 127.051729 37.652193 byradius 500 m
26
14
12
2
15
16
13번 장소를 중심으로 사각형(좌우 400m, 상하 800m) 이내 장소  
127.0.0.1:6379> GEOSEARCH nowon FROMMEMBER 13 BYBOX 400 800 M
7
13

 

 

🤔 내 위치 근방 맛집 마커 찍기 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯

127.0.0.1:6379> GEOADD nowon 127.055194 37.657318 40
127.0.0.1:6379> HSET restaurant:40 name "현대옥" category "한식" rating "4.5"
  • 위와 같이 GEOADD에는 PK만 저장하고, 추가적인 정보는 Redis의 Hash 자료구조로 관리한다.
  • 장소에 대한 추가적인 정보가 필요할 경우 RDB에서 Place Entity를 조회한다.

이렇게 하면 RDB 접근은 최소화하고 빠르게 사용자에게 근처 맛집 정보를 제공해줄 수 있을 것 같다.