...
Redis 데이터 타입 (Collection)
Redis의 장점 중 하나는 Key-Value 스토리지에서 Value는 단순한 Object가 아니라 다양한 자료구조를 갖기 때문이다.
String, Set, Sorted Set, Hash, List 등 다양한 타입을 지원한다.
이번 시간에는 각 레디스의 자료구조를 알아보고 자료구조를 이용하는 명령어(command)를 정리해보는 시간을 가져보겟다.
Redis Collections 를 사용할 때 주의점
하나의 컬렉션에 너무 많은 아이템을 담으면 좋지 않다.
가능하면 10000개 이하의, 몇천개 수준의 데이터 셋을 유지하는게 Redis 성능에 영향주지 않는다.
Redis - Strings
- 일반적인 문자열
- 값은 최대 512 MB이며, String으로 될 수 있는 binary data도, JPEG 이미지도 저장 가능하다.
- 단순 증감 연산에 좋음
- string-string 매핑을 이용하여 연결되는 자료 매핑을 할 수도 있다. HTML 매핑도 가능
Strings 명령어 리스트
- SET: SET, SETNX, SETEX, SETPEX, MSET, MSETNX, APPEND, SETRANGE
- GET: GET, MGET, GETRANGE, STRLEN
- INCR: INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT
- Enterprise: SETS, DELS, APPENDS (subquery)
Commands | Syntax | Description |
DECR | key | 1씩 감가, 신규이면 -1로 setting. |
DECRBY | key decrement | decrement만큼 감소. 신규이면 -decrement로 setting. |
DEL | key [key ...] | 데이터를 삭제 |
GET | key | 데이터를 조회 |
GETSET | key value | 기존 데이터를 조회하고 새 데이터를 저장 |
INCR | key | 1씩 증가, 신규이면 1로 setting. |
INCRBY | key increment | increment만큼 증가. 신규이면 increment로 setting. |
MGET | key [key ...] | 여러개의 데이터를 한번에 조회 |
SET | key value [EX seconds] [PX milliseconds] [NX|XX] |
데이터를 저장, key가 이미 있으면 덮어쓴다. |
SETNX | key value | 지정한 key가 없을 경우에만 데이터를 저장 |
MSET | key value [key value ...] | 여러개의 데이터를 한번에 저장 |
MSETNX | key value [key value ...] | 지정한 key가 없을 경우에만, 여러개의 데이터를 한번에 저장 |
APPEND | key value | 데이터를 추가, 지정한 key가 없으면 저장 |
SETEX | key seconds value | 지정한 시간(초) 이후에 데이터 자동 삭제 |
SETRANGE | key offset value | 지정한 위치(offset)부터 데이터를 겹쳐쓴다 |
STRLEN | key | 데이터의 바이트수를 리턴 |
GETRANGE | key start end | 데이터의 일부 문자열을 조회 |
INCRBYFLOAT | key increment | 실수연산, increment만큼 증가. 신규이면 increment로 setting. |
PSETEX | key milliseconds value | 지정한 시간(밀리초) 이후에 데이터 자동 삭제 |
STRALGO | STRALGO LCS | 두 문자열이 얼마나 유사한지 평가 |
GETEX | key [EX seconds] | 데이터 조회와 만료 시간 설정 |
GETDEL | key | 데이터 조회와 삭제 |
SETS | key (subquery) | SETS key (subquery) |
DELS | (subquery) | DELS (subquery) |
APPENDS | (subquery) | APPENDS key (subquery) |
# 한개 조회
set <key> <value>
get <key> <value>
# 여러개 조회
mset <key> <value> <key> <value> ...
mget <key> <key> <key> ...
127.0.0.1:6379> set hello "world!"
OK
127.0.0.1:6379> get hello
"world!"
127.0.0.1:6379> get count
"-351"
127.0.0.1:6379> set count 50
OK
127.0.0.1:6379> incr count
(integer) 51
127.0.0.1:6379> get count
"51"
127.0.0.1:6379> incrby count 100
(integer) 151
127.0.0.1:6379> decr count
(integer) 150
127.0.0.1:6379> decrby count 500
(integer) -350
127.0.0.1:6379> mset a "hello" b "world"
OK
127.0.0.1:6379> mget a b
1) "hello"
2) "world"
Redis - Bitmaps
- bitmaps은 string의 변형
- bit 단위 연산 가능하다.
- String이 512MB 저장 할 수 있듯이 2^32 bit까지 사용 가능하다.
- 저장할 때, 저장 공간 절약에 큰 장점이 있다.
Bits 명령어 리스트
Commands | Syntax | Description |
GETBIT | key offset | bit 값 조회 |
SETBIT | key offset value | bit 값 조정 |
BITCOUNT | key [start end] | 1인 bit 수를 센다 |
BITOP | key operation destkey key [key ...] | bit 연산( AND, OR, XOR, NOT) 실행 |
BITPOS | key bit [start [end]] | 지정한 bit의 위치를 구한다 |
BITFIELD | key [GET type offset] [SET type offset value] | Perform arbitrary bitfield integer operations on strings |
# setbit <key> <offset> <value>
# key: 해당 비트맵을 칭할 값
# offset: 0 보다 큰 정수의 값
# value: 0 또는 1의 비트 값
> setbit 20220410 4885 1
> getbit 20220410 4885
> bitcount 20220410 # 범위 내의 1로 설정된 bit의 개수를 반환
Redis - Lists
- array 형식의 데이터 구조. 데이터를 순서대로 저장
- 추가 / 삭제 / 조회하는 것은 O(1)의 속도를 가지지만, 중간의 특정 index 값을 조회할 때는 O(N)의 속도를 가지는 단점이 있다.
- 즉, 중간에 추가/삭제가 느리다. 따라서 head-tail에서 추가/삭제 한다. (push / pop 연산)
- 메세지 queue로 사용하기 적절하다.
소셜네트워크에서 타임라인과 같은 기능을 구현할 때 LPUSH를 통해 제일 첫 부분에 Insert하며 LRANGE 명령어를 통해 일정 크기를 고정적으로 빠르게 반환할 수 있다.
LPUSH 명령어와 LTRIM 명령어를 함께 사용하면 Lists의 크기를 항상 일정하게 고정시킬 수 있다.
LPUSH와 RPOP을 이용한다면 message를 전달하는 queue로 사용할 수 있다.
Lists 명령어 리스트
- SET (PUSH): LPUSH, RPUSH, LPUSHX, RPUSHX, LSET, LINSERT, RPOPLPUSH
- GET: LRANGE, LINDEX, LLEN
- POP: LPOP, RPOP, BLPOP, BRPOP
- REM: LREM, LTRIM
- BLOCK: BLPOP, BRPOP, BRPOPLPUSH
- Enterprise: LREVRANGE, LPUSHS, RPUSHS (subquery)
Commands | Syntax | Description |
LPUSH | key value [value ...] | 왼쪽에서 리스트의 오른쪽에 데이터를 저장 |
RPOP | key | 리스트 오른쪽에서 데이터를 꺼내오고, 리스트에서는 삭제 |
LPOP | key | 리스트 왼쪽에서 데이터를 꺼내오고, 리스트에서는 삭제 |
RPUSH | key value [value ...] | 오른쪽에서 리스트의 왼쪽에 데이터를 저장 |
LRANGE | key start stop | 인덱스로 범위를 지정해서 리스트 조회 |
LLEN | key | 리스트에서 데이터의 총 갯수를 조회 |
LINDEX | key index | 인덱스로 특정 위치의 데이터를 조회 |
LSET | key index value | 인덱스로 특정 위치의 값을 바꿈 |
LREM | key count value | 값을 지정해서 삭제 |
LTRIM | key start stop | 인덱스로 지정한 범위 밖의 값들을 삭제 |
RPOPLPUSH | key src_key dest_key | RPOP + LPUSH |
BLPOP | key [key ...] timeout | 리스트에 값이 없을 경우, 지정한 시간만큼 기다려서 값이 들어오면 LPOP 실행 |
BRPOP | key [key ...] timeout | 리스트에 값이 없을 경우, 지정한 시간만큼 기다려서 값이 들어오면 RPOP 실행 |
BRPOPLPUSH | src_key dest_key timeout | 리스트에 값이 없을 경우, 지정한 시간만큼 기다려서 값이 들어오면 RPOPLPUSH 실행 |
LINSERT | key BEFORE|AFTER pivot value | 지정한 값 앞/뒤에 새 값 저장 |
LPUSHX | key value | 기존에 리스트가 있을 경우에만 LPUSH 실행 |
RPUSHX | key value | 기존에 리스트가 있을 경우에만 RPUSH 실행 |
LPOS | key element | 값으로 인덱스를 조회 |
LMOVE | source destination | 리스트간 데이터 이동 |
BLMOVE | source destination | 리스트간 데이터 이동 - 대기 |
LLS | key pattern | 패턴(pattern)으로 값(value) 조회 |
LRM | key pattern | 패턴(pattern)으로 값(value) 삭제 |
LREVRANGE | key key start stop | 인덱스로 범위를 지정해서 역순으로 조회 |
LPUSHS | key (subquery) | 서브쿼리로 데이터를 저장 |
RPUSHS | key (subquery) | 서브쿼리로 데이터를 저장 |
# 왼쪽에 삽입
lpush <key> <value>
# 오른쪽에 삽입
rpush <key> <value>
# 삭제
lpop <key>
rpop <key>
# LPUSH를 통한 list 생성
127.0.0.1:6379> LPUSH myList "a"
(integer) 1
127.0.0.1:6379> LRANGE myList 0 -1
1) "a"
# LPUSH , RPUSH를 통한 요소 삽입 결과.
127.0.0.1:6379> LPUSH myList "b"
(integer) 2
127.0.0.1:6379> RPUSH myList "c"
(integer) 3
127.0.0.1:6379> LRANGE myList 0 -1
1) "b"
2) "a"
3) "c"
# LPUSHX , RPUSHX 사용 예
# key가 없는곳에 추가할려고 하는 경우 0을 반환.
127.0.0.1:6379> LPUSHX myList2 "a"
(integer) 0
127.0.0.1:6379> LPUSH myList "d"
(integer) 4
# 기존에 있던 myList요소들은 [ d , b , a , c ] 순으로 되어있음.
# LPOP , RPOP을 통해 맨 좌 우측 요소 한개씩 제거
127.0.0.1:6379> LPOP myList
"d"
127.0.0.1:6379> RPOP myList
"c"
127.0.0.1:6379> LRANGE myList 0 -1
1) "b"
2) "a"
# 현재 List의 요소 길이를 출력.
127.0.0.1:6379> LLEN myList
(integer) 2
# LREM을 통핸 해당 요소 삭제 , count를 0 으로 해서 요소중에 "a"랑 매칭되는 값을 삭제
127.0.0.1:6379> LREM myList 0 "a"
(integer) 1
127.0.0.1:6379> LRANGE myList 0 -1
1) "b"
# 해당 key에 해당되는 index값을 입력받은 값을 수정.
# 현재 b로 남아있던 요소값을 z로 변경
127.0.0.1:6379> LSET myList 0 "z"
OK
127.0.0.1:6379> LRANGE myList 0 -1
1) "z"
# RPOPLPUSH
127.0.0.1:6379> RPOPLPUSH myList hello
"z"
127.0.0.1:6379> LRANGE hello 0 -1
1) "z"
Redis - Hashes
- field-value로 구성 되어있는 전형적인 hash의 형태 (파이썬의 딕셔너리나 js객체 정도로 이해하면 된다)
- key 하위에 subkey를 이용해 추가적인 Hash Table을 제공하는 자료구조
- 메모리가 허용하는 한, 제한없이 field들을 넣을 수가 있다.
Hashes 명령어 리스트
- SET: HSET, HMSET, HSETNX
- GET: HGET, HMGET, HLEN, HKEYS, HVALS, HGETALL, HSTRLEN, HSCAN, HEXISTS
- REM: HDEL
- INCR: HINCRBY, HINCRBYFLOAT
Commands | Syntax | Description |
HSET | key field value | Field와 value를 저장 |
HDEL | key field [field ...] | Field로 value를 삭제 |
HGET | key field | Field로 value를 조회 |
HLEN | key | Field 갯수 조회 |
HMSET | key field value [field value ...] | 여러개의 field와 value를 저장 |
HMGET | key field [field ...] | 여러개의 value를 조회 |
HKEYS | key | Key에 속한 모든 field name을 조회 |
HVALS | key | Key에 속한 모든 value를 조회 |
HGETALL | key | Key에 속한 모든 field와 value을 조회 |
HINCRBY | key field increment | value를 increment 만큼 증가 또는 감소 |
HEXISTS | key field | Field가 있는지 확인 |
HSETNX | key field value | Field가 기존에 없으면 저장 |
HINCRBYFLOAT | key field increment_float | value를 increment_float 만큼 증가 또는 감소 |
HSCAN | key cursor [MATCH pattern] [COUNT count] |
Field, member를 일정 단위 갯수 만큼씩 조회 |
HSTRLEN | key field | value의 길이(byte)를 조회 |
# 한개 값 삽입 및 삭제
hset <key> <subkey> <value>
hget <key> <subkey>
# 여러 값 삽입 및 삭제
hmset <key> <subkey> <value> <subkey> <value> ...
hnget <key> <subkey> <subkey> <subkey> ...
# 모든 subkey와 value 가져오기, Collection에 너무 많은 key가 있으면 장애의 원인이 됨
hgetall <key>
# 모든 value값만 가져오기
hvlas <key>
# field - value : name - jinmin / year - 1995 / month - 3
127.0.0.1:6379> hset hh name jinmin year 1995 month 3
(integer) 3
127.0.0.1:6379> hget hh name
"jinmin"
127.0.0.1:6379> hget hh year
"1995"
127.0.0.1:6379> hdel hh year
(integer) 1
127.0.0.1:6379> hlen hh
(integer) 2
127.0.0.1:6379> hgetAll hh
1) "name"
2) "jinmin"
3) "month"
4) "3"
127.0.0.1:6379> hkeys hh
1) "name"
2) "month"
127.0.0.1:6379> hvals hh
1) "jinmin"
2) "3"
Redis - Sets
- 중복된 데이터를 담지 않기 위해 사용하는 자료구조 (js의 set이라고 생각하면 된다)
- 유니크한 key값
- 정렬되지 않은 집합
- 중복된 데이터를 여러번 저장하면 최종 한번만 저장된다.
- Set간의 연산을 지원. 교집합, 합집합, 차이를 매우 빠른 시간내에 추출할 수 있다.
- 단, 모든 데이터를 전부 다 갖고올 수 있는 명령이 있으므로 주의해서 사용해야 한다.
Sets 명령어 리스트
- SET: SADD, SMOVE
- GET: SMEMBERS, SCARD, SRANDMEMBER, SISMEMBER, SSCAN
- POP: SPOP
- REM: SREM
- 집합연산: SUNION, SINTER, SDIFF, SUNIONSTORE, SINTERSTORE, SDIFFSTORE
- Enterprise: SLS, SRM, SLEN, SADDS (subquery)
Commands | Syntax | Description |
SADD | key member [member ...] | 집합에 member를 추가 |
SREM | key member [member ...] | 집합에서 member를 삭제 |
SMEMBERS | key | 집합의 모든 member를 조회 |
SCARD | key | 집합에 속한 member의 갯수를 조회 |
SUNION | key [key ...] | 합집합을 구함 |
SINTER | key [key ...] | 교집합을 구함 |
SDIFF | key [key ...] | 차집합을 구함 |
SUNIONSTORE | dest_key src_key [src_key ...] | 합집합을 구해서 새로운 집합에 저장 |
SINTERSTORE | dest_key src_key [src_key ...] | 교집합을 구해서 새로운 집합에 저장 |
SDIFFSTORE | dest_key src_key [src_key ...] | 차집합을 구해서 새로운 집합에 저장 |
SISMEMBER | key member | 집합에 member가 존재하는지 확인 |
SMOVE | src_key dest_key member | 소스 집합의 member를 목적 집합으로 이동 |
SPOP | key [count] | 집합에서 무작위로 member를 가져옴 |
SRANDMEMBER | key [count] | 집합에서 무작위로 member를 조회 |
SSCAN | key cursor [MATCH pattern] [COUNT count] |
member를 일정 단위 갯수 만큼씩 조회 |
SMISMEMBER | key member [member ...] | 집합에 member가 존재하는지 확인 - 여러 개 가능 |
SLS | key pattern | 패턴(pattern)으로 값(value) 조회 |
SRM | key pattern | 패턴(pattern)으로 값(value) 삭제 |
SLEN | key | 키에 속한 멤버 개수를 리턴 |
SADDS | key (subquery) | 서브쿼리로 member를 추가 |
sadd <key> <item>
# 존재 여부를 체크, 있으면 1 없으면 0 반환
sismember <key> <item>
# 삭제
srem <key> <value>
# key의 모든 item 조회
smembers <key>
127.0.0.1:6379> sadd myset a # 추가된 member 갯수 반환
(integer) 1
127.0.0.1:6379> sadd myset a
(integer) 0
127.0.0.1:6379> sadd myset b
(integer) 1
127.0.0.1:6379> sadd myset c
(integer) 1
127.0.0.1:6379> srem myset c # 삭제된 member 갯수 반환
(integer) 1
127.0.0.1:6379> smembers myset
1) "b"
2) "a"
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> sadd myset c d e f # 여러 member 삽입 가능
(integer) 4
127.0.0.1:6379> smembers myset
1) "d"
2) "c"
3) "a"
4) "f"
5) "b"
6) "e"
127.0.0.1:6379> spop myset 3 # 랜덤 member 삭제
1) "d"
2) "c"
3) "f"
127.0.0.1:6379> smembers myset
1) "a"
2) "b"
3) "e"
Redis - Sorted Sets
- set에 score라는 필드가 추가된 데이터 형 (score는 일종의 가중치)
- 일반적으로 set은 정렬이 되어있지않고 insert 한 순서대로 들어간다.
그러나 Sorted Set은 Set의 특성을 그대로 가지며 추가적으로 저장된 member들의 순서도 관리한다. - 데이터가 저장될때부터 score 순으로 정렬되며 저장
- sorted set에서 데이터는 오름차순으로 내부 정렬
- value는 중복 불가능, score는 중복 가능
- 만약 score 값이 같으면 사전 순으로 정렬되어 저장
유저 랭킹 보드서버 같은 구현에서 사용할 수 있다.
Sorted Sets 명령어 리스트
- SET: ZADD
- GET: ZRANGE, ZRANGEBYSCORE, ZRANGEBYLEX, ZREVRANGE, ZREVRANGEBYSCORE, ZREVRANGEBYLEX, ZRANK, ZREVRANK, ZSCORE, ZCARD, ZCOUNT, ZLEXCOUNT, ZSCAN
- POP: ZPOPMIN, ZPOPMAX
- REM: ZREM, ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREMRANGEBYLEX
- INCR: ZINCRBY
- 집합연산: ZUNIONSTORE, ZINTERSTORE
- Enterprise: ZISMEMBER, ZLS, ZRM, SLEN, SADDS (subquery)
Commands | Syntax | Description |
ZADD | key score member [score member ...] | 집합에 score와 member를 추가 |
ZCARD | key | 집합에 속한 member의 갯수를 조회 |
ZINCRBY | key increment member | 지정한 만큼 score 증가, 감소 |
ZRANGE | key start stop [withscores] | index로 범위를 지정해서 조회 |
ZRANGEBYSCORE | key min max [withscores] [limit offset count] |
score로 범위를 지정해서 조회 |
ZREM | key member [member ...] | 집합에서 member를 삭제 |
ZREMRANGEBYSCORE | key min max | score로 범위를 지정해서 member를 삭제 |
ZREVRANGE | key start stop [withscores] | index로 범위를 지정해서 큰 것부터 조회 |
ZSCORE | key member | member를 지정해서 score를 조회 |
ZCOUNT | key min max | score로 범위를 지정해서 갯수 조회 |
ZRANK | key member | member를 지정해서 rank(index)를 조회 |
ZREVRANK | key member | member를 지정해서 reverse rank(index)를 조회 |
ZREMRANGEBYRANK | key start stop | index로 범위를 지정해서 member를 삭제 |
ZUNIONSTORE | dest_key numkeys src_key [src_key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] |
합집합을 구해서 새로운 집합에 저장 |
ZINTERSTORE | dest_key numkeys src_key [src_key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] |
교집합을 구해서 새로운 집합에 저장 |
ZREVRANGEBYSCORE | key max min [withscores] [limit offset count] |
score로 범위를 지정해서 큰 것부터 조회 |
ZSCAN | key cursor [MATCH pattern] [COUNT count] |
score, member를 일정 단위 갯수 만큼씩 조회 |
ZRANGEBYLEX | key min max [limit offset count] | member로 범위를 지정해서 조회 |
ZLEXCOUNT | key min max | member로 범위를 지정해서 갯수 조회 |
ZREMRANGEBYLEX | key min max | member로 범위를 지정해서 member를 삭제 |
ZREVRANGEBYLEX | key max min [limit offset count] | member로 범위를 지정해서 큰 것부터 조회 |
ZPOPMIN | key | 작은 값부터 꺼내온다 |
ZPOPMAX | key | 큰 값부터 꺼내온다 |
BZPOPMIN | key | 데이터가 들어오면 작은 값부터 꺼내온다 |
BZPOPMAX | key | 데이터가 들어오면 큰 값부터 꺼내온다 |
ZMSCORE | member [member ...] | member의 score를 리턴 - 여러 개 가능 |
ZRANDMEMBER | key | 임의(random)의 멤버를 조회 |
ZRANGESTORE | dst src start stop | 조회해서 다른 키에 저장 |
ZUNION | numkeys key [key ...] | 합집합을 구함 |
ZINTER | numkeys key [key ...] | 교집합을 구함 |
ZDIFF | numkeys key [key ...] | 차집합을 구함 |
ZDIFFSTORE | destination numkeys key [key ...] | 차집합을 구해서 새로운 집합에 저장 |
ZISMEMBER | key member | 집합에 member가 존재하는지 확인 |
ZLS | key pattern | 패턴(pattern)으로 값(value) 조회 |
ZRM | key pattern | 패턴(pattern)으로 값(value) 삭제 |
ZLEN | key | 키에 속한 멤버 개수를 리턴 |
ZADDS | key (subquery) | 서브쿼리로 데이터 추가 |
# ZADD : key에 score-member를 추가
127.0.0.1:6379> zadd fruit 2 apple
(integer) 1
127.0.0.1:6379> zadd fruit 10 banana
(integer) 1
# 복수개의 score-member를 추가할 수 있음
127.0.0.1:6379> zadd fruit 8 melon 4 orange 6 watermelon
(integer) 3
# 이미 추가 된 member를 add 시 score가 업데이트
127.0.0.1:6379> zadd fruit 15 apple
(integer) 0
# ZSCORE : member에 해당하는 score 값 리턴
127.0.0.1:6379> zscore fruit apple
"15"
# ZRANK : member에 해당하는 rank(순위) 값 리턴
127.0.0.1:6379> zrank fruit melon
(integer) 2
# ZRANGE : key에 해당하는 start - stop 내가 출력하고 싶은 요소를 추출
127.0.0.1:6379> zrange fruit 0 -1
1) "orange"
2) "watermelon"
3) "melon"
4) "banana"
5) "apple"
Redis - HyperLogLogs
- 굉장히 많은양의 데이터를 dump할때 사용
- 중복되지 않는 대용량 데이터를 count할때 주로 많이 사용한다. (오차 범위 0.81%)
- set과 비슷하지만 저장되는 용량은 매우 작다 (저장 되는 모든 값이 12kb 고정)
- 하지만 저장된 데이터는 다시 확인할 수 없다. (데이터 보호에 적절)
엄청 크고 유니크 한 값 카운팅 할 때 사용
→ 웹 사이트 방문 ip 개수 카운팅, 하루 종일 크롤링 한 url 개수 몇개 인지, 검색 엔진에서 검색 한 단어 몇개 인지
HyperLogLog 명령어 리스트
Commands | Syntax | Description |
PFADD | key ele [ele ...] | 원소(element) 추가 |
PFCOUNT | key [key ...] | 원소 개수 조회 |
PFMERGE | destkey sourcekey [sourcekey ...] | 집합 머지(Merge) |
> PFADD crawled:20171124 "http://www.google.com/"
(integer) 1
> PFADD crawled:20171124 "http://www.redis.com/"
(integer) 1
> PFADD crawled:20171124 "http://www.redis.io/"
(integer) 1
> PFADD crawled:20171125 "http://www.redisearch.io/"
(integer) 1
> PFADD crawled:20171125 "http://www.redis.io/"
(integer) 1
> PFCOUNT crawled:20171124
(integer) 3
> PFMERGE crawled:20171124-25 crawled:20171124 crawled:20171125
OK
> PFCOUNT crawled:20171124-25
(integer) 4
Redis - Streams
- Streams는 log를 저장하기 가장 좋은 자료 구조
- append-only 이며 중간에 데이터가 바뀌지 않는다.
- 읽어 올때 id값 기반으로 시간 범위로 검색
- tail -f 사용 하는 것 처럼 신규 추가 데이터 수신
소비자별 다른 데이터 수신(소비자 그룹) 등 활용 가능하다.
Streams 명령어 리스트
Commands | Syntax | Description |
XADD | key ID field string [field string ...] | 데이터 추가 |
XLEN | key | |
XRANGE | key start end [COUNT count] | |
XREVRANGE | key end start [COUNT count] | |
XREAD | [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] | 데이터 읽기 |
XDEL | key ID [ID ...] | 데이터 삭제 |
XTRIM | key MAXLEN [~] count | 데이터 여러개 삭제 |
XGROUP | [CREATE key group id-or-$] [DESTROY key group] [DELCONSUMER key group consumer] [SETID key group id-or-$] | |
XREADGROUP | GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] | |
XACK | key group ID [ID ...] | |
XPENDING | key group [start end count] [consumer] | |
XCLAIM | key group consumer min-idle-time ID [ID ...] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [FORCE] [JUSTID] | |
XAUTOCLAIM | key group consumer min-idle-time start [COUNT count] [JUSTID] | |
XINFO | [CONSUMERS key group] [GROUPS key] [STREAM key] [HELP] |
>>> 127.0.0.1:6379> XADD customer * id "asdfdasf"
>>> "1647231099585-0"
>>> 127.0.0.1:6379> XADD customer * id "asdfdasf"
>>> "1647231112587-0"
>>> 127.0.0.1:6379> xlen customer
>>> (integer) 2
>>> 127.0.0.1:6379> xdel customer 1647231396280-0
>>> (integer) 1
XADD 커맨드를 이용해 mystream 이라는 키에 데이터를 저장하고 있다.
* 문자는 아이디값을 의미하며 아이디를 직접 지정할수도 있지만, * 문자를 입력하면 redis가 알아서 저장하고 반환해줌
반환된 아이디값은 데이터가 저장된 시간을 의미
Redis 자료구조 활용 사례
사례 1 - 좋아요 처리하기
기본적으로 SNS에서 게시물 좋아요 처리는 한 사용자가 하나의 댓글에 좋아요를 한번씩만 할수 있다.
RDBMS에서 유니크 조건을 걸어서 구현할 수 있지만, 이렇게 insert와 update가 자주 발생하는 경우 RDBMS 성능 저하가 발생하게 된다.
여기서 레디스의 Set을 이용하면 간단하게 구현할 수 있으며, 빠른 시간 안에 처리할 수 있다.
댓글의 번호를 key로 하고, 해당 댓글에 좋아요를 누른 회원 ID를 아이템으로 추가하면 동일한 ID값을 저장할 수 없으므로 한 명의 사용자는 하나의 댓글에 한번 만 좋아요를 누를 수 있게 되는 것이다.
사례 2 - 일일 순 방문자수 구하기
순 방문자수 란 서비스에 사용자가 하루에 여러번 방문했더라도 한번만 카운팅되는 값이다.
간단하게 중복 방문을 제거한 방문자의 지표라고 생각하면 된다.
유저는 천만 명이라 가정하고, 일일 방문자 횟수를 집계하며 이 값은 0시를 기준으로 초기화 된다고 하자.
실제 서비스에서는 이를 구현하기 위해서는 아래와 같은 방법이 있다.
- Google Analytics와 같은 외부 서비스 이용
- access log 분석
- 접속 정보를 로그파일로 작성하여 배치 프로그램 작성
그러나 밑에 두가지 방법은 실시간으로 데이터를 조회할 수 없다는 단점이 있다.
레디스의 비트 연산을 활용하면 간단하게 실시간 순 방문자를 저장하고 조회하는 방법을 구현할 수 있다.
사용자 ID는 0부터 순차적으로 증가된다고 가정하고, string의 각 bit를 하나의 사용자로 생각해 보자
그리고 사용자가 서비스에 방문할 때 사용자 ID에 해당하는 bit를 1로 설정한다.
1개의 bit가 1명을 의미하므로, 천만 명의 유저는 천만 개의 bit로 표현할 수 있고, 이는 곧 1.2MB정도의 크기이다.
레디스 string의 최대 길이는 512MB이므로 천만 명의 사용자를 나타내는 건 충분하다.
2020년 1월 29일에 ID가 7인 사용자가 방문했다고 하자.
그럼 아래 그림처럼 일곱 번째 인덱스를 1로 설정한다.
그러면 이 날에 서비스에 방문한 총방문자 수를 조회하기 위해서는 이 문자열에서 1로 설정된 bit의 개수를 구하는 BITCOUNT 연산을 사용하면 간단하게 구할수 있게 된다..
Redis의 HyperLogLogs 타입으로 이용해도 된다.
위 그림을 설명하자면, 만일 15명의 회원이 있다고 하면 비트 15개를 준비하고, 그 회원이 방문하면 회원의 id를 구해 해당 순서 비트를 1로 바꾸고 카운트 한다는 뜻이다.
사례 3 - 출석 이벤트 구현하기
정해진 기간동안 매일 방문한 사용자를 구하는 서비스를 구현한다면 어떻게 할까?
사례 2번을 이용하여, 구해놓은 string 간의 비트를 비교하는 BITOP 커맨드를 사용하면 이 서비스를 구현할 수 있다.
2020년 1월 29일부터 31일까지 매일 접속한 사용자는 id가 7인 사용자와 11인 사용자라는 것을 BITOP을 이용한 AND 연산을 통해 쉽게 구할 수 있다.
BITOP AND로 비트 연산 결과값이 1이 나오면 그 회원은 매일 방문 한다는 뜻이된다.
사례 4 - 최근 검색 목록 표시하기
사내 협업도구의 프로젝트에서 담당자를 선택할 때, 매번 멤버를 검색해서 입력해야 하는데, 내가 최근 검색했던 담당자가 드롭다운 메뉴로 보여지게 만들고 싶다.
만일 ID필드가 123 사용자(나) 가 최근 검색한 사용자 목록을 보고 싶다면, RDBMS로는 다음과 같이 쿼리를 날려야 한다.
select * from KEYWORD
where ID = 123
order by reg_date desc
limit 5;
하지만 이렇게 RDBMS의 테이블을 이용해서 데이터를 저장한다면 중복 제거도 해야 하고, 멤버별로 저장된 데이터의 개수를 확인하고, 오래된 검색어는 삭제하는 작업까지 이루어져야 한다.
따라서 애초에 중복이 되지 않고, 정렬되어 있는 레디스의 sorted set 을 사용하면 간단하게 구현할 수 있다.
sorted set은 가중치를 기준으로 오름차순으로 정렬되기 때문에, 가중치로 시간을 사용한다면 이 값이 가장 큰, 나중에 입력된 아이템이 맨 마지막 인덱스에 저장된다.
멤버 ID가 123인 사람이 최근 검색한 사람은 아래 그림처럼 정렬되어 저장된니.
이때 가중치(score)는 입력 순간의 나노시간을 나타내는데, 가장 처음 검색한 사람의 ID는 46, 가장 마지막 검색한 사람은 50이 되게 구성한다.
그러면 만일 내가 ID가 51인 사람을 검색하면 아래처럼 마지막에 데이터가 추가되게 된다.
단, 항상 다섯 명만 저장한다고 설계했기 때문에, 이를 위해서는 가장 마지막 값을 지워주어야 한다.
그림에서 보면 0번째 인덱스를 지우면 되기는 하는데, 아이템 개수가 6보다 작을 때에는 0번째 인덱스를 삭제하면 안 되기 때문에 매번 아이템의 수를 먼저 확인해야 하는 번거로움이 있게 된다.
이때 sorted set의 음수 인덱스를 사용하면 된다.
음수 인덱스는 인덱스의 마지막부터 큰 값부터 작은 값으로 매겨지는데, 데이터에 멤버를 추가한 뒤, 항상 -6번째 아이템을 지운다면 특정 개수 이상의 데이터가 저장되는 것을 방지 할 수 있게 된다.
인덱스로 아이템을 지우려면 ZREMRANGEBYRANK 커맨드를 사용하면 된다.
> ZREMRANGEBYRANK recent:member:123 -6 -6
이렇게 데이터에 멤버를 추가한 뒤, 항상 -6번째 아이템을 지운다면 특정 개수 이상의 데이터가 저장되는 것을 방지할 수 있게 된다.
# 참고자료
https://meetup.toast.com/posts/225
http://redisgate.kr/redisgate/ent/ent_intro.php
[NHN FORWARD 2021] Redis 야무지게 사용하기
https://redis.com/ebook/redis-in-action/
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.