📌 Redis Object 개요
Redis 메모리를 저장공간으로 사용하는 In-Memory Key-value 스토어 이다.
- 모든 데이터가 메모리에 저장되므로 빠른 읽기/쓰기가 가능
- 하지만 메모리는 유한한 자원이므로 효율적인 관리가 필수적
메모리를 효율적으로 관리할 수 있도록, Redis는 데이터를 Object로 정의하고있다.
Redis의 고성능과 효율적인 메모리 사용을 가능하기 위해 Object는 아래와 같은 역할을 한다.
- 데이터 표현
- 모든 키-값 쌍을 RedisObject로 통일하여 표현
- 다양한 데이터 타입(String, List, Hash 등)을 하나의 구조로 관리
- 메모리 최적화
- 데이터 특성에 따라 최적의 인코딩 방식을 자동 선택
- 작은 데이터는 압축된 형태로 저장
- 중복 데이터는 참조 방식으로 저장
- 성능 최적화
- 데이터 접근 패턴 추적 (LRU)
- 효율적인 메모리 회수 정책 지원
- 데이터 타입별 특화된 연산 지원
- 메모리 단편화 방지
📌 Redis Object 와 SDS (Simple Dynamic String)
► Object
Object는 실제 데이터가 어떻게 저장될지에 대한 정의이며. 아래와 같다.
typedef struct redisObject {
unsigned type:4; // 데이터 타입(4비트)
unsigned encoding:4; // 인코딩 방식(4비트)
unsigned lru:24; // LRU 시간정보 (24비트)
int refcount; // 참조카운트
void *ptr; // 실제 데이터 구조체를 가리키는 포인터
} robj;
- 필드 설명
- type: 실제 값(0:STRING, 1:LIST, 2:SET, 3:ZSET, 4:HASH)
- encoding: 실제 사용되는 인코딩 값들 (INT, EMBSTR, RAW, HASHTABLE, LISTPACK 등)
- lru (Least Recently Used): LRU 정보, 즉 마지막 접근 시간 저장 (메모리 부족시 오래된 데이터 삭제에 이용)
- refcount: 메모리 관리를 위한 참조 카운팅 (0이 되면 메모리 해제)
- refcount: 객체를 참조하는 횟수 (메모리관리를 위한 카운터, 0되면 메모리에서 해제, 데이터 공유 시 증가)
- ptr: 실제 데이터가 저장된 위치를 가리킴
► SDS (Simple Dynamic String)
SDS는 Redis가 문자열을 저장하기 위해 사용하는 기본 데이터 구조체이다.
길이 정보를 저장하고있으며, 버퍼 오버플로우를 방지하기 위해 메모리 남은 공간을 저장하고, 자동으로 메모리를 재 할당할 수 있도록 한다.
struct sdshdr {
uint32_t len; // 버퍼에 사용된 길이
uint32_t free; // 남은 버퍼 길이
char buf[]; // 실제 문자열 데이터
}
Object 필드 내 *ptr부분에서 SDS 혹은 실제 문자열 데이터를 가르키게 된다.
RedisObject (robj) │ └── ptr (pointer) ──→ SDS │ └───────→ 실제 문자열 데이터 |
즉, 어떤 데이터타입이나 인코딩을 사용하느냐에 따라 SDS를 사용하지 않을수도 있다.
Redis는 가장 효율적인 저장 방식을 자동으로 선택한다.
###################################
# 1. 정수 (int)
# RedisObject
# type: STRING
# encoding: INT
# ptr ──→ 직접 정수값 저장
127.0.0.1:6379> SET number 123
127.0.0.1:6379> OBJECT ENCODING number
"int" // SDS 사용하지 않음, 직접 정수 저장
###################################
# 2. 작은 문자열 (embstr)
# [RedisObject + SDS] (연속된 메모리)
127.0.0.1:6379> SET name "john"
127.0.0.1:6379> OBJECT ENCODING name
"embstr" // RedisObject와 SDS가 연속된 메모리에 할당
###################################
# 3. 큰 문자열 (raw)
# RedisObject → SDS
127.0.0.1:6379> SET bigtext "very...long...text..."
127.0.0.1:6379> OBJECT ENCODING bigtext
"raw" // 일반적인 SDS 사용
###################################
# 4. 리스트
# RedisObject → 자료구조 → SDS들
127.0.0.1:6379> RPUSH mylist "a" "b" "c"
127.0.0.1:6379> OBJECT ENCODING mylist
"quicklist" // 내부적으로 SDS 사용
###################################
# 5.Hash (listpack/hashtable)
# RedisObject
# type: HASH
# encoding: HASHTABLE
# ptr ──→ HashTable ──→ {
# key1: SDS1,
# key2: SDS2,
# ...
# }
127.0.0.1:6379> HSET user name "Tom" age "25"
127.0.0.1:6379> OBJECT ENCODING user
"listpack" // 작은 경우 listpack, 큰 경우 hashtable
► Object, SDS, Datatype, Encoding 관계
중요 컴포넌트들은 아래와 같은 관계를 가진다.
RedisObject → SDS
↓
Data Types (String, List, Hash, Set, Sorted Set)
↓
Encoding (Raw, Int, Embstr, Ziplist, Quicklist...)
비유를 하자면, Redis가 대형 선박이라면, Object는 용도별로 구분된 공간, SDS는 실제 데이터를 담은 박스라고 볼수있다.
구획(Object)은 컨테이너(SDS)를 어떻게 배치하고 관리할지 결정하고,
컨테이너(SDS)는 실제 화물(데이터)을 안전하게 보관한다.
대형 선박 (Redis)
│
├── 구획 (RedisObject)
│ │
│ ├── 일반 물품 보관소 (String Type)
│ │ ├── 컨테이너들 (SDS - raw/embstr)
│ │ └── 바닥에 놓인 작은 물품들 (정수 - int)
│ │
│ ├── 컨테이너 정렬소 (List Type)
│ │ └── 순서대로 정렬된 컨테이너들 (QuickList → SDS들)
│ │
│ ├── 컨테이너 분류소 (Hash Type)
│ │ └── 라벨링된 컨테이너들 (field-SDS, value-SDS 쌍)
│ │
│ ├── 컨테이너 중복제거소 (Set Type)
│ │ └── 유니크한 컨테이너들 (SDS들)
│ │
│ └── 컨테이너 우선순위소 (Sorted Set Type)
│ └── 점수가 매겨진 컨테이너들 (score-SDS 쌍)
📌 Redis 실습 준비하기
먼저 이해하면서 실습할 수 있도록 Redis를 준비해놓자 💪💪
# Redis 최신버전 실행
docker run --name redis -p 6379:6379 -d redis
# Redis CLI 접속
docker exec -it redis redis-cli
📌 Redis Data Type
► 데이터타입 이해하기
데이터 타입은 Object 내에서 type으로 정의되며 (unsigned type:4;) 숫자로 표현된다.
4비트로 표현되므로 최대 16가지 타입을 표현할 수 있다.
define OBJ_STRING 0 // 문자열
define OBJ_LIST 1 // 리스트
define OBJ_SET 2 // 셋
define OBJ_ZSET 3 // 정렬된 셋
define OBJ_HASH 4 // 해시
define OBJ_STREAM 6 // 스트림
Redis Data Type은 기본(Core) 데이터 타입과, 특수 데이터타입이 존재한다.
여기서 주의해야할 점은 기본 데이터 타입들만 RedisObject의 4비트 type필드로 표현된다는것이다.
특수데이터 타입 예시로 아래와 같은 타입이 있고, 모듈 시스템을 통해 확장 되거나 기본 데이터 타입을 기반으로 구현된다.
- Bitmap: 실제로는 STRING 타입의 특별한 처리
- Geospatial: 내부적으로는 ZSET 사용
- HyperLogLog: STRING 타입으로 구현
- Bloom filter: STRING 타입 기반
- Time series: 특별한 모듈로 구현
예를들자면, 아래처럼 구현된다.
# Bitmap은 실제로는 STRING
SETBIT mykey 7 1 # 내부적으로는 STRING 타입
# Geospatial은 ZSET 사용
GEOADD locations 13.361389 38.115556 "Palermo" # 내부적으로는 ZSET
# HyperLogLog도 STRING
PFADD visitors "user1" # 내부적으로는 STRING
Redis에서는 데이터타입을 직접 지정하는게 아니라 명령어를 통해 데이터 타입이 결정된다.
명령어의 첫 글자로 어떤 데이터 타입을 다루는지 알 수 있다. 자세한 내용은 아래에서 다룬다.
- String: 접두어 없음 (SET, GET)
- List: L 또는 R
- Set: S
- Sorted Set: Z
- Hash: H
- Stream: X
데이터 타입을 확인하려면 아래 명령어로 확인할 수 있다.
# 타입 확인 명령어
TYPE key명
► Core Data type
1. Strings (스트링/문자열)
- https://redis.io/docs/latest/develop/data-types/strings/
문자열 유형을 값으로 가지는 가장 기본적인 데이터 타입이다. (값은 최대 512MB, 큰문자열은 메모리 파편화 주의)
일반 문자열 뿐 만 아니라 숫자(정수/실수), JSON문자열, 바이너리 데이터도 저장이 가능하다.
가장 많이 활용되는 유형으로, Redis로 가장 많이 구현되는 캐시/세션으로 활용한다.
- 기본 커멘드
SET key value # 문자열 저장 SETNX # 키가 존재하지 않을때만 문자열 저장 (SET if Not eXists"의 약자, Lock구현에 쓰임) GET key # 문자열 검색 |
- 활용사례
- 캐시 (웹페이지, API응답, SQL쿼리 캐싱)
- 세션저장
- 카운터 (조회수, 좋아요 수)
- 락(Lock) 구현
- API 레이트 미터링
가장 널리 사용되는 쿼리 캐싱은 쿼리를 Hashing하여 Key로 저장하고, 값을 Value로 저장하는 형태로 구현한다.
즉, 쿼리 자체를 Hash해서 같은 쿼리를 실행하면 Hash값을 조회해서 값을 리턴하도록 한다.
Key = Hash(SQL)
Value = 쿼리결과
이건 나중에 기회가 있다면 다시한번 다루도록 한다.
👇👇 실습은 아래 더보기 클릭 👇👇
# 기본적인 GET/GET 활용
127.0.0.1:6379> set name "user01"
OK
127.0.0.1:6379> get name
"user01"
# 값 확인
127.0.0.1:6379> EXISTS name
(integer) 1
# 타입 점검
127.0.0.1:6379> type name
string
# SETNX 활용
127.0.0.1:6379> SETNX mykey "Hello"
(integer) 1 # 반환값: 1 (mykey가 존재하지 않아서 설정됨)
127.0.0.1:6379> SETNX mykey "World"
(integer) 0 # 반환값: 0 (mykey가 이미 존재함)
127.0.0.1:6379> GET mykey
"Hello"
# 값은 JSON으로, TTL 추가
127.0.0.1:6379> SET session:user01 "{\"user_id\":user01,\"username\":\"lina\",\"login_time\":\"2024-12-07T10:00:00Z\"}" EX 100
OK
# TTL 확인
127.0.0.1:6379> TTL session:user01
(integer) 94
# TTL 연장
127.0.0.1:6379> EXPIRE session:user01 3600
(integer) 1
127.0.0.1:6379> TTL session:user01
(integer) 3598
# 값 삭제
127.0.0.1:6379> DEL session:user01
(integer) 1
127.0.0.1:6379> GET session:user01
(nil)
2. Lists (리스트)
- https://redis.io/docs/latest/develop/data-types/lists/
List는 문자열을 순서대로 저장하는 데이터 타입이다.
양방향 연결 리스트로 구성되어있어 양쪽 끝에서 Push/Pop 가능하다.
- 기본 커멘드
LPUSH key value # 왼쪽(head)에 추가 RPUSH key value # 오른쪽(tail)에 추가 LPOP key # 왼쪽에서 제거 및 반환 RPOP key # 오른쪽에서 제거 및 반환 |
- 활용사례
- 메세지 큐, 작업 대기열
- 최근 게시물 목록
- 소셜 미디어 타임라인
Lists는 FIFO(First in, First Out) 큐 시스템으로 사용할 수 있다.
메세지 큐로 활용할때는 블로킹팝(BLPOP)을 활용하는것이 좋으며, 이때는 별도로 공부하는것이 좋다.
⚠️주의사항⚠️
Redis는 데이터의 내구성을 보장하지 않으므로(캐싱 특화), 데이터가 손실될 수 있음을 인지해야한다.
Redis는 In-memory 기반의 데이터 저장소이기 때문에 속도가 매우 빠르고, 비교적 간단하게 큐를 구현할 수 있다.
하지만 서버가 재시작되거나 장애가 발생 시 데이터가 손실될 수 있으며, 메시지 브로커로서의 고급 기능(재시도 등)도 부족하다.
따라서, Redis는 빠른 처리 속도가 필요하고 데이터 손실이 큰 문제가 되지 않는 경우에 적합하며, 내구성이 중요한 경우에는 Kafka와 같은 다른 솔루션을 고려하는 것이 좋다.
Redis Database Backup(RDB), Append Only File(AOF) 설정을 수정 모든 데이터를 즉시 디스크에 쓰도록 구성할수도 있지만,
RDB설정은 마지막 스냅샷 이후의 데이터가 손실될 수 있는 가능성이 있고, AOF는 appendfsync always를 사용하면 성능에 영향을 주고 appendfsync everysec를 사용하여 1초마다 저장하게 설정하더라도 최대 1초의 데이터 손실이 발생할 가능성이 있다.
즉, 아무리 설정해도 전통적인 RDBMS처럼 데이터 내구성(Durability)을 보장하지 않는다.
👇👇 실습은 아래 더보기 클릭 👇👇
# 리스트 생성
127.0.0.1:6379> RPUSH numbers 1 2 3 4 5
(integer) 5
# 1. 전체 범위 조회
127.0.0.1:6379> LRANGE numbers 0 -1 (0은 시작인덱스, -1은 마지막 인덱스를 뜻한다)
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 2. 처음 3개만 조회 (0,1,2 인덱스)
127.0.0.1:6379> LRANGE numbers 0 2
1) "1"
2) "2"
3) "3"
# 3. 2번째부터 4번째까지 조회
127.0.0.1:6379> LRANGE numbers 1 3
1) "2"
2) "3"
3) "4"
# 4. 마지막 2개 조회 (음수 인덱스 사용)
127.0.0.1:6379> LRANGE numbers -2 -1
1) "4"
2) "5"
# 오른쪽에 값 추가
127.0.0.1:6379> RPUSH numbers 6
(integer) 6
127.0.0.1:6379> LRANGE numbers 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
# 왼쪽에 값 추가
127.0.0.1:6379> LPUSH numbers 7
(integer) 7
127.0.0.1:6379> LRANGE numbers 0 -1
1) "7"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
7) "6"
# 리스트 길이 확인
127.0.0.1:6379> LLEN numbers
(integer) 7
# 특정 인덱스 값 조회
127.0.0.1:6379> LINDEX numbers 3
"3"
# 오른쪽에 값 제거
127.0.0.1:6379> RPOP numbers
"6"
127.0.0.1:6379> LRANGE numbers 0 -1
1) "7"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
# 왼쪽에 값 제거
127.0.0.1:6379> LPOP numbers
"7"
127.0.0.1:6379> LRANGE numbers 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 특정 범위만 남기고 삭제
127.0.0.1:6379> LTRIM numbers 2 3
OK
127.0.0.1:6379> LRANGE numbers 0 -1
1) "3"
2) "4"
3. Sets (셋)
- https://redis.io/docs/latest/develop/data-types/sets/
Sets은 순서가 없는 문자열 데이터 타입이며, 중복데이터를 허용하지않는다.
또한, Sets는 집합 연산(교집합, 합집합 등)을 지원하여, 데이터 간의 관계를 쉽게 분석할 수 있다.
따라서, 순서는 중요하지 않지만 중복되면 안되는 데이터들을 저장하거나 일반적인 집합연산(교집합, 합집합, 차집합)에 적합하다.
- 기본 커멘드
SADD key member # 세트에 새로운 멤버를 추가 SREM key member # 지정된 멤버를 집합에서 제거 SISMEMBER key member # 문자열의 집합 멤버십 테스트 SINTER key1 key2 ... # 두 개 이상의 집합이 공통적으로 가지고 있는 멤버 집합(즉, 교집합) 반환 SCARD key # 집합의 크기(일명 카디널리티) 반환 |
- 활용사례
- 특정 블로그 게시글에 액세스 하는 IP리스트
- 태그 시스템 (특정 태그로 조회 해야할때 집합연산)
- 팔로우/팔로워 시스템
👇👇 실습은 아래 더보기 클릭 👇👇
# 세트에 새로운 값 저장
127.0.0.1:6379> SADD myset "apple"
(integer) 1
127.0.0.1:6379> SADD myset "banana"
(integer) 1
127.0.0.1:6379> SADD myset "cherry"
(integer) 1
# 세트 데이터 조회
127.0.0.1:6379> SMEMBERS myset
1) "apple"
2) "banana"
3) "cherry"
# 세트에 데이터 제거
127.0.0.1:6379> SREM myset "banana"
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "apple"
2) "cherry"
# 세트 삭제
127.0.0.1:6379> DEL myset
(integer) 1
127.0.0.1:6379> SMEMBERS myset
(empty array)
# 교집합/합집합/차집합
127.0.0.1:6379> SADD set1 "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> SADD set2 "banana" "cherry" "melon"
(integer) 3
127.0.0.1:6379> SINTER set1 set2 # 교집합 (SINTER)
1) "banana"
2) "cherry"
127.0.0.1:6379> SUNION set1 set2 # 합집합 (SUNION)
1) "apple"
2) "banana"
3) "cherry"
4) "melon"
127.0.0.1:6379> SDIFF set1 set2 # 차집합 (SDIFF)
1) "apple"
127.0.0.1:6379> SDIFF set2 set1 # 차집합 (SDIFF)
1) "melon"
4. Sorted sets (소티드 셋)
- https://redis.io/docs/latest/develop/data-types/sorted-sets/
Sorted sets은 Sets과 비슷한 특징을 가지지만 각 맴버가 Score를 가지며, Score를 기준으로 정렬된 데이터 타입이다.
즉, 중복제거가 가능하고, 집합 연산에 특화되어 있으면서도, Score를 가지는 특징을 가진다.
- 기본 커멘드
ZADD key score member # 정렬된 셋에 멤버와 점수를 추가 ZREM key member # 지정된 멤버를 정렬된 셋에서 제거 ZSCORE key member # 특정 멤버의 점수를 반환 ZRANGE key start stop # 지정된 범위의 멤버를 반환 (점수 기준 오름차순) ZREVRANGE key start stop # 지정된 범위의 멤버를 반환 (점수 기준 내림차순) ZRANK key member # 특정 멤버의 순위를 반환 (오름차순) ZCARD key # 정렬된 셋의 크기(일명 카디널리티) 반환 |
- 활용사례
- 리더보드 (점수에 따라 순위가 매겨지는 시스템)
- 우선순위 큐
- 타임스탬프 기반의 데이터 정렬
👇👇 실습은 아래 더보기 클릭 👇👇
# 세트에 새로운 값(멤버) 저장
127.0.0.1:6379> ZADD leaderboard 100 "Alice"
(integer) 1
127.0.0.1:6379> ZADD leaderboard 72 "Bob"
(integer) 1
127.0.0.1:6379> ZADD leaderboard 88 "Charlie"
(integer) 1
# 값(맴버) 조회
127.0.0.1:6379> ZRANGE leaderboard 0 -1 # 오름차순으로 전체 범위 조회
1) "Bob"
2) "Charlie"
3) "Alice"
127.0.0.1:6379> ZREVRANGE leaderboard 0 -1 # 내림차순으로 전체 범위 조회
1) "Alice"
2) "Charlie"
3) "Bob"
# 특정 맴버의 값 반환
127.0.0.1:6379> ZSCORE leaderboard "Bob"
"72"
# 특정 맴버의 순위 반환
127.0.0.1:6379> ZRANK leaderboard "Charlie"
(integer) 1
# 정렬된 셋의 크기 반환
127.0.0.1:6379> ZCARD leaderboard
(integer) 3
# 맴버와 점수 함께 조회
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES # 오름차순
1) "Bob"
2) "72"
3) "Charlie"
4) "88"
5) "Alice"
6) "100"
127.0.0.1:6379> ZREVRANGE leaderboard 0 -1 WITHSCORES # 내림차순
1) "Alice"
2) "100"
3) "Charlie"
4) "88"
5) "Bob"
6) "72"
# 맴버 제거
127.0.0.1:6379> ZREM leaderboard "Alice"
(integer) 1
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES
1) "Bob"
2) "72"
3) "Charlie"
4) "88"
5. Hashes (해시)
- https://redis.io/docs/latest/develop/data-types/hashes/
Hash는 값에 field-value 쌍을 저장하는 데이터 타입이다.
위 String 타입에서도 JSON을 저장할 수 있었는데, Hash의 경우 개별필드에 접근이 가능하여 부분 업데이트하는데 더 용이하다.
- 기본 커멘드
HSET key field value # 하나 이상의 필드에 값 설정 HGET key field # 주어진 필드의 값 반환 HMGET key field1 field2 ... # 여러 필드 한번에 조회 HINCRBY key field increment # 주어진 필드의 값을 제공된 정수만큼 증가 |
- 활용사례
- 사용자 프로필 저장
- 설정 정보 관리
- 상품 정보 저장
- 복잡한 세션 데이터 저장
👇👇 실습은 아래 더보기 클릭 👇👇
# 데이터 저장
127.0.0.1:6379> HSET user:user02 username "user02" email "user02@example.com" age "30"
(integer) 3
# 특정 필드 조회
127.0.0.1:6379> HGET user:user02 username
"user02"
# 여러 필드 한번에 조회
127.0.0.1:6379> HMGET user:user02 username email age
1) "user02"
2) "user02@example.com"
3) "30"
# 모든 field-value 조회
127.0.0.1:6379> HGETALL user:user02
1) "username"
2) "user02"
3) "email"
4) "user02@example.com"
5) "age"
6) "30"
# 특정 필드 존재 여부 확인
127.0.0.1:6379> HEXISTS user:user02 email
(integer) 1
# 모든 필드 이름 조회
127.0.0.1:6379> HKEYS user:user02
1) "username"
2) "email"
3) "age"
# 모든 값 조회
127.0.0.1:6379> HVALS user:user02
1) "user02"
2) "user02@example.com"
3) "30"
# 필드 개수 조회
127.0.0.1:6379> HLEN user:user02
(integer) 3
# 숫자 값 증가
127.0.0.1:6379> HINCRBY user:user02 age 2
(integer) 32
# 특정 필드 값 수정
127.0.0.1:6379> HSET user:user02 username "lina"
(integer) 0 # # 기존 필드 수정시 0 반환, 새로운 필드 추가시 1 반환
# 특정 필드 삭제 (특정 필드 선택적으로 삭제)
127.0.0.1:6379> HDEL user:user02 username
(integer) 1
# 키 자체를 전부 삭제
127.0.0.1:6379> DEL user:user02
(integer) 0
HASH 타입의 경우 데이터 구조를 저장할 때 두 가지 인코딩 방식을 사용한다.
인코딩을 선택해서 사용하는것이 아니라 자동으로 설정되며,
중간에 조건이 넘으면 listpack에서 hashtable로 자동으로 전환된다.
구분 | 인코딩 | 조건 | 장점 |
작은 Hash | Ziplist listpack (7.0이상) |
필드 개수가 512개 이하 모든 필드와 값의 길이가 64바이트 이하 (두 조건 모두 만족해야 함) |
메모리 사용량이 적음 작은 데이터에 접근이 빠름 |
큰 Hash | Hashtable | 필드 개수가 512개 초과 필드나 값의 길이가 64바이트 초과 |
대량 데이터 처리에 효율적 개별 필드 접근이 O(1)로 매우 빠름 필드 추가/삭제가 자유로움 메모리 파편화가 적음 |
인코딩 방식을 확인해보자.
# small hash
127.0.0.1:6379> HSET smallhash field1 "123123" field2 "this is data"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING smallhash
"listpack"
# big hash
127.0.0.1:6379> HSET bighash description "This is a very long description that definitely exceeds 64 bytes. We need to make sure it's long enough to trigger the hashtable encoding in Redis. This should do it!"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING bighash
"hashtable"
6. Streams (스트림)
- https://redis.io/docs/latest/develop/data-types/streams/
Streams는 로그와 같은 데이터 시퀀스를 관리하는 데이터 타입으로, 메시지 큐와 유사한 기능을 제공한다.
각 메시지는 고유한 ID를 생성하며, ID를 사용하여 연관된 항목을 검색하거나 후속항목을 읽고 처리할 수 있다.
- 기본 커멘드
XADD key ID field value # 스트림에 새 메시지 추가 XREAD COUNT n STREAMS key ID # 스트림에서 메시지 읽기 XRANGE # 두 ID 사이의 항목 범위 반환 XLEN key # 스트림의 길이 반환 XDEL key ID # 스트림에서 메시지 삭제 |
- 활용사례
- Pub/Sub
- 이벤트 소싱: 사용자 동작 클릭 등 추적
- 로그 수집: 실시간 로그 데이터 수집 및 분석 (예, 센서 모니터링)
- 데이터 스트리밍: 실시간 데이터 피드 관리
Redis를 Pub/Sub으로 활용하는것은 XGROUP 커멘드 Docs에서 자세히 볼 수 있으며, 아래는 간단한 예시이다.
# 스트림에 데이터 추가
127.0.0.1:6379> XADD events * type "user_signup" user_id "123"
"1735318144850-0"
127.0.0.1:6379> XADD events * type "purchase" user_id "456" amount "99.99"
"1735318174147-0"
# Subscriber 그룹 생성
127.0.0.1:6379> XGROUP CREATE events mygroup $ MKSTREAM
OK
# Subscriber 데이터 소비
127.0.0.1:6379> XREADGROUP GROUP mygroup consumer1 COUNT 0 STREAMS events > # 읽지않은 메세지 읽기
1) 1) "events"
2) 1) 1) "1735318174147-0"
2) 1) "type"
2) "purchase"
3) "user_id"
4) "456"
5) "amount"
6) "99.99"
127.0.0.1:6379> XREADGROUP GROUP mygroup consumer1 COUNT 0 STREAMS events > # 읽지 않은 메세지 없음
(nil)
127.0.0.1:6379> XREADGROUP GROUP mygroup consumer1 COUNT 0 STREAMS events 0 # 특정 메세지 읽기
1) 1) "events"
2) 1) 1) "1735318174147-0"
2) 1) "type"
2) "purchase"
3) "user_id"
4) "456"
5) "amount"
6) "99.99"
# 메세지 확인 처리
127.0.0.1:6379> XREADGROUP GROUP mygroup consumer1 COUNT 0 STREAMS events 0
1) 1) "events"
2) 1) 1) "1735318174147-0"
2) 1) "type"
2) "purchase"
3) "user_id"
4) "456"
5) "amount"
6) "99.99"
127.0.0.1:6379> XACK events mygroup 1735318174147-0
(integer) 1
127.0.0.1:6379> XREADGROUP GROUP mygroup consumer1 COUNT 0 STREAMS events 0
1) 1) "events"
2) (empty array)
👇👇 실습은 아래 더보기 클릭 👇👇
# 스트림에 새 메세지 저장
127.0.0.1:6379> XADD user_clicks * user_id 1 action "click" page "home"
"1735316702887-0"
127.0.0.1:6379> XADD user_clicks * user_id 2 action "click" page "about"
"1735316712422-0"
127.0.0.1:6379> XADD user_clicks * user_id 1 action "click" page "contact"
"1735316719007-0"
127.0.0.1:6379> XADD user_clicks * user_id 2 action "click" page "log-in"
"1735316729574-0"
# 스트림 메세지 읽기
127.0.0.1:6379> XREAD COUNT 2 STREAMS user_clicks 0 # 0(처음)부터 2개의 메세지를 읽기
1) 1) "user_clicks"
2) 1) 1) "1735316702887-0"
2) 1) "user_id"
2) "1"
3) "action"
4) "click"
5) "page"
6) "home"
2) 1) "1735316712422-0"
2) 1) "user_id"
2) "2"
3) "action"
4) "click"
5) "page"
6) "about"
# 모든 메세지 조회
127.0.0.1:6379> XRANGE user_clicks - +
1) 1) "1735316702887-0"
2) 1) "user_id"
2) "1"
3) "action"
4) "click"
5) "page"
6) "home"
2) 1) "1735316712422-0"
2) 1) "user_id"
2) "2"
3) "action"
4) "click"
5) "page"
6) "about"
3) 1) "1735316719007-0"
2) 1) "user_id"
2) "1"
3) "action"
4) "click"
5) "page"
6) "contact"
4) 1) "1735316729574-0"
2) 1) "user_id"
2) "2"
3) "action"
4) "click"
5) "page"
6) "log-in"
# 스트림 길이 반환
127.0.0.1:6379> XLEN user_clicks
(integer) 4
# 메세지 삭제
127.0.0.1:6379> XDEL user_clicks 1735316719007-0
(integer) 1
127.0.0.1:6379> XRANGE user_clicks - +
1) 1) "1735316702887-0"
2) 1) "user_id"
2) "1"
3) "action"
4) "click"
5) "page"
6) "home"
2) 1) "1735316712422-0"
2) 1) "user_id"
2) "2"
3) "action"
4) "click"
5) "page"
6) "about"
3) 1) "1735316729574-0"
2) 1) "user_id"
2) "2"
3) "action"
4) "click"
5) "page"
6) "log-in"
⚠️주의사항⚠️ 1. 데이터 분배 저장
Redis Streams에서 특정 필드 값으로 직접 필터링하는 기능은 없기 때문에, 위 실습 예시대로 프로덕션에 구현하면 큰일날수있다.
수많은 데이터를 한번에 가져와서 애플리케이션에서 필터링해야하기 때문이다.
따라서 적절하게 분배하여 데이터를 잘 저장하는것을 추천한다. (데이터 파티셔닝 혹은 보조 인덱스 사용이라고 부르기도 한다)
# 권장하지않음 : 모든 데이터를 하나의 스트림에 저장
XADD user_clicks * user_id 1 action "click" page "home"
XADD user_clicks * user_id 2 action "click" page "about"
XADD user_clicks * user_id 1 action "click" page "contact"
# 권장 : 적절한 보조 인덱스를 사용하여 별도의 스트림에 저장
XADD user_clicks:1 * action "click" page "home"
XADD user_clicks:2 * action "click" page "about"
XADD user_clicks:1 * action "click" page "contact"
⚠️주의사항⚠️ 2. 데이터 내구성
위 List에서도 설명했지만, 한번 더 강조한다.
Redis는 데이터의 내구성을 보장하지 않으므로(캐싱 특화), 데이터가 손실될 수 있음을 인지해야한다.
Redis는 In-memory 기반의 데이터 저장소이기 때문에 속도가 매우 빠르고, 비교적 간단하게 큐를 구현할 수 있다.
하지만 서버가 재시작되거나 장애가 발생 시 데이터가 손실될 수 있으며, 메시지 브로커로서의 고급 기능(재시도 등)도 부족하다.
따라서, Redis는 빠른 처리 속도가 필요하고 데이터 손실이 큰 문제가 되지 않는 경우에 적합하며, 내구성이 중요한 경우에는 Kafka와 같은 다른 솔루션을 고려하는 것이 좋다.
Redis Database Backup(RDB), Append Only File(AOF) 설정을 수정 모든 데이터를 즉시 디스크에 쓰도록 구성할수도 있지만,
RDB설정은 마지막 스냅샷 이후의 데이터가 손실될 수 있는 가능성이 있고, AOF는 appendfsync always를 사용하면 성능에 영향을 주고 appendfsync everysec를 사용하여 1초마다 저장하게 설정하더라도 최대 1초의 데이터 손실이 발생할 가능성이 있다.
즉, 아무리 설정해도 전통적인 RDBMS처럼 데이터 내구성(Durability)을 보장하지 않는다.
► Special Data Type
1. Beatmaps (비트맵)
https://redis.io/docs/latest/develop/data-types/bitmaps/
Bitmaps는 Redis의 String 타입을 사용하여 비트 단위(0/1)의 조작을 지원하는 데이터 타입이다.
효율적인 비트 연산을 통해 대량의 데이터를 간단하게 처리할 수 있다.
- 기본 커멘드
SETBIT key offset value # 특정 비트 위치에 값을 설정 GETBIT key offset # 특정 비트 위치의 값을 반환 BITCOUNT key [start end] # 비트맵에서 1로 설정된 비트의 개수 반환 BITOP <AND | OR | XOR | NOT> destkey key [key ...]. # 비트 연산 수행 (AND, OR, XOR, NOT) BITPOS key bit [start [end [BYTE | BIT]]] # 지정된 값 (0 또는 1)을 갖는 첫 번째 비트 세트 반환 |
- 활용사례
- 플래그 관리: 사용자의 상태나 설정을 비트로 관리
- 유니크 방문자 수 계산: 특정 기간 동안의 방문자 수 효율적으로 계산
- 대량 데이터 처리: 대규모 데이터의 간단한 비트 계산
👇👇 실습은 아래 더보기 클릭 👇👇
# 데이터 입력 (특정 사용자가 방문했음을 기록)
127.0.0.1:6379> SETBIT day1:visitors 1 1
(integer) 0
127.0.0.1:6379> SETBIT day1:visitors 2 1
(integer) 0
127.0.0.1:6379> SETBIT day1:visitors 9 1
(integer) 0
# 데이터 조회 (특정 사용자 방문했는지 확인)
127.0.0.1:6379> GETBIT day1:visitors 1
(integer) 1
127.0.0.1:6379> GETBIT day1:visitors 2
(integer) 1
127.0.0.1:6379> GETBIT day1:visitors 3
(integer) 0
# 총 유니크 방문자 수 계산
127.0.0.1:6379> BITCOUNT day1:visitors
(integer) 3
# OR 연산 (Day1, Day2에 방문한 총 사용자)
127.0.0.1:6379> BITOP OR total_visitors day1:visitors day2:visitors
(integer) 2
127.0.0.1:6379> BITCOUNT total_visitors
(integer) 4
# AND 연산 (Day1, Day2에 모두 방문한 사용자)
127.0.0.1:6379> BITOP AND total_visitors day1:visitors day2:visitors
(integer) 2
127.0.0.1:6379> BITCOUNT total_visitors
(integer) 1
2. Geospatial (지리공간)
- https://redis.io/docs/latest/develop/data-types/geospatial/
Geospatial은 Redis에서 위치 정보를 저장하고 처리할 수 있는 데이터 타입이며,
위도와 경도를 기반으로 거리 계산, 반경 내 위치 검색 등의 기능을 제공한다.
- 기본 커멘드
GEOADD key longitude latitude member # 위치 정보를 추가 GEOPOS key member # 특정 멤버의 위치 반환 GEODIST key member1 member2 [unit] # 두 위치 간의 거리 반환 GEORADIUS key longitude latitude radius [unit] # 반경 내 위치 검색 GEORADIUSBYMEMBER key member radius [unit] # 특정 멤버를 중심으로 반경 내 위치 검색 |
- 활용사례
- 위치 기반 서비스: 사용자의 현재 위치를 기반으로 주변 정보를 제공
- 물류 및 배송 관리: 특정 위치를 중심으로 반경 내 배송지 검색
- 지도 및 내비게이션: 경로 탐색 및 거리 계산
👇👇 실습은 아래 더보기 클릭 👇👇
# 데이터 입력 (도시 위치 정보 입력)
127.0.0.1:6379> GEOADD cities 126.9784 37.5665 "Seoul"
(integer) 1
127.0.0.1:6379> GEOADD cities 129.0756 35.1796 "Busan"
(integer) 1
127.0.0.1:6379> GEOADD cities 116.4074 39.9042 "Beijing"
(integer) 1
127.0.0.1:6379> GEOADD cities 139.6917 35.6895 "Tokyo"
(integer) 1
127.0.0.1:6379> GEOADD cities -0.1276 51.5074 "London"
(integer) 1
127.0.0.1:6379> GEOADD cities -74.0060 40.7128 "New York"
(integer) 1
127.0.0.1:6379> GEOADD cities 2.3522 48.8566 "Paris"
(integer) 1
127.0.0.1:6379> GEOADD cities 151.2093 -33.8688 "Sydney"
(integer) 1
127.0.0.1:6379> GEOADD cities 13.4050 52.5200 "Berlin"
(integer) 1
127.0.0.1:6379> GEOADD cities 55.2708 25.2048 "Dubai"
(integer) 1
127.0.0.1:6379> GEOADD cities 37.6173 55.7558 "Moscow"
(integer) 1
127.0.0.1:6379> GEOADD cities -58.3816 -34.6037 "Buenos Aires"
(integer) 1
# 조회 (서울 위치 반환)
127.0.0.1:6379> GEOPOS cities "Seoul"
1) 1) "126.97840064764022827"
2) "37.5665003062872529"
# 거리 반환 (두 두시 간의 거리)
127.0.0.1:6379> GEODIST cities "Seoul" "Busan" km
"325.1823"
127.0.0.1:6379> GEODIST cities "Seoul" "New York" km
"11055.6856"
# 특정 위치 중심의 도시 검색
127.0.0.1:6379> GEORADIUS cities 126.9784 37.5665 50 km
1) "Seoul"
# 특정 도시 중심으로 반경 내 검색
127.0.0.1:6379> GEORADIUSBYMEMBER cities "Paris" 500 km
1) "Paris"
2) "London"
127.0.0.1:6379> GEORADIUSBYMEMBER cities "Seoul" 500 km
1) "Seoul"
2) "Busan"
127.0.0.1:6379> GEORADIUSBYMEMBER cities "Seoul" 1000 km
1) "Beijing"
2) "Busan"
3) "Seoul"
3. HyperLogLog (HLL)
- https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/
HyperLogLog는 대규모 데이터셋의 유니크 요소 수를 추정하는 데 사용되는 데이터 타입이다.
실제 데이터를 저장하지 않고, 해시값을 사용해서 유니크 요소의 개수를 추정하는데 특화되어있다.
실제 데이터가 저장되는것이 아님을 주의하자!
- 특징
- 추정 기반: 실제 데이터를 저장하지 않고, 해시 값을 사용하여 유니크 요소의 개수를 추정함
- 메모리 효율성: 약 12KB의 고정된 메모리만 사용하여 수십억 개의 유니크 요소를 추정할 수 있음
- 정확도: 약 0.81%의 오차 범위 내에서 유니크 요소 수를 추정함
- 기본 커멘드
PFADD key element [element ...] # 요소를 HyperLogLog에 추가 PFCOUNT key [key ...] # 유니크 요소의 추정 개수 반환 PFMERGE destkey sourcekey [sourcekey ...] # 여러 HyperLogLog를 병합 |
- 활용사례
- 유니크 방문자 수 계산: 웹사이트의 일일 유니크 방문자 수 추정
- 이벤트 카운팅: 대규모 이벤트의 유니크 발생 횟수 추정
- 데이터 분석: 대량의 데이터에서 유니크 요소 수를 빠르게 추정
👇👇 실습은 아래 더보기 클릭 👇👇
# 데이터 입력 (사용자가 노래를 재생할때마다 HLL에 추가)
127.0.0.1:6379> PFADD ROSE_APT "user1"
(integer) 1
127.0.0.1:6379> PFADD ROSE_APT "user2"
(integer) 1
127.0.0.1:6379> PFADD ROSE_APT "user3"
(integer) 1
127.0.0.1:6379> PFADD ROSE_APT "user1"
(integer) 0
# 유니크 사용자 수 추정
127.0.0.1:6379> PFCOUNT ROSE_APT
(integer) 3
# 여러 노래의 유니크 사용자 수 병합
127.0.0.1:6379> PFADD AESPA_SUPERNOVA "user2"
(integer) 1
127.0.0.1:6379> PFADD AESPA_SUPERNOVA "user4"
(integer) 1
127.0.0.1:6379> PFMERGE K_POP ROSE_APT AESPA_SUPERNOVA
OK
127.0.0.1:6379> PFCOUNT K_POP
(integer) 4