Redis는 애초에 'High performance'를 목적에 두고 만들어졌기 때문에, 성능 개선을 위한 작업들을 많이 진행해 왔습니다. Pipelining이 그 중 하나고, 이번에는 Redis Scripting이라고도 불리는 eval을 알아 보겠습니다.

eval [script] [numkeys] [key ...] [arg ...]

EVAL은 Redis 버전 2.6.0부터 내장된 lua 인터프리터를 사용하여 스크립트를 평가하는 데 사용됩니다. 필수적으로 eval [script] [numkeys] 커맨드가 들어가 있어야 합니다.

  • script : 문자열 형태의 lua 스크립트입니다.
  • numkeys : 키의 갯수입니다. 이는 뒤에 추가적으로 붙을 선택 인자들 중 몇 개가 key인지를 lua가 알 수 있도록 합니다.

선택 인자 [key ...][arg ...]는 각각 lua에서 사용할 수 있도록 KEYSARGV 배열에 바인딩됩니다. Lua는 배열의 인덱스를 1부터 센다(one-based numbering)는 것에 주의해야 합니다.

아래 두 개의 lua 함수를 사용하여, lua script에서 redis 명령을 호출할 수 있습니다.

  • redis.call()
  • redis.pcall()

redis.call()은 redis.pcall()과 유사하지만, redis 명령 수행 중 오류가 발생하면 lua 오류를 발생시킵니다. 반대로 redis.pcall()은 오류를 trap하고 오류를 나타내는 lua 테이블을 반환합니다. 아래는 eval과 lua script를 사용한 간단한 set 명령입니다.

127.0.0.1:6379> eval "return redis.call('set', 1, 123)" 0
OK

script는 "return redis.call('set', 1, 123)", numkeys는 0입니다. 실제로 전달되는 명령은 set 1 123이 됩니다. eval의 컨벤션 상, 스크립트가 사용하는 모든 키는 KEYS 배열을 사용하여 전달되어야 하므로, 아래처럼 변경해야 합니다.

127.0.0.1:6379> eval "return redis.call('set', KEYS[1], 123)" 1 1
OK

어떤 이점이 있나?

복잡도만 늘어난 것으로 보이지만, Lua 스크립트는 다음과 같은 장점을 가집니다.

  • Pipelining처럼, 여러 명령을 한 번의 request/response만으로 수행할 수 있습니다.
  • 원하는 함수를 redis에서 지원하고 있지 않더라도 lua 스크립트로 대체 가능합니다.(반환되는 값 count, 반환되는 value 모두 더하기 등)
  • 아래와 같이, 스크립트를 재활용할 수도 있습니다.

스크립트 재활용하기

Lua script를 script load를 통해 실행하면, redis의 script cache라는 곳에 캐시됩니다. 그리고 그 실행 결과로 SHA 해시가 반환됩니다. 이는 각 lua script의 식별자로 활용되어, evalsha 명령어로 실행 가능합니다.

127.0.0.1:6379> script load "return {KEYS[1], KEYS[2]}"
"ee9729934e089eab4ec897f3c113f45c6a426b9d"
127.0.0.1:6379> evalsha ee9729934e089eab4ec897f3c113f45c6a426b9d 2 1 2
1) "1"
2) "2"

lua script의 길이가 길다면, 매 실행마다 script의 모든 코드를 전달하여 실행하는 것보다 미리 캐시된 script를 sha 값을 이용해 실행하는 것이 성능상 이점을 얻을 수 있습니다.

script exists로 특정 sha에 해당하는 script가 존재하는지 조회할 수 있고, script flush를 통해 script cache에 저장된 모든 script를 삭제할 수 있습니다.

127.0.0.1:6379> script exists ee9729934e089eab4ec897f3c113f45c6a426b9d
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists ee9729934e089eab4ec897f3c113f45c6a426b9d
1) (integer) 0

eval은 러닝 커브로서 lua를 배워야 한다는 점이 있지만, 성능을 개선하기 위해 매우 강력한 무기이므로 배워 두면 정말 좋을 것 같습니다.

'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] Pipelining  (0) 2018.09.06
[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] select  (0) 2018.09.06

Redis는 고성능의 key-value store지만, TCP 기반의 클라이언트-서버 모델을 따르기에 아래와 같이 동작하고, 네트워크 IO에 대한 병목이 존재할 수밖에 없습니다.(물리적인 네트워크 지연, 3-way handshake 등의 이유로)

  • 클라이언트가 서버에게 전송한 쿼리는 소켓을 통해 읽혀지고, 보통 blocking 형태로 response됩니다.
  • 서버는 명령을 처리하고 응답을 다시 클라이언트로 보냅니다.

따라서, 아래의 예에서 수행하는 5번의 작업은 각각 1회씩 request와 response를 거칩니다. 총 5번의 request/response가 발생합니다.

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> incr a
(integer) 2
127.0.0.1:6379> incr a
(integer) 3
127.0.0.1:6379> incr a
(integer) 4
127.0.0.1:6379> incr a
(integer) 5

Redis가 로컬에서 동작하고 있다면 클라이언트와 서버 간의 네트워킹 링크가 굉장히 빠르겠지만, 두 호스트 간에 물리적으로 많은 홉이 사용되었다면 지연이 발생될 가능성이 큽니다. request/response 모델에 의해 클라이언트 - 서버 - 클라이언트 순서로 패킷이 흘러가야 하니까요.

이 시간을 RTT(Round Trip Time)라고 합니다. 만약 서버가 초당 100k(100,000)개의 요청을 처리할 수 있다고 하더라도, RTT가 250ms라면 초당 최대 4개의 요청만을 처리할 수 있게 됩니다. 로컬에서 동작하는 Redis의 경우엔 RTT가 훨씬 더 짧지만, 많은 read/write를 수행하기 위해서는 여전히 많은 양입니다. 제 경우엔 RTT가 0.12ms였는데, 처리량이 초당 100k라면 이에 훨씬 못 미치는 속도입니다.

이러한 병목을 개선하기 위해, Redis에서는 pipelining API를 제공합니다. 이를 통해 클라이언트는 복수 개의 작업을 쌓아 한 번에 전송(request)할 수 있게 됩니다. 따라서 응답을 전혀 기다리지 않고 여러 명령을 한꺼번에 서버로 보낼 수 있고, 응답을 한번에 읽을 수 있습니다.

커맨드에서 pipelining하기

넷캣(netcat)을 사용하겠습니다. Netcat(nc)은 TCP나 UDP를 사용하는 네트워크 연결에서 데이터를 읽고 쓰는 간단한 리눅스 유틸리티입니다. Unix의 cat이 파일을 읽거나 쓰듯이, nc는 네트워크에 읽고 씁니다. 조금 더 알아보니, 해킹에도 꽤 유용하게 쓰인다고 합니다.

$ printf "PING\r\nPING\r\nPING\r\n" | nc localhost 6379
+PONG
+PONG
+PONG

별 차이 없어 보이지만, 3번의 ping 요청에 대해 RTT 비용을 단 한 번만 사용하게 되었습니다.

코드로 확인하기

이제 코드로 확인해 봅시다. redis-py를 사용했습니다.

Pipelining은 패킷의 왕복 시간으로 인한 대기 시간 비용을 줄이기 위한 방법일 뿐만 아니라, Redis 서버에서 수행할 수 있는 초당 작업량이 엄청나게 증가나게 됩니다. 다만, 파이프라이닝을 하는 동안에는 클라이언트의 메모리가 소모되므로 파이프라이닝을 할 때에는 한 번의 파이프라이닝 대기열에 포함시킬 작업의 합리적인 수를 가장 먼저 산정해야 합니다.

'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] Eval  (0) 2018.09.06
[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] select  (0) 2018.09.06

이전에 redis-cliRedis 서버에 접속하는 것까지 해 봤습니다. Redis에는 많은 데이터 타입들이 있는데, 가장 기본은 문자열입니다. Redis에서 문자열은 Byte string, 정수, 실수를 저장하기 위해 사용됩니다. 가장 일반적인 형태의 key-value store로서 redis를 사용하기 위한 타입이며, 여기서는 string 타입을 위한 get, set 등의 기본 조작을 수행해 봅시다.

set [key] [value] [EX seconds] [PX milliseconds] [NX|XX]

set은 redis에 key-value 매핑을 설정하기 위해 사용합니다. 기본적인 커맨드는 set key value이며, EX나 PX 등을 통해 해당 매핑에 expire(유효 기한)을 설정할 수 있습니다.

127.0.0.1:6379> set a 1
OK

key가 "a", value가 1이며 유효 기한은 없습니다. 별도로 제거하지 않는 이상 계속해서 남아 있습니다.

127.0.0.1:6379> set b 2 EX 3
OK

key가 "b", value가 2이며 유효 기한은 3초입니다.

set 명령어는 기본적으로 insert를 수행하나, key가 이미 존재한다면 update를 수행합니다.

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set a 3
OK
127.0.0.1:6379> set a 10
OK

get [key]

key에 해당하는 value를 반환합니다. 존재하지 않는 경우 (nil)을 반환합니다. Redis는 key들을 해시하므로, 시간복잡도는 O(1)으로 고정됩니다.

127.0.0.1:6379> get a
"10"
127.0.0.1:6379> get b
(nil)

incr [key], decr [key]

incrdecr 각각 key에 해당하는 value를 숫자로 평가 가능한지 판단한 이후, 1 증가시키거나, 감소시킵니다.

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> incr a
(integer) 2
127.0.0.1:6379> incr a
(integer) 3
127.0.0.1:6379> decr a
(integer) 2

value를 숫자로 평가할 수 없는 경우, 에러가 발생합니다.

127.0.0.1:6379> set a abc
OK
127.0.0.1:6379> incr a
(error) ERR value is not an integer or out of range

exists [key ...]

1개 이상의 key들을 받아 각각이 redis에 존재하는지를 검사하고, 존재하는 매핑의 수를 반환합니다.

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set b 3
OK
127.0.0.1:6379> exists a
(integer) 1
127.0.0.1:6379> exists a b
(integer) 2
127.0.0.1:6379> exists a b abc cde q s
(integer) 2

del [key ...]

1개 이상의 key들을 받아 해당하는 매핑을 제거하고, 제거한 매핑의 수를 반환합니다.

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set b 3
OK
127.0.0.1:6379> set c 5
OK
127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> del b c d e
(integer) 2


'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] Eval  (0) 2018.09.06
[Redis] Pipelining  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] select  (0) 2018.09.06

이전기본적인 push(lpush, rpush)와 pop(lpop, rpop), 약간의 조회 커맨드(lindex, lrange), 리스트 조작 커맨드를 한가지(ltrim) 알아봤습니다. 이번엔 list에 수행할 수 있는, 조금 더 많은 커맨드들에 대해 알아보겠습니다.

BLPOP [key ...] [timeout], BRPOP [key ...] [timeout]

각각 lpop과 rpop의 blocking 버전으로서, blocking pop이라고도 부릅니다. 인자로 전달된 키들을 순서대로 확인하면서, 비어있지 않은 리스트를 먼저 찾은 후 해당 리스트의 가장 첫(blpop), 또는 가장 마지막(brpop) 요소를 pop합니다. list에서 pop할 최소 한 개의 엘리먼트를 발견하거나, timeout을 초과할 때까지 redis 클라이언트가 block됩니다.

위의 예에서, 마지막 blpop 명령은 최대 10초동안(설정된 timeout) blocking되어 있습니다.

  • 그 사이에 key a에 묶여 있는 list에 값이 push된다면, 해당 엘리먼트를 pop하고 명령이 종료됩니다.
  • push되지 않는다면, timeout만큼 기다리다 nil을 반환하고 명령이 종료됩니다.

이러한 blocking pop 명령은 messaging queue나 task queue를 만드는 데에 유용하게 사용할 수 있습니다.

RPOPLPUSH [source_key] [dest_key]

source_key의 리스트에서 가장 마지막 요소를 rpop함과 동시에, 반환 값을 dest_key의 리스트에 lpush합니다. pop할 요소가 없는 경우 (nil)을 반환합니다.

BRPOPLPUSH [source_key] [dest_key] [timeout]

rpoplpush의 blocking 버전입니다.

'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] Pipelining  (0) 2018.09.06
[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] select  (0) 2018.09.06
[Redis] Set  (0) 2018.09.06

Redis에는 value에 대해 많은 데이터 타입들이 존재합니다. set [key] [value] 꼴의 명령을 사용했던 것들은 value에 string이 사용되는데, 이번에는 push/pop을 사용하는 list에 대해 알아보도록 합시다. list를 사용함으로써, redis를 queue처럼 활용할 수 있습니다.

List

List를 사용하면 sequence의 양 쪽 끝에서 요소를 push하고 pop하거나, 개별 요소를 가져오고 list에 수행할 수 있는 다양한 기타 작업을 수행할 수 있습니다. 아래는 가장 많이 사용되는 list 명령들입니다.

  • rpush(rpush [key] [value ...]) : list의 오른쪽 끝에 value들을 추가합니다.
  • lpush(lpush [key] [value ...]) : list의 왼쪽 끝에 value들을 추가합니다.
  • rpop(rpop [key]) : list의 오른쪽 끝에서 요소 하나를 pop(제거하며 반환)합니다.
  • lpop(lpop [key]) : list의 왼쪽 끝에서 요소 하나를 pop합니다.
  • lindex(lindex [key] [offset]) : offset에 해당하는 요소를 반환합니다. zero-based numbring 방식을 따릅니다.
  • lrange(lrange [key] [start] [end]) : start와 end 사이의 요소들을 모두 반환합니다.
  • ltrim(ltrim [key] [start] [end]) : start와 end 사이의 요소들만 남도록 list를 잘라냅니다.

위의 명령어들을 사용한 예는 다음과 같습니다.

lindex, lrange, ltrim 등 인덱스가 들어가는 곳에 음수가 들어갈 수 있다는 것을 알고 있으면 매우 유용합니다.

'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] select  (0) 2018.09.06
[Redis] Set  (0) 2018.09.06
[Redis] Publish/Subscribe  (0) 2018.09.06

Redis는 0부터 시작하는 숫자 인덱스를 갖는, 논리적으로 독립되어 있는 데이터베이스 구조를 지원합니다. 서로 다른 인덱스의 데이터베이스를 사용하더라도, 이들은 동일한 파일에 함께 유지되므로 완전히 독립된 데이터베이스가 아니라 네임스페이스 개념이라고 생각하는 것이 좋습니다. 그러나 key에 대한 부분은 독립되기 때문에 여러 응용 프로그램에 서로 다른 Redis 데이터베이스를 사용하더라도 key 식별은 걱정하지 않아도 됩니다.

select [index]

데이터베이스 선택은 select를 사용합니다. 기본 인덱스는 0번이며, 0번이 아닌 인덱스가 select된 경우 콘솔에 함께 표시됩니다.

127.0.0.1:6379>
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 2
OK
127.0.0.1:6379[2]>

서로 다른 데이터베이스에서 key들은 독립되어 있습니다.

127.0.0.1:6379> set a 123
OK
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> set a 456
OK
127.0.0.1:6379[1]> get a
"456"
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> get a
"123"

'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] Set  (0) 2018.09.06
[Redis] Publish/Subscribe  (0) 2018.09.06

set 타입은 특정 key에 대해 중복되지 않고, 순서가 보장되지 않는 값의 집합을 정의합니다. 내부적으로 hash table을 사용하므로 push/pop이 아닌 add/remove 패턴의 명령어를 사용합니다.

sadd [key] [item ...]

key의 set에 item들을 추가합니다. 성공적으로 추가된 item의 수를 반환합니다.

127.0.0.1:6379> sadd a 1 2 3
(integer) 3
127.0.0.1:6379> sadd a 1 3 4
(integer) 1

srem [key] [item ...]

key의 set에 존재하는 item들을 제거합니다. 성공적으로 제거된 item의 수를 반환합니다.

127.0.0.1:6379> srem a 1 2 3
(integer) 3
127.0.0.1:6379> srem a 1 2 3 4
(integer) 1

sismember [key] [item]

key의 set에 item이 존재하는지 체크합니다. item이 존재하는 경우 1을, 존재하지 않는 경우 0을 반환합니다. Python의 'in' 키워드와 유사합니다.

127.0.0.1:6379> sadd a 1 2 3
(integer) 3
127.0.0.1:6379> sismember a 1
(integer) 1
127.0.0.1:6379> sismember a 3
(integer) 1
127.0.0.1:6379> sismember a 4
(integer) 0

scard [key]

key의 set에 존재하는 item의 갯수를 반환합니다.

127.0.0.1:6379> sadd a 1 2 3
(integer) 3
127.0.0.1:6379> scard a
(integer) 3

smembers [key]

key의 set에 존재하는 모든 item들을 반환합니다.

127.0.0.1:6379> sadd a 1 2 3
(integer) 3
127.0.0.1:6379> smembers a
1) "1"
2) "2"
3) "3"

그리고 아래의 명령들을 사용할 수 있습니다.

  • srandmember [key] [count=1] : set에 존재하는 item들을 최대 count개 랜덤 선택하여 반환합니다. count에 음수를 전달하더라도 절댓값으로 처리됩니다.
  • spop [key] : set에 존재하는 item을 랜덤하게 선택해 반환하며 제거합니다.
  • smove [source-key] [dest-key] [item] : source에 item이 존재한다면, 해당 item을 source에서 제거함과 동시에 dest로 이동시키며, 이동에 성공한 경우 해당 item을 반환합니다.
  • sdiff [key] [compare-key ...] : compare-key들의 set에 모두 존재하지 않는 item들만 반환합니다. a에 {1, 2, 3}, b에 {1, 2}, c에 {2}가 들어 있는 상태에서 sdiff a b c의 결과는 {3}입니다.
  • sinter [key] [compare-key ...] : sdiff와 반대로, compare-key들의 set에 모두 존재하는 item들만 반환합니다. a에 {1, 2, 3}, b에 {1, 2}, c에 {2}가 들어 있는 상태에서 sdiff a b c의 결과는 {2}입니다.


'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] select  (0) 2018.09.06
[Redis] Publish/Subscribe  (0) 2018.09.06

Redis는 다양한 데이터 타입을 지원하면서, publish/subscribe(pub/sub) 모델까지 지원합니다. pub/sub는 메시지 큐와는 특성이 조금 다른데, pub/sub 모델에서는 채널을 subscribe한 모든 subscriber에게 메시지를 전달하기 때문에, 별도로 메시지를 보관하지 않는다는 것입니다. 아래는 Redis를 통해 pub/sub 모델을 사용하기 위한 커맨드들입니다.

  • subscribe [channel ...] : 전달된 channel들을 subscribe합니다.
  • unsubscribe [channel ...] : 전달된 channel들을 unsubscribe합니다. 아무 채널도 전달되지 않으면, subscribe되어 있던 모든 채널을 unsubscribe합니다.
  • publish [channel] [message] : 전달된 channel에 message를 publish합니다.
  • psubscribe [pattern ...] : 지정된 pattern들과 일치하는 채널들을 subscribe합니다.
  • punsubscribe [pattern ...] : 지정된 pattern들과 일치하는 채널들을 unsubscribe합니다. unsubscribe 명령처럼, 아무 채널도 전달되지 않으면 모든 채널을 unsubscribe합니다.

subscribe [channel ...]

전달된 channel들을 subscribe합니다. subscribe하고 나면, 해당 채널로 브로드캐스팅되는 메시지들을 읽기 위해 Ctrl-C로 인터럽트되기 전까지 무한히 blocking됨과 동시에 메시지 하나가 들어옵니다.

127.0.0.1:6379> subscribe test
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "test"
3) (integer) 1

메시지는 위처럼 3개의 요소들로 구성됩니다.

  1. 메시지 타입 : subscribe, message, unsubscribe로 나뉘며, 각각 채널을 성공적으로 subscribe한 경우, 채널로 일반 메시지가 publish된 경우, 채널을 성공적으로 unsubscribe한 경우에 해당됩니다.
  2. 채널 이름 : 메시지가 브로드캐스팅된 채널 이름입니다. 특히 여러 개의 채널을 subscribe한 경우에, 수신된 메시지의 채널을 분별하기 위해 사용합니다.
  3. 메시지 : 전송된 메시지입니다.

publish [channel] [message]

전달된 channel에 message를 publish합니다.

127.0.0.1:6379> publish test adsf
(integer) 1
127.0.0.1:6379> publish test hello
(integer) 1

성공적으로 publish되면, 1이 반환됩니다. 아래는 위처럼 메시지를 publish한 후, subscriber 측에 수신된 메시지 정보입니다.

1) "message"
2) "test"
3) "Hello"
1) "message"
2) "test"
3) "adsf"

'데이터베이스 > Redis' 카테고리의 다른 글

[Redis] String(기본 조작)  (0) 2018.09.06
[Redis] List의 고급 커맨드들  (0) 2018.09.06
[Redis] List  (0) 2018.09.06
[Redis] select  (0) 2018.09.06
[Redis] Set  (0) 2018.09.06

+ Recent posts