Redis 모듈 API
Redis 모듈 API (Modules API)
Redis 모듈 작성 입문.
출처: 공식문서
모듈 문서는 다음 페이지로 구성됩니다:
- Redis 모듈 소개(이 파일). Redis Modules 시스템과 API의 개요. 여기서 읽기를 시작하는 게 좋습니다
- 네이티브 데이터 타입 구현(Implementing native data types): 모듈에 네이티브 데이터 타입을 구현하는 방법
- 블로킹 연산(Blocking operations): 즉시 응답하지 않고, Redis 서버를 블로킹하지 않으면서 클라이언트를 블로킹하며, 가능할 때마다 응답을 제공하는 블로킹 명령 작성법
- Redis modules API reference:
module.c의 RedisModule 함수 상단 주석에서 생성됨. 각 함수가 어떻게 동작하는지 이해하기 좋은 레퍼런스
Redis 모듈은 외부 모듈로 Redis 기능을 확장할 수 있게 해, 코어 자체 내부에서 할 수 있는 것과 유사한 기능을 가진 새 Redis 명령을 빠르게 구현합니다.
Redis 모듈은 시작 시 또는 MODULE LOAD 명령으로 Redis에 로드할 수 있는 동적 라이브러리입니다. Redis는 redismodule.h라는 단일 C 헤더 파일 형태의 C API를 내보냅니다. 모듈은 C로 작성되도록 설계되었지만, C++이나 C 바인딩 기능이 있는 다른 언어도 사용할 수 있습니다.
모듈은 서로 다른 버전의 Redis에 로드되도록 설계되므로, 주어진 모듈은 특정 Redis 버전에서 실행되도록 설계·재컴파일할 필요가 없습니다. 이를 위해 모듈은 특정 API 버전으로 Redis 코어에 등록합니다. 현재 API 버전은 "1"입니다.
이 문서는 Redis 모듈의 알파 버전에 대한 것입니다. API, 기능, 기타 세부 사항은 향후 바뀔 수 있습니다.
모듈 로딩
개발 중인 모듈을 테스트하려면 다음 redis.conf 구성 지시어로 모듈을 로드할 수 있습니다:
loadmodule /path/to/mymodule.so
러타임에 다음 명령으로 모듈을 로드하는 것도 가능합니다:
MODULE LOAD /path/to/mymodule.so
로드된 모든 모듈을 나열하려면:
MODULE LIST
마지막으로 다음 명령으로 모듈을 언로드할 수 있습니다(원하면 나중에 재로드):
MODULE UNLOAD mymodule
위 mymodule은 .so 접미사를 뺀 파일 이름이 아니라, 모듈이 Redis 코어에 등록할 때 쓴 이름입니다. 이름은 MODULE LIST로 얻을 수 있습니다. 그러나 동적 라이브러리의 파일 이름이 모듈이 Redis 코어에 등록하는 이름과 같게 하는 것이 좋은 관행입니다.
작성할 수 있는 가장 단순한 모듈
모듈의 여러 부분을 보여주기 위해, 무작위 숫자를 출력하는 명령을 구현하는 매우 단순한 모듈을 보여드리겠습니다.
#include "redismodule.h"
#include <stdlib.h>
int HelloworldRand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
RedisModule_ReplyWithLongLong(ctx,rand());
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"helloworld.rand",
HelloworldRand_RedisCommand, "fast random",
0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
예제 모듈에는 두 함수가 있습니다. 하나는 HELLOWORLD.RAND라는 명령을 구현합니다. 이 함수는 그 모듈에 특정합니다. 그러나 RedisModule_OnLoad()라는 다른 함수는 모든 Redis 모듈에 존재해야 합니다. 모듈이 초기화되고, 명령을 등록하며, 쓰는 잠재적 사설 데이터 구조를 위한 진입점입니다.
모듈이 명령을 모듈 이름 뒤에 점, 그리고 명령 이름으로 호출하는 것(HELLOWORLD.RAND처럼)이 좋습니다. 그래야 충돌 가능성이 낮아집니다.
서로 다른 모듈이 충돌하는 명령을 가지면 두 모듈이 Redis에서 동시에 동작할 수 없습니다. RedisModule_CreateCommand 함수가 한 모듈에서 실패해 모듈 로딩이 오류 조건을 반환하며 중단되기 때문입니다.
모듈 초기화
위 예는 RedisModule_Init() 함수의 사용을 보여줍니다. 그것은 모듈 OnLoad 함수가 호출해야 하는 첫 함수여야 합니다. 함수 프로토타입:
int RedisModule_Init(RedisModuleCtx *ctx, const char *modulename,
int module_version, int api_version);
Init 함수는 Redis 코어에 모듈이 주어진 이름, 버전(MODULE LIST가 보고), 그리고 특정 API 버전을 사용하려 한다는 것을 알립니다.
API 버전이 틀리거나, 이름이 이미 사용 중이거나, 유사한 오류가 있으면 함수가 REDISMODULE_ERR을 반환하고 모듈 OnLoad 함수는 오류와 함께 가능한 빨리 반환해야 합니다.
Init 함수가 호출되기 전에는 다른 API 함수를 호출할 수 없습니다. 그렇지 않으면 모듈이 세그폴트(segfault)하고 Redis 인스턴스가 크래시합니다.
두 번째 호출 함수인 RedisModule_CreateCommand는 Redis 코어에 명령을 등록하는 데 쓰입니다. 프로토타입:
int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name,
RedisModuleCmdFunc cmdfunc, const char *strflags,
int firstkey, int lastkey, int keystep);
보시다시피 대부분의 Redis 모듈 API 호출은 모두 첫 인수로 모듈의 컨텍스트를 받으므로, 그것을 호출하는 모듈, 주어진 명령을 실행하는 명령·클라이언트 등에 대한 참조를 가집니다.
새 명령을 만들려면 위 함수에 컨텍스트, 명령 이름, 명령을 구현하는 함수 포인터, 명령 플래그, 명령 인수에서 키 이름의 위치가 필요합니다.
명령을 구현하는 함수는 다음 프로토타입이어야 합니다:
int mycommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
명령 함수 인수는 다른 모든 API 호출에 전달될 컨텍스트, 명령 인수 벡터, 사용자가 전달한 총 인수 수입니다.
보시다시피 인수는 특정 데이터 타입인 RedisModuleString에 대한 포인터로 제공됩니다. 이것은 불투명(opaque) 데이터 타입으로, 접근·사용을 위한 API 함수가 있고 필드에 직접 접근할 필요는 결코 없습니다.
예제 명령 구현을 들여다보면 또 다른 호출을 찾을 수 있습니다:
int RedisModule_ReplyWithLongLong(RedisModuleCtx *ctx, long long integer);
이 함수는 INCR이나 SCARD 같은 다른 Redis 명령처럼, 명령을 호출한 클라이언트에 정수를 반환합니다.
모듈 정리
대부분의 경우 특별한 정리가 필요 없습니다. 모듈이 언로드되면 Redis가 자동으로 명령 등록을 해제하고 알림 구독을 취소합니다. 그러나 모듈이 일부 영속 메모리나 구성을 포함하는 경우, 모듈은 선택적 RedisModule_OnUnload 함수를 포함할 수 있습니다. 모듈이 이 함수를 제공하면 모듈 언로드 과정에서 호출됩니다. 프로토타입:
int RedisModule_OnUnload(RedisModuleCtx *ctx);
OnUnload 함수는 REDISMODULE_ERR을 반환해 모듈 언로드를 막을 수 있습니다. 그렇지 않으면 REDISMODULE_OK를 반환해야 합니다.
Redis 모듈의 설정과 의존성
Redis 모듈은 Redis나 다른 라이브러리에 의존하지 않고, 특정 redismodule.h 파일로 컴파일될 필요도 없습니다. 새 모듈을 만들려면 최신 버전의 redismodule.h를 소스 트리에 복사하고, 원하는 라이브러리를 모두 링크하고, RedisModule_OnLoad() 함수 심볼을 내보내는 동적 라이브러리를 만들면 됩니다.
모듈은 다양한 Redis 버전에 로드될 수 있습니다.
모듈은 특정 API 함수가 모든 버전에 없는, 더 새롭고 오래된 Redis 버전 모두를 지원하도록 설계될 수 있습니다. 현재 실행 중인 Redis 버전에 API 함수가 구현되어 있지 않으면 함수 포인터가 NULL로 설정됩니다. 이는 모듈이 함수를 사용하기 전에 존재하는지 확인하게 합니다:
if (RedisModule_SetCommandInfo != NULL) {
RedisModule_SetCommandInfo(cmd, &info);
}
최근 redismodule.h 버전에서는 편의 매크로 RMAPI_FUNC_SUPPORTED(funcname)가 정의됩니다. 매크로를 쓰든 NULL과 비교하든 개인 취향입니다.
Redis 모듈에 구성 파라미터 전달
모듈이 MODULE LOAD 명령으로 로드되거나 redis.conf의 loadmodule 지시어를 쓸 때, 사용자는 모듈 파일 이름 뒤에 인수를 추가해 구성 파라미터를 모듈에 전달할 수 있습니다:
loadmodule mymodule.so foo bar 1234
위 예에서 문자열 foo, bar, 1234가 argv 인수에서 RedisModuleString 포인터 배열로 모듈 OnLoad() 함수에 전달됩니다. 전달된 인수 개수는 argc에 있습니다.
그 문자열들에 접근하는 방법은 이 문서의 나머지에서 설명합니다. 보통 모듈은 모듈 구성 파라미터를 모듈 전체에서 접근할 수 있는 정적 전역 변수에 저장해, 구성이 서로 다른 명령의 동작을 바꿀 수 있게 합니다.
RedisModuleString 객체 다루기
모듈 명령에 전달되는 명령 인수 벡터 argv와 다른 모듈 API 함수의 반환 값은 RedisModuleString 타입입니다.
보통 모듈 문자열을 다른 API 호출에 직접 전달하지만, 때로 문자열 객체에 직접 접근해야 할 수 있습니다.
문자열 객체로 작업하는 몇 가지 함수:
const char *RedisModule_StringPtrLen(RedisModuleString *string, size_t *len);
위 함수는 문자열에 접근해 포인터를 반환하고 그 길이를 len에 설정합니다. const 포인터 한정자에서 보이듯 문자열 객체 포인터에 결코 써서는 안 됩니다.
원하면 다음 API로 새 문자열 객체를 만들 수 있습니다:
RedisModuleString *RedisModule_CreateString(RedisModuleCtx *ctx, const char *ptr, size_t len);
위 명령이 반환한 문자열은 RedisModule_FreeString()의 대응 호출로 해제해야 합니다:
void RedisModule_FreeString(RedisModuleString *str);
그러나 문자열 해제를 피하고 싶다면, 이 문서의 뒷부분에서 다루는 자동 메모리 관리가 대신 해주는 좋은 대안이 될 수 있습니다.
argv 인수 벡터를 통해 제공된 문자열은 결코 해제할 필요가 없습니다. 자신이 만든 새 문자열, 또는 반환된 문자열을 해제해야 한다고 명시된 다른 API가 반환한 새 문자열만 해제하면 됩니다.
숫자에서 문자열 만들기 또는 문자열을 숫자로 파싱
정수에서 새 문자열을 만드는 것은 매우 흔한 연산이므로 이를 위한 함수가 있습니다:
RedisModuleString *mystr = RedisModule_CreateStringFromLongLong(ctx,10);
마찬가지로 문자열을 숫자로 파싱하려면:
long long myval;
if (RedisModule_StringToLongLong(ctx,argv[1],&myval) == REDISMODULE_OK) {
/* Do something with 'myval' */
}
모듈에서 Redis 키 접근
대부분의 Redis 모듈은 유용하려면 Redis 데이터 공간과 상호작용해야 합니다(항상 그런 것은 아님. 예를 들어 ID 생성기는 Redis 키를 결코 건드리지 않을 수 있음). Redis 모듈은 Redis 데이터 공간에 접근하는 두 가지 서로 다른 API를 가집니다. 하나는 매우 빠른 접근과 Redis 데이터 구조를 조작하는 함수 집합을 제공하는 저수준 API입니다. 다른 API는 더 고수준으로, Lua 스크립트가 Redis에 접근하는 것처럼 Redis 명령을 호출하고 결과를 가져올 수 있게 합니다.
고수준 API는 API로는 사용할 수 없는 Redis 기능에 접근하는 데도 유용합니다.
일반적으로 모듈 개발자는 저수준 API를 선호해야 합니다. 저수준 API로 구현된 명령이 네이티브 Redis 명령과 비슷한 속도로 실행되기 때문입니다. 그러나 고수준 API를 위한 사용 사례도 분명히 있습니다. 예를 들어 종종 병목은 데이터 접근이 아니라 데이터 처리일 수 있습니다.
또한 때로 저수준 API를 쓰는 것이 고수준 API보다 어렵지 않다는 점에 주의하세요.
Redis 명령 호출
Redis에 접근하는 고수준 API는 RedisModule_Call() 함수와, Call()이 반환한 응답 객체에 접근하는 데 필요한 함수들의 합입니다.
RedisModule_Call은 특별한 호출 규약을 사용하며, 함수에 인수로 전달하는 객체의 종류를 지정하는 데 쓰는 형식 지정자(format specifier)가 있습니다.
Redis 명령은 단지 명령 이름과 인수 목록으로 호출됩니다. 그러나 명령을 호출할 때 인수는 서로 다른 종류의 문자열에서 비롯될 수 있습니다: null-종결 C 문자열, 명령 구현의 argv 파라미터로 받은 RedisModuleString 객체, 포인터와 길이가 있는 바이너리 안전 C 버퍼 등.
예를 들어 첫 인수(키)는 인수 벡터 argv(RedisModuleString 객체 포인터 배열)에서 받은 문자열, 둘째 인수(증분)는 숫자 "10"을 나타내는 C 문자열로 INCRBY를 호출하려면 다음 함수 호출을 씁니다:
RedisModuleCallReply *reply;
reply = RedisModule_Call(ctx,"INCRBY","sc",argv[1],"10");
첫 인수는 컨텍스트, 둘째는 항상 명령 이름이 있는 null-종결 C 문자열입니다. 셋째는 형식 지정자로, 각 문자가 뒤따르는 인수의 타입에 해당합니다. 위 경우 "sc"는 RedisModuleString 객체와 null-종결 C 문자열을 의미합니다. 다른 인수들은 지정된 두 인수일 뿐입니다. 실제로 argv[1]은 RedisModuleString이고 "10"은 null-종결 C 문자열입니다.
형식 지정자의 전체 목록:
c— Null-종결 C 문자열 포인터b— C 버퍼, 두 인수 필요: C 문자열 포인터와 size_t 길이s— argv에서 또는 RedisModuleString 객체를 반환하는 다른 Redis 모듈 API에서 받은 RedisModuleStringl— Long long 정수v— RedisModuleString 객체 배열!— 이 수정자는 명령을 레플리카와 AOF에 복제하라고 함수에 알립니다. 인수 파싱 관점에서는 무시됩니다A—!가 주어졌을 때 AOF 전파를 억제하라고 알리는 수정자: 명령이 레플리카에만 전파됨R—!가 주어졌을 때 레플리카 전파를 억제하라고 알리는 수정자: 명령이 활성화되면 AOF에만 전파됨
함수는 성공 시 RedisModuleCallReply 객체를 반환하고, 오류 시 NULL을 반환합니다.
NULL은 명령 이름이 유효하지 않거나, 형식 지정자가 인식되지 않는 문자를 쓰거나, 명령이 잘못된 인수 개수로 호출될 때 반환됩니다. 위 경우 errno 변수가 EINVAL로 설정됩니다. Cluster가 활성화된 인스턴스에서 대상 키가 로컬이 아닌 해시 슬롯에 관한 것일 때도 NULL이 반환됩니다. 이 경우 errno가 EPERM으로 설정됩니다.
RedisModuleCallReply 객체 다루기
RedisModule_Call은 RedisModule_CallReply* 함수 계열로 접근할 수 있는 응답 객체를 반환합니다.
응답의 타입(Redis 프로토콜이 지원하는 데이터 타입 중 하나에 해당)을 얻으려면 RedisModule_CallReplyType() 함수를 씁니다:
reply = RedisModule_Call(ctx,"INCRBY","sc",argv[1],"10");
if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
long long myval = RedisModule_CallReplyInteger(reply);
/* Do something with myval. */
}
유효한 응답 타입:
REDISMODULE_REPLY_STRING— Bulk string 또는 상태 응답REDISMODULE_REPLY_ERROR— 오류REDISMODULE_REPLY_INTEGER— 부호 있는 64비트 정수REDISMODULE_REPLY_ARRAY— 응답 배열REDISMODULE_REPLY_NULL— NULL 응답
문자열, 오류, 배열은 연관된 길이를 가집니다. 문자열·오류의 경우 길이는 문자열 길이에 해당합니다. 배열의 경우 길이는 요소 수입니다. 응답 길이를 얻으려면 다음 함수를 씁니다:
size_t reply_len = RedisModule_CallReplyLength(reply);
정수 응답의 값을 얻으려면 위 예에서 본 것처럼 다음 함수를 씁니다:
long long reply_integer_val = RedisModule_CallReplyInteger(reply);
잘못된 타입의 응답 객체로 호출하면 위 함수는 항상 LLONG_MIN을 반환합니다.
배열 응답의 하위 요소는 이렇게 접근합니다:
RedisModuleCallReply *subreply;
subreply = RedisModule_CallReplyArrayElement(reply,idx);
위 함수는 범위 밖 요소에 접근하려 하면 NULL을 반환합니다.
문자열과 오류(문자열과 같지만 다른 타입)는 다음 방식으로 접근할 수 있으며, 결과 포인터(오용이 매우 명시적이도록 const 포인터로 반환됨)에 결코 써서는 안 됩니다:
size_t len;
char *ptr = RedisModule_CallReplyStringPtr(reply,&len);
응답 타입이 문자열이나 오류가 아니면 NULL이 반환됩니다.
RedisCallReply 객체는 모듈 문자열 객체(RedisModuleString 타입)와 같지 않습니다. 그러나 때로 모듈 문자열을 기대하는 API 함수에 string 또는 integer 타입의 응답을 전달해야 할 수 있습니다.
그런 경우, 저수준 API를 쓰는 것이 명령 구현의 더 간단한 방법인지 평가하거나, string, error, integer 타입의 call reply에서 새 문자열 객체를 만드는 다음 함수를 쓸 수 있습니다:
RedisModuleString *mystr = RedisModule_CreateStringFromCallReply(myreply);
응답이 올바른 타입이 아니면 NULL이 반환됩니다. 반환된 문자열 객체는 평소처럼 RedisModule_FreeString()으로 또는 자동 메모리 관리(해당 섹션 참고)를 활성화해 해제해야 합니다.
Call reply 객체 해제
응답 객체는 RedisModule_FreeCallReply로 해제해야 합니다. 배열의 경우 최상위 응답만 해제하면 되고 중첩 응답은 해제할 필요가 없습니다. 현재 모듈 구현은 실수로 중첩 응답 객체를 해제해도 크래시를 피하는 보호를 제공하지만, 이 기능이 영원히 여기 있으란 보장은 없으므로 API의 일부로 간주해서는 안 됩니다.
자동 메모리 관리(이 문서의 뒷부분에서 설명)를 쓰면 응답을 해제할 필요가 없습니다(그래도 가능한 한 빨리 메모리를 해제하고 싶다면 할 수 있음).
Redis 명령에서 값 반환
일반 Redis 명령처럼 모듈로 구현된 새 명령은 호출자에게 값을 반환할 수 있어야 합니다. API는 이 목적을 위한 함수 집합을 내보내며, Redis 프로토콜의 보통 타입과 그런 타입을 요소로 하는 배열을 반환합니다. 또한 어떤 오류 문자열·코드로도 오류를 반환할 수 있습니다(오류 코드는 "BUSY the sever is busy" 오류 메시지의 "BUSY" 문자열처럼 오류 메시지의 처음 대문자들).
클라이언트에 응답을 보내는 모든 함수는 RedisModule_ReplyWith<something>이라 합니다.
오류를 반환하려면:
RedisModule_ReplyWithError(RedisModuleCtx *ctx, const char *err);
잘못된 타입의 키에 대한 미리 정의된 오류 문자열이 있습니다:
REDISMODULE_ERRORMSG_WRONGTYPE
예:
RedisModule_ReplyWithError(ctx,"ERR invalid arguments");
위 예에서 long long으로 응답하는 방법을 이미 보았습니다:
RedisModule_ReplyWithLongLong(ctx,12345);
이진 값이나 개행을 포함할 수 없는(그래서 "OK" 같은 짧은 단어를 보내기에 적합한) simple string으로 응답하려면:
RedisModule_ReplyWithSimpleString(ctx,"OK");
바이너리 안전한 "bulk strings"로 응답하는 것은 두 함수로 가능합니다:
int RedisModule_ReplyWithStringBuffer(RedisModuleCtx *ctx, const char *buf, size_t len);
int RedisModule_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str);
첫 함수는 C 포인터와 길이를 받습니다. 둘째는 RedisModuleString 객체를 받습니다. 손에 든 소스 타입에 따라 하나를 쓰세요.
배열로 응답하려면 배열 길이를 내보내는 함수를 쓰고, 그 뒤에 배열의 요소 수만큼 위 함수들을 호출하면 됩니다:
RedisModule_ReplyWithArray(ctx,2);
RedisModule_ReplyWithStringBuffer(ctx,"age",3);
RedisModule_ReplyWithLongLong(ctx,22);
중첩 배열을 반환하는 것은 쉽습니다. 중첩 배열 요소는 또 다른 RedisModule_ReplyWithArray() 호출 후 하위 배열 요소를 내보내는 호출을 쓰면 됩니다.
동적 길이의 배열 반환
때로 배열의 항목 수를 미리 알 수 없습니다. 예를 들어 숫자가 주어지면 소인수를 출력하는 FACTOR 명령을 구현하는 Redis 모듈을 생각해 보세요. 숫자를 소인수분해하고, 소인수를 배열에 저장했다가 나중에 명령 응답을 만드는 대신, 길이를 아직 모르는 배열 응답을 시작하고 나중에 설정하는 더 나은 해결책이 있습니다. 이는 RedisModule_ReplyWithArray()의 특별 인수로 이루어집니다:
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
위 호출은 배열 응답을 시작해, 배열 항목을 만들기 위해 다른 ReplyWith 호출을 쓸 수 있습니다. 마지막으로 길이를 설정하려면:
RedisModule_ReplySetArrayLength(ctx, number_of_items);
FACTOR 명령의 경우 이는 이와 유사한 코드로 해석됩니다:
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
number_of_factors = 0;
while(still_factors) {
RedisModule_ReplyWithLongLong(ctx, some_factor);
number_of_factors++;
}
RedisModule_ReplySetArrayLength(ctx, number_of_factors);
이 기능의 또 다른 일반 사용 사례는 어떤 컬렉션의 배열을 순회해 어떤 종류의 필터링을 통과하는 것만 반환하는 것입니다.
연기된 응답을 가진 여러 중첩 배열이 가능합니다. 각 SetArray() 호출은 가장 최근의 대응 ReplyWithArray() 호출의 길이를 설정합니다:
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
... generate 100 elements ...
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
... generate 10 elements ...
RedisModule_ReplySetArrayLength(ctx, 10);
RedisModule_ReplySetArrayLength(ctx, 100);
이는 100개 항목 배열을 만들고, 마지막 요소가 10개 항목 배열입니다.
Arity와 타입 검사
종종 명령이 인수 개수와 키 타입이 올바른지 검사해야 합니다. 잘못된 arity를 보고하려면 RedisModule_WrongArity()라는 특정 함수가 있습니다. 사용은 사소합니다:
if (argc != 2) return RedisModule_WrongArity(ctx);
잘못된 타입 검사는 키를 열고 타입을 확인하는 것을 포함합니다:
RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
REDISMODULE_READ|REDISMODULE_WRITE);
int keytype = RedisModule_KeyType(key);
if (keytype != REDISMODULE_KEYTYPE_STRING &&
keytype != REDISMODULE_KEYTYPE_EMPTY)
{
RedisModule_CloseKey(key);
return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
}
키가 예상 타입이거나 비어 있을 때 둘 다 명령을 진행하고 싶을 때가 종종 있다는 점에 주의하세요.
키에 대한 저수준 접근
키에 대한 저수준 접근은 키에 연관된 값 객체에 직접 연산을 수행하게 해, Redis가 내장 명령을 구현하는 데 내부적으로 쓰는 것과 유사한 속도로 합니다.
키가 열리면 다른 모든 저수준 API 호출과 함께 키 또는 연관 값에 연산을 수행하는 데 쓰일 키 포인터가 반환됩니다.
API가 매우 빠르도록 설계되었으므로 런타임 검사를 너무 많이 할 수 없습니다. 그래서 사용자는 따라야 할 특정 규칙을 알아야 합니다:
- 같은 키를 여러 번 여는 것 중 적어도 하나가 쓰기용으로 열린 경우는 미정의이고 크래시로 이어질 수 있음
- 키가 열려 있는 동안에는 저수준 키 API로만 접근해야 함. 예를 들어 키를 연 다음
RedisModule_Call()API로 같은 키에DEL을 호출하면 크래시가 발생함. 그러나 키를 열고, 저수준 API로 연산을 수행하고, 닫은 다음, 다른 API로 같은 키를 관리하고, 나중에 다시 열어 더 작업하는 것은 안전함
키를 열려면 RedisModule_OpenKey 함수를 씁니다. 값에 접근·수정하는 데 앞으로 쓸 키 포인터를 반환합니다:
RedisModuleKey *key;
key = RedisModule_OpenKey(ctx,argv[1],REDISMODULE_READ);
둘째 인수는 RedisModuleString 객체여야 하는 키 이름입니다. 셋째 인수는 모드: REDISMODULE_READ 또는 REDISMODULE_WRITE. |로 두 모드를 비트 OR 해 키를 두 모드 모두로 열 수 있습니다. 현재 쓰기용으로 열린 키는 읽기로도 접근할 수 있지만 이는 구현 세부 사항으로 간주해야 합니다. 제대로 된 모듈에서는 올바른 모드를 써야 합니다.
키에 쓰기를 시도할 때 키가 생기므로, 존재하지 않는 키를 쓰기용으로 열 수 있습니다. 그러나 읽기 전용으로 키를 열 때 키가 존재하지 않으면 RedisModule_OpenKey가 NULL을 반환합니다.
키 사용이 끝나면 다음으로 닫을 수 있습니다:
RedisModule_CloseKey(key);
자동 메모리 관리가 활성화되면 키를 닫을 필요가 없다는 점에 주의하세요. 모듈 함수가 반환하면 Redis가 여전히 열려 있는 모든 키를 닫을 책임을 집니다.
키 타입 얻기
키의 값을 얻으려면 RedisModule_KeyType() 함수를 씁니다:
int keytype = RedisModule_KeyType(key);
다음 값 중 하나를 반환합니다:
REDISMODULE_KEYTYPE_EMPTY
REDISMODULE_KEYTYPE_STRING
REDISMODULE_KEYTYPE_LIST
REDISMODULE_KEYTYPE_HASH
REDISMODULE_KEYTYPE_SET
REDISMODULE_KEYTYPE_ZSET
위는 보통의 Redis 키 타입들에, 키 포인터가 아직 존재하지 않는 빈 키와 연관되어 있음을 알리는 empty 타입이 더해진 것입니다.
새 키 만들기
새 키를 만들려면 쓰기용으로 열고, 키 쓰기 함수 중 하나로 씁니다. 예:
RedisModuleKey *key;
key = RedisModule_OpenKey(ctx,argv[1],REDISMODULE_WRITE);
if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
RedisModule_StringSet(key,argv[2]);
}
키 삭제
그냥 쓰면:
RedisModule_DeleteKey(key);
키가 쓰기용으로 열려 있지 않으면 함수가 REDISMODULE_ERR을 반환합니다. 키가 삭제된 후에는 새 키 명령의 대상이 되도록 설정됩니다. 예를 들어 RedisModule_KeyType()은 빈 키라고 반환하고, 쓰면 (쓴 API에 따라) 어쩌면 다른 타입의 새 키가 만들어집니다.
키 만료(TTL) 관리
키 만료를 제어하기 위해 키에 연관된 time to live를 설정, 수정, 얻기, 해제할 수 있는 두 함수가 제공됩니다.
열린 키의 현재 만료를 조회하는 함수:
mstime_t RedisModule_GetExpire(RedisModuleKey *key);
이 함수는 키의 time to live를 밀리초로 반환하거나, 키에 연관된 만료가 없거나 존재하지 않음을 알리는 특별 값 REDISMODULE_NO_EXPIRE를 반환합니다(키 타입이 REDISMODULE_KEYTYPE_EMPTY인지 확인해 두 경우를 구별할 수 있음).
키의 만료를 변경하려면 다음 함수를 씁니다:
int RedisModule_SetExpire(RedisModuleKey *key, mstime_t expire);
존재하지 않는 키에 호출하면 REDISMODULE_ERR이 반환됩니다. 함수가 기존 열린 키에만 만료를 연관시킬 수 있기 때문입니다(존재하지 않는 열린 키는 데이터 타입 특정 쓰기 연산으로 새 값을 만드는 데만 유용함).
다시 만료 시간은 밀리초로 지정됩니다. 키에 현재 만료가 없으면 새 만료가 설정됩니다. 키에 이미 만료가 있으면 새 값으로 교체됩니다.
키에 만료가 있고 새 만료로 특별 값 REDISMODULE_NO_EXPIRE를 쓰면, PERSIST 명령처럼 만료가 제거됩니다. 키가 이미 영속적이면 아무 연산도 수행되지 않습니다.
값의 길이 얻기
열린 키에 연관된 값의 길이를 검색하는 단일 함수가 있습니다. 반환된 길이는 값-특정적이며, 문자열은 문자열 길이, 집계 데이터 타입(list, set, sorted set, hash의 요소 수)은 요소 개수입니다.
size_t len = RedisModule_ValueLength(key);
키가 존재하지 않으면 함수가 0을 반환합니다.
String 타입 API
Redis SET 명령처럼 새 문자열 값을 설정하는 것은 다음으로 수행됩니다:
int RedisModule_StringSet(RedisModuleKey *key, RedisModuleString *str);
이 함수는 Redis SET 명령 자체와 정확히 동작합니다. 즉 이전 값(어떤 타입이든)이 있으면 삭제합니다.
기존 문자열 값에 접근하는 것은 속도를 위해 DMA(직접 메모리 접근)로 수행됩니다. API가 포인터와 길이를 반환하므로 문자열에 직접 접근하고 필요하면 수정할 수 있습니다.
size_t len, j;
char *myptr = RedisModule_StringDMA(key,&len,REDISMODULE_WRITE);
for (j = 0; j < len; j++) myptr[j] = 'A';
위 예에서 문자열에 직접 씁니다. 쓰려면 WRITE 모드를 요청해야 한다는 점에 유의하세요.
DMA 포인터는 DMA 호출 후, 포인터를 쓰기 전에 키로 다른 연산을 수행하지 않을 때만 유효합니다.
때로 문자열을 직접 조작할 때 크기도 바꿔야 합니다. 이 목적에 RedisModule_StringTruncate 함수가 쓰입니다. 예:
RedisModule_StringTruncate(mykey,1024);
이 함수는 필요에 따라 문자열을 잘라내거나 늘리며, 이전 길이가 요청한 새 길이보다 작으면 0 바이트로 패딩합니다. 키가 열린 빈 키와 연관되어 문자열이 존재하지 않으면 문자열 값이 만들어져 키에 연관됩니다.
StringTruncate()를 호출할 때마다 이전 것은 유효하지 않을 수 있으므로 DMA 포인터를 다시 얻어야 합니다.
List 타입 API
리스트 값에서 push·pop할 수 있습니다:
int RedisModule_ListPush(RedisModuleKey *key, int where, RedisModuleString *ele);
RedisModuleString *RedisModule_ListPop(RedisModuleKey *key, int where);
두 API 모두 where 인수가 tail에서 push·pop할지 head에서 할지 지정합니다:
REDISMODULE_LIST_HEAD
REDISMODULE_LIST_TAIL
RedisModule_ListPop()이 반환한 요소는 RedisModule_CreateString()으로 만든 문자열과 같아, RedisModule_FreeString()으로 또는 자동 메모리 관리를 활성화해 해제해야 합니다.
Set 타입 API
작업 중입니다(Work in progress).
Sorted set 타입 API
문서 누락. 다음 함수는 module.c의 상단 주석을 참조하세요:
RedisModule_ZsetAddRedisModule_ZsetIncrbyRedisModule_ZsetScoreRedisModule_ZsetRem
그리고 sorted set 반복자:
RedisModule_ZsetRangeStopRedisModule_ZsetFirstInScoreRangeRedisModule_ZsetLastInScoreRangeRedisModule_ZsetFirstInLexRangeRedisModule_ZsetLastInLexRangeRedisModule_ZsetRangeCurrentElementRedisModule_ZsetRangeNextRedisModule_ZsetRangePrevRedisModule_ZsetRangeEndReached
Hash 타입 API
문서 누락. 다음 함수는 module.c의 상단 주석을 참조하세요:
RedisModule_HashSetRedisModule_HashGet
집계 값 순회
작업 중입니다.
명령 복제
복제된 Redis 인스턴스 맥락이나 AOF 파일 영속성에서 모듈 명령을 일반 Redis 명령처럼 사용하려면, 모듈 명령이 복제를 일관되게 처리하는 것이 중요합니다.
고수준 API로 명령을 호출할 때, 다음 예처럼 RedisModule_Call()의 형식 문자열에 "!" 수정자를 쓰면 복제가 자동으로 발생합니다:
reply = RedisModule_Call(ctx,"INCRBY","!sc",argv[1],"10");
보시다시피 형식 지정자는 "!sc"입니다. 느낌표는 형식 지정자로 파싱되지 않고, 내부적으로 명령을 "복제해야 함"으로 표시합니다.
위 프로그래밍 스타일을 쓰면 문제가 없습니다. 그러나 때로 상황이 더 복잡하고 저수준 API를 씁니다. 그 경우 명령 실행에 부작용이 없고 항상 일관되게 같은 작업을 수행한다면, 명령을 사용자가 실행한 그대로(verbatim) 복제할 수 있습니다. 그러려면 다음 함수를 호출하면 됩니다:
RedisModule_ReplicateVerbatim(ctx);
위 API를 쓸 때는 다른 복제 함수를 쓰면 안 됩니다. 잘 섞인다는 보장이 없기 때문입니다.
그러나 이것이 유일한 옵션은 아닙니다. RedisModule_Call()과 유사하지만 명령을 호출하는 대신 AOF/레플리카 스트림에 보내는 API로, 명령 실행의 결과로 어떤 명령을 복제할지 Redis에 정확히 알릴 수도 있습니다. 예:
RedisModule_Replicate(ctx,"INCRBY","cl","foo",my_increment);
RedisModule_Replicate를 여러 번 호출할 수 있고, 각각 명령 하나를 내보냅니다. 내보내진 전체 시퀀스가 MULTI/EXEC 트랜잭션으로 감싸져, AOF·복제 효과가 단일 명령 실행과 같습니다.
Redis 7.0 이전에는, 두 형태의 복제를 섞고 싶다면(더 단순한 접근이 있으므로 반드시 좋은 생각은 아님) Call() 복제와 Replicate() 복제 사이에 규칙이 있습니다. Call()로 복제된 명령은 항상 최종 MULTI/EXEC 블록에서 처음 내보내지고, Replicate()로 내보낸 모든 명령은 뒤따릅니다.
자동 메모리 관리
보통 C 언어로 프로그램을 쓸 때 프로그래머는 메모리를 수동으로 관리해야 합니다. 그래서 Redis 모듈 API는 문자열 해제, 열린 키 닫기, 응답 해제 등의 함수를 가집니다.
그러나 명령이 포함된 환경과 엄격한 API 집합으로 실행되므로, Redis는 모듈에 자동 메모리 관리를 제공할 수 있고, 그 대가로 약간의 성능(대부분의 경우 매우 낮은 비용)이 듭니다.
자동 메모리 관리가 활성화되면:
- 열린 키를 닫을 필요 없음
- 응답을 해제할 필요 없음
- RedisModuleString 객체를 해제할 필요 없음
그래도 원하면 할 수 있습니다. 예를 들어 자동 메모리 관리가 활성화되어도 문자열을 많이 할당하는 루프 안에서는 더 이상 쓰지 않는 문자열을 해제하고 싶을 수 있습니다.
자동 메모리 관리를 활성화하려면 명령 구현 시작에 다음 함수를 호출하면 됩니다:
RedisModule_AutoMemory(ctx);
자동 메모리 관리가 보통 가야 할 길이지만, 경험 많은 C 프로그래머는 속도와 메모리 사용 이점을 얻기 위해 쓰지 않을 수 있습니다.
모듈에 메모리 할당
일반 C 프로그램은 malloc()과 free()로 동적으로 메모리를 할당·해제합니다. Redis 모듈에서 malloc 사용이 기술적으로 금지되지는 않지만, malloc, free, realloc, strdup의 정확한 대체물인 Redis Modules 특정 함수를 쓰는 것이 훨씬 좋습니다. 그 함수들:
void *RedisModule_Alloc(size_t bytes);
void* RedisModule_Realloc(void *ptr, size_t bytes);
void RedisModule_Free(void *ptr);
void RedisModule_Calloc(size_t nmemb, size_t size);
char *RedisModule_Strdup(const char *str);
그것들은 libc의 동등 호출과 정확히 동작하지만, Redis가 쓰는 것과 같은 할당자를 쓰고, 이 함수들로 할당된 메모리는 INFO 명령의 memory 섹션에서 보고되며, maxmemory 정책을 강제할 때 계산되고, 일반적으로 Redis 실행 파일의 시민입니다. 반면 모듈이 libc malloc()으로 할당한 것은 Redis에 투명합니다.
메모리 할당에 모듈 함수를 쓰는 또 다른 이유는, 모듈 내부에서 네이티브 데이터 타입을 만들 때 RDB 로딩 함수가 역직렬화된 문자열(RDB 파일에서)을 RedisModule_Alloc() 할당으로 직접 반환할 수 있으므로, 로드 후 데이터 구조에 복사하는 대신 직접 데이터 구조를 채우는 데 쓸 수 있기 때문입니다.
풀 할당자 (Pool allocator)
때로 명령 구현에서 명령 실행 끝에 유지되지 않을, 명령 자체를 실행하는 데만 기능적인 많은 작은 할당을 수행해야 합니다.
이 작업은 Redis 풀 할당자를 쓰면 더 쉽게 수행할 수 있습니다:
void *RedisModule_PoolAlloc(RedisModuleCtx *ctx, size_t bytes);
malloc()과 유사하게 동작하며, bytes보다 크거나 같은 다음 2의 거듭제곱(최대 정렬 8바이트)에 정렬된 메모리를 반환합니다. 그러나 메모리를 블록으로 할당하므로 할당 오버헤드가 작고, 더 중요하게는 명령이 반환될 때 할당된 메모리가 자동으로 해제됩니다.
그래서 일반적으로 수명이 짧은 할당은 풀 할당자의 좋은 후보입니다.
Redis Cluster 호환 명령 작성
문서 누락. module.c에서 다음 함수를 확인하세요:
RedisModule_IsKeysPositionRequest(ctx);
RedisModule_KeyAtPos(ctx,pos);
더 알아보기 (Learn more)
- Redis 모듈 API 레퍼런스 (module.c 상단 주석)
- Redis 모듈에서 블로킹 명령 구현
- Redis 모듈에서 네이티브 타입 사용