메뉴

문서정보

목차

LPUSH와 RPUSH로 데이터 밀어 넣기

Redis 리스트(List)는 입력순서에 따라 정렬된 string의 목록이다. 목록의 왼쪽 혹은 오른쪽 끝에 새로운 요소를 밀어 넣는 식으로 리스트에 string 데이터를 추가 할 수 있다. 링크드리스트(Linked List)의 구현이라고 보면 된다.

LPUSH(Left push)는 왼쪽에 데이터를 추가하는 반면, RPUSH(Right Push)는 오른쪽에 데이터를 추가한다.

 LPUSH RPUSH

LPUSH와 RPUSH의 사용방법이다.
RPUSH key value [value ...]
LPUSH key value [value ...]

RPUSH를 이용해서 초기 데이터인 seoul, london, paris를 입력해보자.
RPUSH city seoul london paris
(integer) 3
city key에 3개의 value를 밀어넣었다. key의 값은 LRANGE로 읽을 수 있다.
LRANGE key start stop
start와 stop를 이용해서, 읽을 범위를 설정할 수 있다. 0부터 시작한다. stop에 -1을 설정하면 start에서 끝까지 데이터를 읽는다.
LRANGE city 0 3
1) "seoul"
2) "london"
3) "paris"
이제 위의 그림대로 "lisbon"과 "doha"를 넣어보자. 왼쪽끝에서 아이템을 밀어넣어야 하므로 LPUSH 명령을 이용해야 한다.
LPUSH city lisbon doha
(integer) 5
반환 값으로 리스트의 크기를 반환한다. 원하는데로 저장이 됐는지 확인해보자.
LRANGE city 0 -1
1) "doha"
2) "lisbon"
3) "seoul"
4) "london"
5) "paris"

이제 "tokyo"와 "boston"를 오른쪽에 밀어 넣어보자.
RPUSH city tokyo boston
(integer) 7

LRANGE city 0 -1
1) "doha"
2) "lisbon"
3) "seoul"
4) "london"
5) "paris"
6) "tokyo"
7) "boston"

POP으로 꺼내기

LPOP와 RPOP 명령을 이용해서 리스트로 부터 아이템을 꺼낼 수 있다. 꺼낸다는 것은 "읽고 지운다"는 의미다. LPOP는 리스트의 왼쪽 즉 0번째 아이템을 꺼내고, RPOP는 리스트의 오른쪽 마지막 아이템을 꺼낸다. 테스트용 리스트를 만들었다.
> RPUSH mylist a b c d e
(integer) 5
> LRANGE mylist 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"

LPOP과 RPOP으로 아이템을 꺼내보자.
> LPOP mylist
"a"
> RPOP mylist
"e"
> LRANGE mylist 0 -1
1) "b"
2) "c"
3) "d"

List 명령어들

리스트와 관련된 명령어들의 목록이다. 간단하게 각 명령어들을 설명한 후, 주로 사용하는 명령어들을 실제 서비스 시나리오를 예제 삼아서 설명하겠다. 아래 접두사/접미사 규칙을 숙지하면 각 명령어를 좀 더 쉽게 사용 할 수 있을 것이다.
명령 설명
BLPOP key [key ...] timeout 리스트의 첫번째(가장 왼쪽) 아이템을 가져오고 삭제한다. 가져올 아이템이 없다면 timeout 동안 기다린다.
BRPOP key [key ...] timeout 리스트의 마지막 아이템을 가져오고 삭제한다. 가져올 아이템이 없다면 timeout 동안 기다린다.
LINDEX key index index 위치에 있는 데이터를 가져온다.
LLEN key key에 있는 아이템의 갯수를 반환한다.
LPOP key 리스트의 첫번째 아이템을 가져오고 삭제한다.
LPUSH key value [value ...] 리스트의 왼쪽에 아이템을 추가한다.
LPUSHX key value key가 있을 때만 아이템을 추가한다.
LRANGE key start stop start에서 stop 범위에 있는 값을 가져온다.
LREM key count value count 만큼 일치하는 value를 삭제한다.
LSET key indxes value 임의의 위치(index)에 value를 추가 한다.
LTRIM key start stop start에서 stop 범위에 있는 값을 자른다. 결과적으로 start ~ stop 범위의 value만 남는다.
RPOP key 리스트의 마지막에 있는 아이템을 읽고 삭제한다.
RPUSH key value [value ...] 리스트의 마지막에 하나 이상의 value를 추가한다.
RPUSHX key value key가 존재할 경우, 리스트의 마지막에 value를 추가한다.
RPOPLPUSH source destination source key로 부터 RPOP을 한다음, destination key에 RPOP한 아이템을 LPUSH 한다.
BRPOPLPUSH source destination RPOPPLPUSH의 블럭 버전

Capped lists

리스트의 일반적인 시나리오 중 하나는 마지막 N개의 아이템을 저장하는 것이다. 실시간 메시징시스템을 만든다고 가정해 보자. 유저가 온라인 상태에서 메시지를 보내면 즉시 유저에게 전달될 것이다. 하지만 유저가 오프라인 상태 혹은 다른 이유로 메시지가 전달되지 않았다면, 이 메시지는 메시지 박스에 저장할 것이다. 유저가 오랜시간 오프라인 상태에 있을 수 있으니, 최근 메시지 N개만 남겨야 할 것이다. 이러한 종류의 리스트를 capped lists 라고 한다.

 Capped lists

데이터베이스에서 메시지를 직접 읽는 대신에 Redis에서 읽는다. 보통 유저는 처음 N개만큼의 데이터를 주로 읽을 건데, 데이터를 RDBMS가 아닌 Redis에서 읽으므로 서비스 성능을 높일 수 있다. Redis에 저장된 데이터 N개를 초과해서 과거의 데이터를 보고 싶다면, 이때 RDBMS에서 데이터를 가져오면 된다. 유저 입장에서는 서비스 진입단계에서 메시지를 빠르게 확인 할 수 있으며, 과거의 데이터는 읽는 속도가 떨어져도 별 문제가 안되므로 체감 성능이 크게 좋아진다.

Capped List는 rpushltrim으로 구현할 수 있다.
> LPUSH messagebox:yundream 1 2 3 4 5
(integer) 5
> LRANGE messagebox:yundream 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
> LTRIM messagebox:yundream 0 2
OK
> LRANGE messagebox:yundream 0 -1
1) "5"
2) "4"
3) "3"
  1. messagebox:yundream 에 5개의 메시지를 LPUSH로 밀어 넣었다.
  2. LTRIM으로 0 ~ 2, 3개의 아이템을 제외한 나머지 아이템을 삭제했다.
  3. 결과적으로 크기가 3인 메시지박스를 구현했다.
메시지가 들어올 때마다 TRIM을 하는 건 비효율적이므로 실제 구현을 할 때는 메시지 박스의 크기를 좀 더 크게 잡아서 TRIM 횟수를 줄이는 등의 방법을 써야 할 것이다. 예를 들어 100개의 최근 아이템을 저장하고 싶다면, 메시지 박스의 크기를 200으로 설정하고 200이 됐을 때, LTRIM을 이용 100개를 TRIM하는 식으로 구현 할 수 있을 것이다. LPUSH를 할 때 리스트의 전체 원소의 갯수를 반환하므로 이 반환 값을 비교해서 TRIM 여부를 결정하면 된다.

쇼핑카트 구현

쇼핑카트를 만들어보자. 먼저 쇼핑 프로세스를 정의해보자.
  1. 유저가 로그인한다. 로그인을 하면 세션을 발급할 것이다. 이 세션은 RDBMS와 Redis에 저장된다.
  2. 유저가 아이템을 쇼핑카드에 넣는다.
  3. 유저는 하나 이상의 아이템을 담을 수 있으므로 LIST 자료구조가 적당할 거다.
  4. key를 만들고 아이템을 PUSH 한다. key는 session을 포함해서, 찾을 수 있도록 네이밍한다.
  5. 유저는 아이템을 뺄 수도 있다.
  6. 유저가 로그아웃하면 key를 삭제한다. 웹 애플리케이션의 경우 로그아웃하지 않고 웹 브라우저를 닫을 수 있으므로, key에 timeout을 정해야 할 것이다.
쇼핑카트를 구현하는 오래된 방법은 쿠키(cookie)에 저장하는 거다. 쿠키는 지금도 이런 목적으로 사용 할 수 있지만 모바일 애플리케이션에서 사용하기에는 좋은 방법이 아니다. 웹브라우저로만 대상으로 하는 웹 애플리케이션이 아니라면, 서버사이드에서 Redis로 해결하는게 더 좋은 방법이다.

yundream 유저의 세션 key가 "session-1234"라고 가정하자. 유저가 아이템을 구매하면 LPUSH로 아이템 ID를 밀어 넣는다. 쇼핑카트의 key 이름은 "cart:session"으로 정했다.
> LPUSH cart:session-1234 item-01a8ad3
(integer) 1
> LPUSH cart:session-1234 item-2aedji1
(integer) 2
> LPUSH cart:session-1234 item-359dzjJ
(integer) 3
LRANGE로 유저의 쇼핑카트를 확인 할 수 있다.
> LRANGE cart:session-1234 0 -1
1) "item-359dzjJ"
2) "item-2aedji1"
3) "item-01a8ad3"
지금은 value로 아이템 ID만 입력했지만, 실 서비스에서는 구매시간, 구매 갯수와 같은 정보들도 입력해야 하기 때문에 JSON등의 포맷을 사용해야 할 것이다.

유저는 쇼핑카트에서 특정 아이템을 삭제할 수 있어야 한다. item-2aedji1을 삭제해보자.
> LREM cart:session-1234 0 item-2aedji1
(integer) 1
> LRANGE cart:session-1234 0 -1
1) "item-359dzjJ"
2) "item-01a8ad3"
LREM은 아이템을 지우기 위해서 사용한다. LREM은 count 와 value 를 옵션으로 사용하는데, value를 count 만큼 삭제한다. 이때 count가 양수인지, 음수인지, 0인지에 따라서 삭제하는 패턴이 달라진다. 예를 들어 "LREM mykey -2 3"을 입력하면 마지막 2개의 "3"을 삭제한다. 테스트를 위해서 데이터를 입력했다.
> LPUSH testkey 0 1 2 3 3 4 3
(integer) 7
> LRANGE testkey 0 -1
1) "3"
2) "4"
3) "3"
4) "3"
5) "2"
6) "1"
7) "0"
마지막 "3" 2개를 삭제해보자.
> LREM testkey -2 3
(integer) 2
> LRANGE testkey 0 -1
1) "3"
2) "4"
3) "2"
4) "1"
5) "0"

간단하게 해결 된 것 같지만 여러 정보를 저장한 JSON 형태로 저장을 했다면, 삭제가 어려워질 수 있다. JSON 데이터에 시간, 아이템 갯수등을 넣었다고 가정해보자. 일치하는 아이템을 넣어야 하는데, 불가능하지는 않겠지만 코드가 지저분해 질 것이다.

인덱스로 삭제 할 수 없을까 ? 왜 인지는 모르겠으나 Redis는 인덱스로 삭제하는 명령을 제공하지 않는다. 그래서 굳이 두 번의 명령으로 삭제를 해야 한다. 아래와 같은 아이템을 가진 리스트를 만들어보자.
> RPUSH myitem 0 1 2 3 3 4 3
> LRANGE myitem 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "3" <--- 이 녀석을 삭제하고 싶다.
6) "4"
7) "3"
4번째 인덱스의 "3"을 삭제한다고 가정해보자. LREM으로는 삭제 할 수가 없다.

이 경우 LSET 으로 4번째 아이템을 "DELETE" 표시를 하고, LREM으로 삭제하면 된다.
> LSET myitem 4 DELETE
OK
> LRANGE myitem 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "DELETE" <--- DELETE 표시를 한다.
6) "4"
7) "3"

> LREM myitem 0 DELETE  // 모든 DELETE 아이템을 삭제한다.
(integer) 1

> LRANGE myitem 0 -1    // 원하는 아이템을 삭제했다.
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
6) "3"

« Redis Data structure - Strings 목차