中文交流群
LLM (큰 언어 모델)을 Redis와 통합하는 Redis-Llm이라는 Redis 모듈을 만듭니다. 질문을하면서 Redis-Plus-Plus를 배울 수 있습니다.
이것은 Redis의 C ++ 클라이언트 라이브러리입니다. HiredIS를 기반으로하며 C ++ 17, C ++ 14 및 C ++ 11과 호환됩니다.
참고 : 저는 원어민이 아닙니다. 따라서 문서가 명확하지 않으면 문제를 열거 나 요청을 시작하십시오. 최대한 빨리 응답하겠습니다.
마스터 브랜치는 모든 테스트를 통과하는 안정적인 지점입니다. 개발 분기는 불안정합니다. 기여하려면 Dev Branch에서 풀 요청을 작성하십시오.
Redis-Plus-Plus는 Hiredis를 기반으로하므로 먼저 HiredIS를 설치해야합니다. Hiredis 의 최소 버전 요구 사항은 V0.12.1 입니다. 그러나 Hiredis 의 최신 안정적인 릴리스는 항상 권장됩니다.
참고 : Hiredis의 버전이 1 개만 설치되어 있는지 확인해야합니다. 그렇지 않으면 유선 문제가 발생할 수 있습니다. 예를 들면 다음과 같은 문제를 확인하십시오. 발행 135, Issue 140 및 Issue 158.
일반적으로 C ++ 패키지 관리자와 함께 HiredIS를 설치할 수 있습니다. 예를 들어 sudo apt-get install libhiredis-dev 같은 가장 쉬운 방법입니다. 그러나 최신 HiredIS 코드 또는 지정된 버전 (예 : Async 지원이 Hiredis v1.0.0 이상)을 설치하려면 소스에서 설치할 수 있습니다.
다시 참고 : 여러 버전의 HiredIS를 설치하지 마십시오.
git clone https://github.com/redis/hiredis.git
cd hiredis
make
make install기본적으로 Hiredis는 /usr /local 에 설치됩니다. 비 디폴트 위치에 HiredIS를 설치하려면 다음 명령을 사용하여 설치 경로를 지정하십시오.
make PREFIX=/non/default/path
make PREFIX=/non/default/path installRedis-Plus-Plus는 Cmake와 함께 제작되었습니다.
git clone https://github.com/sewenew/redis-plus-plus.git
cd redis-plus-plus
mkdir build
cd build
cmake ..
make
make install
cd .. HiredIS가 비 디펜트 위치에 설치되면 CMAKE_PREFIX_PATH 사용하여 HiredIS 의 설치 경로를 지정해야합니다. 기본적으로 Redis-Plus-Plus는 /usr /local 에 설치됩니다. 그러나 CMAKE_INSTALL_PREFIX 사용하여 비 디펜션 위치에 Redis-Plus-Plus를 설치할 수 있습니다.
cmake -DCMAKE_PREFIX_PATH=/path/to/hiredis -DCMAKE_INSTALL_PREFIX=/path/to/install/redis-plus-plus .. 버전 1.3.0이므로 기본적으로 Redis-Plus-Plus는 -std=c++17 표준으로 구축됩니다. std :: string_view 및 std :: 옵션 기능을 사용할 수 있습니다. 그러나 -std=c++11 또는 -std=c++14 표준으로도 구축 할 수 있으며,이 경우 std::string_view 및 std::optional 의 간단한 구현이 있습니다. C ++ 표준을 명시 적으로 지정하려면 다음 Cmake 플래그를 사용할 수 있습니다. -DREDIS_PLUS_PLUS_CXX_STANDARD=11 .
cmake -DCMAKE_PREFIX_PATH=/path/to/hiredis -DCMAKE_INSTALL_PREFIX=/path/to/install/redis-plus-plus -DREDIS_PLUS_PLUS_CXX_STANDARD=11 ..참고 : C ++ 17 표준을 사용하여 Redis-Plus-Plus 를 구축하는 경우 C ++ 17 표준으로 응용 프로그램 코드를 구축해야합니다.
Redis-Plus-Plus를 컴파일 할 때는 테스트 프로그램을 컴파일하여 시간이 걸릴 수 있습니다. 그러나 다음 cmake 옵션을 사용하여 빌드 테스트를 비활성화 할 수 있습니다. -DREDIS_PLUS_PLUS_BUILD_TEST=OFF .
cmake -DCMAKE_PREFIX_PATH=/path/to/hiredis -DCMAKE_INSTALL_PREFIX=/path/to/install/redis-plus-plus -DREDIS_PLUS_PLUS_BUILD_TEST=OFF .. 기본적으로 Redis-Plus-Plus는 정적 라이브러리와 공유 라이브러리를 모두 구축합니다. 그중 하나만 빌드하려면 -DREDIS_PLUS_PLUS_BUILD_STATIC=OFF 또는 -DREDIS_PLUS_PLUS_BUILD_SHARED=OFF 로 다른 하나를 비활성화 할 수 있습니다.
Redis-Plus-Plus는 -fPIC 옵션, 즉 위치 독립 코드를 갖춘 정적 라이브러리를 기본적으로 빌드합니다. 그러나 -DREDIS_PLUS_PLUS_BUILD_STATIC_WITH_PIC=OFF 로 비활성화 할 수 있습니다.
이제 Hiredis는 Windows 지원을 받았으며 Visual Studio 2017 이후 Visual Studio는 CMAKE를 지원합니다. 따라서 Redis-Plus-Plus는 현재 Windows 플랫폼도 지원합니다. Visual Studio 2017에서 완전히 테스트되었고 나중에 Win 10에서 완전히 테스트되었습니다. Visual Studio 환경에 익숙하지 않으며 다음 문서는 정확하지 않을 수 있습니다. Windows 플랫폼에 익숙하다면 Windows에 Redis-Plus-Plus를 설치하는 방법에 대해이 문서를 자유롭게 업데이트하십시오.
다음은 Visual Studio 2017 이상으로 Cmake 프로젝트를 구축하는 방법에 대한 링크입니다. 익숙하지 않은 경우 먼저이 지침을 읽는 것이 좋습니다.
참고 : IMHO, Visual Studio 2017의 CMAKE 프로젝트 지원은 그다지 성숙하지 않으며 Visual Studio 2019를 사용하여 Hiredis 및 *Redis-Plus-Plus를 구축하는 것이 좋습니다.
우선, 마스터 브랜치에서 최신 고용 코드를 가져와야합니다. 이전 버전은 Windows 플랫폼을 지원하지 않을 수 있습니다. Hiredis 의 cmakelists.txt는 cmake 3.12 이상에서만 지원되는 add_compile_definitions 메소드를 사용합니다. 그러나 Visual Studio 2017의 Cmake 버전은 그보다 오래되었습니다. 따라서 Visual Studio 2017을 사용하는 경우 cmakelists.txt 파일에 다음 줄을 주석해야합니다.
#IF(WIN32)
# ADD_COMPILE_DEFINITIONS(_CRT_SECURE_NO_WARNINGS WIN32_LEAN_AND_MEAN)
#ENDIF()Open Folder 기능을 사용하여 HiredIS 프로젝트를 열고 위에서 언급 한 지침 (링크)으로 빌드 할 수 있습니다.
Redis-Plus-Plus는 Hiredis 에 의존하기 때문에 구축하기 전에 HiredIS 의 설치 경로를 지정해야합니다. Open Folder 기능을 사용하여 Redis-Plus-Plus 프로젝트를 열 수 있습니다. cmakesetting.json 파일 (Visual Studio에서 자동으로 생성)을 편집하려면 Hiredis_header , Hiredis_lib 및 Test_hiredis_lib 변수를 설정하려면 Hiredis 헤더의 설치 경로, Hiredis Dynamic 라이브러리의 설치 경로 및 Hiredis STATIC 라이브러리의 설치 경로를 지정해야합니다. 다음은 cmakesetting.json 파일의 예입니다.
{
"configurations" : [
{
"name" : " x64-Release " ,
"generator" : " Visual Studio 15 2017 Win64 " ,
"configurationType" : " Release " ,
"buildRoot" : " ${env.LOCALAPPDATA} \ CMakeBuild \ ${workspaceHash} \ build \ ${name} " ,
"cmakeCommandArgs" : " " ,
"buildCommandArgs" : " -m -v:minimal " ,
"variables" : [
{
"name" : " HIREDIS_HEADER " ,
"value" : " installation path of hiredis header files " ,
"type" : " PATH "
},
{
"name" : " HIREDIS_LIB " ,
"value" : " installation path of dynamic library of hiredis " ,
"type" : " FILEPATH "
},
{
"name" : " TEST_HIREDIS_LIB " ,
"value" : " installation path of static library of hiredis " ,
"type" : " FILEPATH "
}
]
}
]
} 그런 다음 위에서 언급 한 지침 (링크)을 빌드 할 수 있습니다. 디버그 모드에서 Visual Studio 2017로 구축하는 경우 테스트를 구축 할 때 /bigobj 오류가 발생할 수 있습니다. 이 경우 -DREDIS_PLUS_PLUS_BUILD_TEST=OFF 또는 릴리스 모드에서 빌드하여 빌드 테스트를 비활성화 할 수 있습니다.
메모 :
REDIS_PLUS_PLUS_CXX_STANDARD cmake 옵션을 11으로 설정할 수 있습니다.Visual Studio를 사용하여 프로젝트를 구축하고 이에 대해 궁금한 점이 있으시면 아래 단계를 따르십시오. 다음은 Visual Studio 2022 커뮤니티에서 테스트되었습니다.
# download two projects into this folder
mkdir redis ++
cd redis ++
# make sure you create a hiredis first to work as a library
mkdir hiredis - lib
cd hiredis - lib
mkdir lib
git clone https: // github.com / redis / hiredis.git
cd hiredis 지금까지 각 단계마다 괜찮을 것입니다. 그런 다음 CMakeLists.txt 파일을 엽니 다. 다음 줄을 수정하고 댓글을 달아주십시오
...
# SET(CMAKE_DEBUG_POSTFIX d)
...그런 다음 Hiredis Project 폴더로 돌아갑니다
mkdir build
cd build
# convert project into visual studio 2022, if necessary choose you version e.g 19 2019 etc.
cmake - G " Visual Studio 17 2022 " ..
. / hiredis.sln 스타트 업 프로젝트로 hiredis 설정 한 다음 디버그 모드에서 Build Solution 클릭하십시오.
성공적인 빌드 후 Debug 아래의 모든 파일을 hiredis-lib/lib 폴더로 복사하십시오.
여기서 Hiredis의 작업은 완료되어야합니다.
그런 다음 redis++ 폴더로 돌아갑니다. 여기서 터미널을 열십시오
git clone https: // github.com / sewenew / redis - plus - plus.git
cd redis - plus - plus
mkdir build
cd build 이제 PC에 항상 OpenSSL이 있어야합니다. 그렇지 않으면 초콜릿을 사용하여 설치할 수 있습니다. Visual Studio 2022의 경우이 링크를 따라 vpckg 사용하여 PTHREAD를 별도로 설치하십시오.
모든 준비 후. 모든 프로젝트를 변환하려면
cmake - DCMAKE_PREFIX_PATH = " $ ( ABSOLUTE_PATH ) hiredis-lib " - G " Visual Studio 17 2022 " ..
cd build
. / redis ++ .sln redis++_static 시작 프로젝트로 설정 한 다음 Build Solution 클릭하십시오
지금까지 빌드가 성공적으로 완료되었습니다!
Windows 플랫폼에서 응용 프로그램 코드도 Windows.h를 포함 해야하는 경우. sw/redis ++/redis ++. h가 Windows 에 포함되어 있는지 확인해야합니다. 자세한 내용은이 문제를 확인하십시오.
GNU/데비안 패키지 구축에 대한 기본 지원에는 CMAKE 사용이 제공됩니다. 다음 예제는 데비안 패키지를 만드는 방법을 보여줍니다.
mkdir build ; cd build
cmake ..
cpack -G DEB설치 접두사는 다음과 같이 수정 될 수 있습니다.
mkdir build ; cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
cpack -G DEBRedis-Plus-Plus는 다음 컴파일러로 완전히 테스트되었습니다.
gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
gcc version 5.5.0 20171010 (Ubuntu 5.5.0-12ubuntu1)
gcc version 6.5.0 20181026 (Ubuntu 6.5.0-2ubuntu1~18.04)
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)
gcc version 8.3.0 (Ubuntu 8.3.0-6ubuntu1~18.04.1)
gcc version 9.2.1 20191008 (Ubuntu 9.2.1-9ubuntu2)
gcc version 10.2.1 20210110 (Debian 10.2.1-6)
clang version 3.9.1-19ubuntu1 (tags/RELEASE_391/rc2)
clang version 4.0.1-10 (tags/RELEASE_401/final)
clang version 5.0.1-4 (tags/RELEASE_501/final)
clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
clang version 7.0.0-3~ubuntu0.18.04.1 (tags/RELEASE_700/final)
clang version 8.0.1-3build1 (tags/RELEASE_801/final)
Apple clang version 11.0.0 (clang-1100.0.33.12)
Visual Studio 2017 (Win 10)
Visual Studio 2019 (Win 10) -DREDIS_PLUS_PLUS_BUILD_TEST=ON (기본 동작, -DREDIS_PLUS_PLUS_BUILD_TEST=OFF 로 빌드 테스트를 비활성화 할 수있는 Redis -Plus -Plus를 구축하면 빌드/ 테스트 디렉토리에서 테스트 프로그램을 얻을 수 있습니다.
테스트를 실행하려면 Redis 인스턴스와 Redis 클러스터를 설정해야합니다. 테스트 프로그램은 대부분의 Redis 명령을 서버 및 클러스터로 보내므로 최신 버전의 Redis를 설정해야합니다. 그렇지 않으면 테스트가 실패 할 수 있습니다. 예를 들어, 테스트를 위해 Redis 4.0을 설정하면 ZPOPMAX 명령 (REDIS 5.0 명령)을 서버로 보내려고 할 때 테스트 프로그램이 실패합니다. 다른 Redis 버전으로 테스트를 실행하려면 Redis-Plus-Plus/SRC/SW/Redis ++/ Directory의 테스트 소스 파일에서 Redis가 지원하지 않은 명령을 주석해야합니다. 불편을 드려 죄송합니다.이 문제를 해결하여 미래에 테스트 프로그램이 모든 버전의 Redis와 함께 작동하도록하겠습니다.
참고 : 최신 버전의 Redis는 테스트를 실행하기위한 요구 사항 일뿐입니다. 실제로, 당신은 모든 버전 (즉, redis 2.0 이상)의 Redis와 함께 Redis-plus-plus를 사용할 수 있습니다.
테스트 프로그램이 읽거나 쓰는 키가 응용 프로그램과 충돌 할 수 있으므로 프로덕션 장애에서 테스트 프로그램을 실행 하지 마십시오 .
Redis 및 Redis 클러스터로 테스트를 실행하려면 다음 명령으로 테스트 프로그램을 실행할 수 있습니다.
./build/test/test_redis++ -h host -p port -a auth -n cluster_node -c cluster_portRedis로 만 테스트를 실행하려면 호스트 , 포트 및 인증 옵션 만 지정하면됩니다.
./build/test/test_redis++ -h host -p port -a auth마찬가지로 Redis 클러스터로 테스트를 실행하려면 Cluster_Node , Cluster_port 및 Auth 옵션을 지정합니다.
./build/test/test_redis++ -a auth -n cluster_node -c cluster_port기본적으로 테스트 프로그램은 멀티 스레드 환경에서 Redis-Plus-Plus를 실행하지 않습니다. 오랜 시간이 걸릴 수있는 멀티 스레드 테스트를하려면 -m 옵션을 지정할 수 있습니다.
./build/test/test_redis++ -h host -p port -a auth -n cluster_node -c cluster_port -m모든 테스트가 통과 된 경우 테스트 프로그램은 다음 메시지를 인쇄합니다.
Pass all tests그렇지 않으면 오류 메시지를 인쇄합니다.
Redis-Plus-Plus는 Hiredis 의 포장지이기 때문에 Hiredis 만큼 빠르게 실행됩니다. 벤치 마크 모드에서 test_redis ++를 실행하여 환경의 성능을 확인할 수 있습니다.
./build/test/test_redis++ -h host -p port -a auth -n cluster_node -c cluster_port -b -t thread_num -s connection_pool_size -r request_num -k key_len -v val_len10 기본적으로.5 기본적으로.100000 .10 기본적으로.10 기본적으로. Bechmark는 테스트를 위해 100 랜덤 바이너리 키를 생성 하며이 키의 크기는 key_len 에 의해 지정됩니다. 벤치 마크가 실행되면이 키를 읽고 읽고 쓰게됩니다. 따라서 프로덕션 환경에서 테스트 프로그램을 실행 하지 마십시오 . 그렇지 않으면 데이터가 부정확하게 삭제 될 수 있습니다.
코드를 컴파일하면 공유 라이브러리와 정적 라이브러리가 모두 얻을 수 있습니다. Redis-Plus-Plus는 HiredIS 에 따라 다르므로 두 라이브러리를 응용 프로그램에 연결해야합니다. 또한 C ++ 표준, -std=c++17 , -std=c++14 또는 -std=c++11 을 지정하고 스레드 관련 옵션을 지정하는 것을 잊지 마십시오.
GCC를 예로 들어보십시오.
g++ -std=c++17 -o app app.cpp /path/to/libredis++.a /path/to/libhiredis.a -pthread Hiredis 및 Redis-Plus-Plus가 비 기본 위치에 설치되면 -I 옵션을 사용하여 헤더 경로를 지정해야합니다.
g++ -std=c++17 -I/non-default/install/include/path -o app app.cpp /path/to/libredis++.a /path/to/libhiredis.a -pthreadg++ -std=c++17 -o app app.cpp -lredis++ -lhiredis -pthread Hiredis 및 Redis-Plus-Plus가 비 디폴트 위치에 설치된 경우 -I 및 -L 옵션을 사용하여 헤더 및 라이브러리 경로를 지정해야합니다.
g++ -std=c++17 -I/non-default/install/include/path -L/non-default/install/lib/path -o app app.cpp -lredis++ -lhiredis -pthread공유 라이브러리와 링크하고 응용 프로그램을 실행할 때 다음 오류 메시지가 표시 될 수 있습니다.
error while loading shared libraries: xxx: cannot open shared object file: No such file or directory. 링커가 공유 라이브러리를 찾을 수 없기 때문입니다. 문제를 해결하기 위해 HiredIS 및 Redis-Plus-Plus 라이브러리를 설치 한 경로를 LD_LIBRARY_PATH 환경 변수에 추가 할 수 있습니다. 예를 들어:
export LD_LIBRARY_PATH= $LD_LIBRARY_PATH :/usr/local/lib문제 해결 방법에 대한 자세한 내용은이 stackoverflow 질문을 확인하십시오.
CMAKE를 사용하여 응용 프로그램을 구축하는 경우 cmakelists.txt 에 Hiredis 및 Redis-Plus-Plus 의존성을 추가해야합니다.
# <---------- set c++ standard ------------->
# NOTE: you must build redis-plus-plus and your application code with the same standard.
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_STANDARD_REQUIRED ON )
# <------------ add hiredis dependency --------------->
find_path (HIREDIS_HEADER hiredis)
target_include_directories ( target PUBLIC ${HIREDIS_HEADER} )
find_library (HIREDIS_LIB hiredis)
target_link_libraries ( target ${HIREDIS_LIB} )
# <------------ add redis-plus-plus dependency -------------->
# NOTE: this should be *sw* NOT *redis++*
find_path (REDIS_PLUS_PLUS_HEADER sw)
target_include_directories ( target PUBLIC ${REDIS_PLUS_PLUS_HEADER} )
find_library (REDIS_PLUS_PLUS_LIB redis++)
target_link_libraries ( target ${REDIS_PLUS_PLUS_LIB} )cmakelists.txt 의 완전한 예는이 문제를 참조하십시오.
또한 Default가 아닌 위치에 Hiredis 및 Redis-Plus-Plus를 설치 한 경우 CMAKE_PREFIX_PATH 옵션으로 CMAKE를 실행 하여이 두 라이브러리의 설치 경로를 지정해야합니다.
cmake -DCMAKE_PREFIX_PATH=/installation/path/to/the/two/libs ..
# include < sw/redis++/redis++.h >
using namespace sw ::redis ;
try {
// Create an Redis object, which is movable but NOT copyable.
auto redis = Redis ( " tcp://127.0.0.1:6379 " );
// ***** STRING commands *****
redis. set ( " key " , " val " );
auto val = redis. get ( " key " ); // val is of type OptionalString. See 'API Reference' section for details.
if (val) {
// Dereference val to get the returned value of std::string type.
std::cout << *val << std::endl;
} // else key doesn't exist.
// ***** LIST commands *****
// std::vector<std::string> to Redis LIST.
std::vector<std::string> vec = { " a " , " b " , " c " };
redis. rpush ( " list " , vec. begin (), vec. end ());
// std::initializer_list to Redis LIST.
redis. rpush ( " list " , { " a " , " b " , " c " });
// Redis LIST to std::vector<std::string>.
vec. clear ();
redis. lrange ( " list " , 0 , - 1 , std::back_inserter (vec));
// ***** HASH commands *****
redis. hset ( " hash " , " field " , " val " );
// Another way to do the same job.
redis. hset ( " hash " , std::make_pair ( " field " , " val " ));
// std::unordered_map<std::string, std::string> to Redis HASH.
std::unordered_map<std::string, std::string> m = {
{ " field1 " , " val1 " },
{ " field2 " , " val2 " }
};
redis. hmset ( " hash " , m. begin (), m. end ());
// Redis HASH to std::unordered_map<std::string, std::string>.
m. clear ();
redis. hgetall ( " hash " , std::inserter (m, m. begin ()));
// Get value only.
// NOTE: since field might NOT exist, so we need to parse it to OptionalString.
std::vector<OptionalString> vals;
redis. hmget ( " hash " , { " field1 " , " field2 " }, std::back_inserter (vals));
// ***** SET commands *****
redis. sadd ( " set " , " m1 " );
// std::unordered_set<std::string> to Redis SET.
std::unordered_set<std::string> set = { " m2 " , " m3 " };
redis. sadd ( " set " , set. begin (), set. end ());
// std::initializer_list to Redis SET.
redis. sadd ( " set " , { " m2 " , " m3 " });
// Redis SET to std::unordered_set<std::string>.
set. clear ();
redis. smembers ( " set " , std::inserter (set, set. begin ()));
if (redis. sismember ( " set " , " m1 " )) {
std::cout << " m1 exists " << std::endl;
} // else NOT exist.
// ***** SORTED SET commands *****
redis. zadd ( " sorted_set " , " m1 " , 1.3 );
// std::unordered_map<std::string, double> to Redis SORTED SET.
std::unordered_map<std::string, double > scores = {
{ " m2 " , 2.3 },
{ " m3 " , 4.5 }
};
redis. zadd ( " sorted_set " , scores. begin (), scores. end ());
// Redis SORTED SET to std::vector<std::pair<std::string, double>>.
// NOTE: The return results of zrangebyscore are ordered, if you save the results
// in to `std::unordered_map<std::string, double>`, you'll lose the order.
std::vector<std::pair<std::string, double >> zset_result;
redis. zrangebyscore ( " sorted_set " ,
UnboundedInterval< double >{}, // (-inf, +inf)
std::back_inserter (zset_result));
// Only get member names:
// pass an inserter of std::vector<std::string> type as output parameter.
std::vector<std::string> without_score;
redis. zrangebyscore ( " sorted_set " ,
BoundedInterval< double >( 1.5 , 3.4 , BoundType::CLOSED), // [1.5, 3.4]
std::back_inserter (without_score));
// Get both member names and scores:
// pass an back_inserter of std::vector<std::pair<std::string, double>> as output parameter.
std::vector<std::pair<std::string, double >> with_score;
redis. zrangebyscore ( " sorted_set " ,
BoundedInterval< double >( 1.5 , 3.4 , BoundType::LEFT_OPEN), // (1.5, 3.4]
std::back_inserter (with_score));
// ***** SCRIPTING commands *****
// Script returns a single element.
auto num = redis. eval < long long >( " return 1 " , {}, {});
// Script returns an array of elements.
std::vector<std::string> nums;
redis. eval ( " return {ARGV[1], ARGV[2]} " , {}, { " 1 " , " 2 " }, std::back_inserter (nums));
// mset with TTL
auto mset_with_ttl_script = R"(
local len = #KEYS
if (len == 0 or len + 1 ~= #ARGV) then return 0 end
local ttl = tonumber(ARGV[len + 1])
if (not ttl or ttl <= 0) then return 0 end
for i = 1, len do redis.call("SET", KEYS[i], ARGV[i], "EX", ttl) end
return 1
)" ;
// Set multiple key-value pairs with TTL of 60 seconds.
auto keys = { " key1 " , " key2 " , " key3 " };
std::vector<std::string> args = { " val1 " , " val2 " , " val3 " , " 60 " };
redis. eval < long long >(mset_with_ttl_script, keys. begin (), keys. end (), args. begin (), args. end ());
// ***** Pipeline *****
// Create a pipeline.
auto pipe = redis. pipeline ();
// Send mulitple commands and get all replies.
auto pipe_replies = pipe . set ( " key " , " value " )
. get ( " key " )
. rename ( " key " , " new-key " )
. rpush ( " list " , { " a " , " b " , " c " })
. lrange ( " list " , 0 , - 1 )
. exec ();
// Parse reply with reply type and index.
auto set_cmd_result = pipe_replies. get < bool >( 0 );
auto get_cmd_result = pipe_replies. get <OptionalString>( 1 );
// rename command result
pipe_replies. get < void >( 2 );
auto rpush_cmd_result = pipe_replies. get < long long >( 3 );
std::vector<std::string> lrange_cmd_result;
pipe_replies. get ( 4 , back_inserter (lrange_cmd_result));
// ***** Transaction *****
// Create a transaction.
auto tx = redis. transaction ();
// Run multiple commands in a transaction, and get all replies.
auto tx_replies = tx. incr ( " num0 " )
. incr ( " num1 " )
. mget ({ " num0 " , " num1 " })
. exec ();
// Parse reply with reply type and index.
auto incr_result0 = tx_replies. get < long long >( 0 );
auto incr_result1 = tx_replies. get < long long >( 1 );
std::vector<OptionalString> mget_cmd_result;
tx_replies. get ( 2 , back_inserter (mget_cmd_result));
// ***** Generic Command Interface *****
// There's no *Redis::client_getname* interface.
// But you can use *Redis::command* to get the client name.
val = redis. command <OptionalString>( " client " , " getname " );
if (val) {
std::cout << *val << std::endl;
}
// Same as above.
auto getname_cmd_str = { " client " , " getname " };
val = redis. command <OptionalString>(getname_cmd_str. begin (), getname_cmd_str. end ());
// There's no *Redis::sort* interface.
// But you can use *Redis::command* to send sort the list.
std::vector<std::string> sorted_list;
redis. command ( " sort " , " list " , " ALPHA " , std::back_inserter (sorted_list));
// Another *Redis::command* to do the same work.
auto sort_cmd_str = { " sort " , " list " , " ALPHA " };
redis. command (sort_cmd_str. begin (), sort_cmd_str. end (), std::back_inserter (sorted_list));
// ***** Redis Cluster *****
// Create a RedisCluster object, which is movable but NOT copyable.
auto redis_cluster = RedisCluster ( " tcp://127.0.0.1:7000 " );
// RedisCluster has similar interfaces as Redis.
redis_cluster. set ( " key " , " value " );
val = redis_cluster. get ( " key " );
if (val) {
std::cout << *val << std::endl;
} // else key doesn't exist.
// Keys with hash-tag.
redis_cluster. set ( " key{tag}1 " , " val1 " );
redis_cluster. set ( " key{tag}2 " , " val2 " );
redis_cluster. set ( " key{tag}3 " , " val3 " );
std::vector<OptionalString> hash_tag_res;
redis_cluster. mget ({ " key{tag}1 " , " key{tag}2 " , " key{tag}3 " },
std::back_inserter (hash_tag_res));
} catch ( const Error &e) {
// Error handling.
}Doxygen 스타일 문서는 redis.h를 볼 수 있습니다.
Redis 클래스는 Redis 서버에 대한 연결 풀을 유지합니다. 연결이 끊어지면 Redis Redis 서버에 자동으로 다시 연결됩니다.
ConnectionOptions 및 ConnectionPoolOptions 로 Redis 인스턴스를 초기화 할 수 있습니다. ConnectionOptions Redis 서버에 대한 연결 옵션을 지정하고 ConnectionPoolOptions Conneciton 풀에 대한 옵션을 지정합니다. ConnectionPoolOptions 선택 사항입니다. 지정되지 않으면 Redis Redis 서버에 대한 단일 연결을 유지합니다.
ConnectionOptions connection_options;
connection_options.host = " 127.0.0.1 " ; // Required.
connection_options.port = 6666 ; // Optional. The default port is 6379.
connection_options.password = " auth " ; // Optional. No password by default.
connection_options.db = 1 ; // Optional. Use the 0th database by default.
// Optional. Timeout before we successfully send request to or receive response from redis.
// By default, the timeout is 0ms, i.e. never timeout and block until we send or receive successfuly.
// NOTE: if any command is timed out, we throw a TimeoutError exception.
connection_options.socket_timeout = std::chrono::milliseconds( 200 );
// Connect to Redis server with a single connection.
Redis redis1 (connection_options);
ConnectionPoolOptions pool_options;
pool_options.size = 3 ; // Pool size, i.e. max number of connections.
// Optional. Max time to wait for a connection. 0ms by default, which means wait forever.
// Say, the pool size is 3, while 4 threds try to fetch the connection, one of them will be blocked.
pool_options.wait_timeout = std::chrono::milliseconds( 100 );
// Optional. Max lifetime of a connection. 0ms by default, which means never expire the connection.
// If the connection has been created for a long time, i.e. more than `connection_lifetime`,
// it will be expired and reconnected.
pool_options.connection_lifetime = std::chrono::minutes( 10 );
// Connect to Redis server with a connection pool.
Redis redis2 (connection_options, pool_options); 참고 : ConnectionOptions::socket_timeout 설정하고 차단 명령을 호출하려면 Redis::brpop , Redis::blpop , Redis::bzpopmax , Redis::bzpopmin :: ConnectionOptions::socket_timeout 이 차단 명령을 지정한 시간대보다 더 커야합니다. 그렇지 않으면 TimeoutError 얻고 메시지를 잃을 수 있습니다.
더 많은 옵션은 연결 및 연결 풀로 옵션을 참조하십시오. 연결 풀에서 논의하려면 80 호를 참조하십시오.
참고 : Redis 클래스는 움직일 수 있지만 복사 할 수 없습니다.
// auto redis3 = redis1; // this won't compile.
// But it's movable.
auto redis3 = std::move(redis1);Redis-Plus-Plus는 Unix 도메인 소켓으로 Redis 서버에 연결하는 것을 지원합니다.
ConnectionOptions options;
options.type = ConnectionType::UNIX;
options.path = " /path/to/socket " ;
Redis redis (options);URI로 Redis 서버에 연결할 수도 있습니다.
tcp://[[username:]password@]host[:port][/db]
redis://[[username:]password@]host[:port][/db]
unix://[[username:]password@]path-to-unix-domain-socket[/db]
체계 와 호스트 부품은 필요하며 다른 부분은 선택 사항입니다. UNIX 도메인 소켓으로 Redis에 연결하는 경우 UNIX 구성표를 사용해야합니다. 그렇지 않으면 TCP 또는 Redis 구성표를 사용해야합니다. 다음은 해당 옵션 부품의 기본값 목록입니다.
참고 : 비밀번호 또는 사용자 이름에 '@'또는 사용자 이름에 ':'가 포함 된 경우 URI로 Redis 객체를 구성 할 수 없습니다. Redis-Plus-Plus는 URI를 잘못 구문 분석하기 때문입니다. 이 경우 Redis 객체를 구성하기 위해 ConnectionOptions 사용해야합니다.
참고 : Redis 6.0은 ACL을 지원하며 연결의 사용자 이름을 지정할 수 있습니다. 그러나 Redis 6.0 이전에는 그렇게 할 수 없습니다.
또한 다음 연결 옵션 및 연결 풀 옵션은 URI의 쿼리 문자열 (예 : TCP : //127.0.0.1?
| 옵션 | 매개 변수 | 기본 |
|---|---|---|
ConnectionOptions::user | 사용자 | 기본 |
ConnectionOptions::password | 비밀번호 | 빈 문자열, 즉 비밀번호가 없습니다 |
ConnectionOptions::db | DB | 0 |
ConnectionOptions::keep_alive | Keep_alive | 거짓 |
ConnectionOptions::connect_timeout | connect_timeout | 0ms |
ConnectionOptions::socket_timeout | socket_timeout | 0ms |
ConnectionOptions::resp | resp | 2 |
ConnectionPoolOptions::size | pool_size | 1 |
ConnectionPoolOptions::wait_timeout | pool_wait_timeout | 0ms |
ConnectionPoolOptions::connection_lifetime | pool_connection_lifetime | 0ms |
ConnectionPoolOptions::connection_idle_time | pool_connection_idle_time | 0ms |
메모 :
// Single connection to the given host and port.
Redis redis1 ( " tcp://127.0.0.1:6666 " );
// Use default port, i.e. 6379.
Redis redis2 ( " tcp://127.0.0.1 " );
// Connect to Redis with password, and default port.
Redis redis3 ( " tcp://[email protected] " );
// Connect to Redis and select the 2nd (db number starts from 0) database.
Redis redis4 ( " tcp://127.0.0.1:6379/2 " );
// Set keep_alive option to true with query string.
Redis redis5 ( " tcp://127.0.0.1:6379/2?keep_alive=true " );
// Set socket_timeout to 50 milliseconds, and connect_timeout to 1 second with query string.
Redis redis6 ( " tcp://127.0.0.1?socket_timeout=50ms&connect_timeout=1s " );
// Connect to Unix Domain Socket.
Redis redis7 ( " unix://path/to/socket " ); Redis 6.0이므로 새로운 버전의 Redis 프로토콜 (예 : RESP3)을 지원합니다. 이 새로운 프로토콜을 사용하려면 ConnectionOptions::resp 3으로 설정해야합니다.
ConnectionOptions opts;
opts.resp = 3;
// Set other options...
기본적으로 ConnectionOptions::resp 는 2, 즉 RESP 버전 2를 사용합니다. 지금까지 버전 2와 3 만 지원되며 ConnectionOptions::resp 다른 숫자로 설정하면 동작이 정의되지 않습니다.
참고 :이 새로운 프로토콜을 사용하려면 최신 HiredIS를 설치해야합니다 (Hiredis-V1.0.2조차도 RESP3 지원에 대한 버그가 있습니다).
수영장의 연결은 게으르게 생성됩니다. 연결 풀이 초기화되면 Redis 의 생성자 Redis 서버에 연결되지 않습니다. 대신 명령을 보내려고 할 때만 서버에 연결됩니다. 이런 식으로, 우리는 불필요한 연결을 피할 수 있습니다. 따라서 풀 크기가 5이지만 최대 동시 연결 횟수는 3이면 풀에는 3 개의 연결 만 있습니다.
Redis 객체가 서버에 성공적으로 연결되는지 확인할 필요가 없습니다. Redis Redis 서버에 연결을 생성하지 못하거나 언젠가 연결이 끊어지면 Redis 와 함께 명령을 보내려고 할 때 유형 Error 제외합니다. 예외가 발생하더라도 연결이 고장 났을 때 새로운 Redis 객체를 만들 필요가 없습니다. Redis 객체를 재사용하여 명령을 보낼 수 있으며 Redis 객체는 자동으로 서버에 다시 연결하려고합니다. 성공적으로 다시 연결하면 서버로 명령을 보냅니다. 그렇지 않으면 다시 예외가 발생합니다.
예외에 대한 자세한 내용은 예외 섹션을 참조하십시오.
Redis 서버에 새로운 연결을 생성하기 때문에 Redis 객체를 만드는 것은 저렴하지 않습니다. 따라서 가능한 한 Redis 물체를 재사용하는 것이 좋습니다. 또한 멀티 스레드 환경에서 Redis 의 멤버 기능을 호출하는 것이 안전하며 여러 스레드에서 Redis 객체를 공유 할 수 있습니다.
// This is GOOD practice.
auto redis = Redis( " tcp://127.0.0.1 " );
for ( auto idx = 0 ; idx < 100 ; ++idx) {
// Reuse the Redis object in the loop.
redis. set ( " key " , " val " );
}
// This is VERY BAD! It's very inefficient.
// NEVER DO IT!!!
for ( auto idx = 0 ; idx < 100 ; ++idx) {
// Create a new Redis object for each iteration.
auto redis = Redis ( " tcp://127.0.0.1 " );
redis. set ( " key " , " val " );
}Redis-Plus-Plus는 또한 TLS 지원이 있습니다. 그러나이 기능을 사용하려면 Hiredis 및 Redis-Plus-Plus를 구축 할 때이를 활성화해야합니다.
참고 : 지금까지 TLS 기능은 Windows 플랫폼에서 테스트되지 않았습니다. 나는 앞으로 그것을 고칠 것이다.
TLS 지원으로 HiredIS를 구축 할 때 버전 v1.0.0 또는 후자의 Hiredis를 다운로드하고 USE_SSL=1 플래그를 지정해야합니다.
make PREFIX=/non/default/path USE_SSL=1
make PREFIX=/non/default/path USE_SSL=1 install 그런 다음 -DREDIS_PLUS_PLUS_USE_TLS=ON 옵션을 지정하여 TLS 지원을 활성화하기 위해 Redis-Plus-Plus를 구축 할 수 있습니다.
cmake -DREDIS_PLUS_PLUS_USE_TLS=ON ..TLS 지원으로 Redis에 연결하려면 다음 연결 옵션을 지정해야합니다.
ConnectionOptions opts;
opts.host = " 127.0.0.1 " ;
opts.port = 6379 ;
opts.tls.enabled = true ; // Required. `false` by default.
opts.tls.cert = " /path/to/client/certificate " ; // Optional
opts.tls.key = " /path/to/private/key/file " ; // Optional
opts.tls.cacert = " /path/to/CA/certificate/file " ; // You can also set `opts.tls.cacertdir` instead.
opts.tls.sni = " server-name-indication " ; // Optional tls.cert 및 tls.key 는 선택 사항이지만 그 중 하나를 지정하면 다른 것도 지정해야합니다. tls.cacert 지정하는 대신 인증서가 저장된 디렉토리에 tls.cacertdir 지정할 수도 있습니다.
이 옵션은 redis-cli 의 TLS 관련 명령 줄 인수와 동일하므로 redis-cli --help 실행하여 이러한 옵션에 대한 자세한 설명을 얻을 수도 있습니다.
그런 다음이 ConnectionOptions 사용하여 Redis 객체를 생성하여 TLS 지원으로 Redis 서버에 연결할 수 있습니다.
참고 : 응용 프로그램 코드를 작성할 때는 libhiredis.a , libhiredis_ssl.a , libredis++.a (또는 해당 공유 라이브러리), -lssl 및 -lcrypto 와 링크해야합니다.
기본적으로 Redis-Plus-Plus는 OpenSSL 라이브러리를 자동으로 초기화하며 IE는 SSL_library_init 호출하고 필요한 경우 잠금을 초기화합니다. 그러나 응용 프로그램 코드는 이미 OpenSSL 라이브러리를 초기화 할 수 있습니다. 이 경우 tls::disable_auto_init() 에게 호출하여 초기화를 비활성화 할 수 있습니다. 이 기능을 한 번만 호출하여 다른 Redis-Plus-Plus 작업 전에 호출해야합니다. 그렇지 않으면 행동이 정의되지 않습니다.
Hiredis v1.1.0 이후 인증서 검증 건너 뛰기를 지원합니다. 이 기능을 Redis-Plus-Plus 와 함께 사용하려면이 문제를 확인할 수 있습니다.
Redis 객체를 통해 Redis 명령을 보낼 수 있습니다. Redis 각 Redis 명령에 대해 하나 이상의 (과부하) 메소드가 있습니다. 이 메소드는 해당 명령과 동일 (하단) 이름을 갖습니다. 예를 들어 DEL key [key ...] 명령에 대한 3 가지 과부하 메소드가 있습니다.
// Delete a single key.
long long Redis::del ( const StringView &key);
// Delete a batch of keys: [first, last).
template < typename Input>
long long Redis::del (Input first, Input last);
// Delete keys in the initializer_list.
template < typename T>
long long Redis::del (std::initializer_list<T> il);입력 매개 변수를 사용하면 이러한 메소드는 Redis 프로토콜을 기반으로 Redis 명령을 작성하고 명령을 Redis 서버로 보냅니다. 그런 다음 응답을 동기식으로 받고 구문 분석하고 발신자에게 돌아갑니다.
이러한 방법의 매개 변수 및 리턴 값을 자세히 살펴 보겠습니다.
이러한 방법의 대부분은 해당 명령과 동일한 매개 변수를 가지고 있습니다. 다음은 매개 변수 유형 목록입니다.
| 매개 변수 유형 | 설명 | 예 | 메모 |
|---|---|---|---|
| StringView | 문자열 유형의 매개 변수. 일반적으로 키, 값, 멤버 이름, 필드 이름 등에 사용됩니다. | bool redis :: HSET (const StringView & Key, Const StringView & Field, Const StringView & Val) | StringView 에 대한 자세한 내용은 StringView 섹션을 참조하십시오 |
| 긴 | 정수 유형의 매개 변수. 일반적으로 인덱스 (예 : 목록 명령) 또는 정수에 사용됩니다 | void ltrim (const StringView & Key, Long Long Start, Long Long Stop) 긴 긴 감소 (Const StringView & Key, 긴 긴 감소) | |
| 더블 | 부동 소수점 유형의 매개 변수. 일반적으로 점수 (예 : 정렬 된 세트 명령) 또는 부동 소수점 유형에 사용됩니다. | Double IncrbyFloat (const StringView & Key, Double Dycrement) | |
| std :: Chrono :: 지속 시간 std :: Chrono :: Time_point | 시간 관련 매개 변수 | BOOL EXPIRE (Const StringView & Key, Const STD :: Chrono :: Seconds & TimeOut) bool expireat (const stringview & key, const std :: chrono :: time_point <std :: system_clock, std :: chrono :: seconds> & tp) | |
| std :: pair <stringView, StringView> | Redis Hash의 (필드, 값) 쌍에 사용됩니다 | BOOL HSET (const StringView & Key, const std :: pair <StringView, StringView> & Item) | |
| std :: pair <double, double> | Redis Geo (경도, 위도) 쌍에 사용됩니다 | OptionalLonglong Georadius (const StringView & Key, Const STD :: Pair <Double, Double> & Location, Double Radius, Geounit Unit, Const StringView & Destination, Bool String_dist, Long Long Count) | |
| 반복자 쌍 | 한 쌍의 반복자를 사용하여 입력 범위를 지정하여 STL 컨테이너의 데이터를 이러한 방법으로 전달할 수 있습니다. | 템플릿 <typename input> Long Long del (입력 먼저, 입력 마지막 입력) | 빈 범위라면 예외를 던지십시오, 즉 첫 == 마지막 |
| std :: initializer_list <t> | 이니셜 라이저 목록을 사용하여 입력 배치를 지정하십시오. | 템플릿 <typename t> Long Long del (std :: initializer_list <t> il) | |
| 몇 가지 옵션 | 일부 명령에 대한 옵션 | UpdateType , 템플릿 <typename t> class boundedInterval | 자세한 내용은 command_options.h를 참조하십시오 |
std :: string_view는 읽기 전용 문자열 매개 변수 유형에 적합한 선택입니다. 그러나 std::string_view c ++ 17 표준에만 소개되었으므로 -std=c++11 사용하여 Redis -Plus -Plus를 구축하는 경우 (즉, cmake 명령이 포함 된 -DREDIS_PLUS_PLUS_CXX_STANDARD=11 지정하여) 또는 std std::string_view , -std=c++14 , stringview, StringView 의 간단한 구현. -std=c++17 표준 (즉, 기본 동작) std::string_view 사용하여 Redis-Plus-Plus를 구축 할 수 있습니다. 그런 다음 std::string_view 와 별칭으로 StringView 구현이 무시됩니다. 이것은 using StringView = std::string_view Redis-Plus-Plus 라이브러리 내부에서 수행됩니다.
std::string 및 c 스타일 문자열에서 StringView 로 변환되므로 std::string 또는 c 스타일 문자열을 StringView 매개 변수가 필요한 메소드로 전달할 수 있습니다.
// bool Redis::hset(const StringView &key, const StringView &field, const StringView &val)
// Pass c-style string to StringView.
redis.hset( " key " , " field " , " value " );
// Pass std::string to StringView.
std::string key = " key " ;
std::string field = " field " ;
std::string val = " val " ;
redis.hset(key, field, val);
// Mix std::string and c-style string.
redis.hset(key, field, " value " );Redis 프로토콜은 5 가지 종류의 답변을 정의합니다.
long long 유지하기에 충분히 큽니다. 또한 이러한 답변은 무일하게 일 수 있습니다. 예를 들어, 존재하지 않는 키의 값을 GET 려고 할 때 Redis는 Null Bulk String Reply를 반환합니다.
위에서 언급했듯이 답변은 이러한 방법의 반환 값으로 구문 분석됩니다. 다음은 반환 유형의 목록입니다.
| 반환 유형 | 설명 | 예 | 메모 |
|---|---|---|---|
| 무효의 | 항상 "OK"문자열을 반환 해야하는 상태 답변 | 이름 바꾸기 , setex | |
| std :: 문자열 | 항상 "OK"를 반환하지 않는 상태 답변 및 대량 문자열 응답 | 핑 , 정보 | |
| 부 | 항상 0 또는 1을 반환하는 정수 응답 | 만료 , HSET | 부울 반환 값의 의미는 부울 반환 값 섹션을 참조하십시오. |
| 긴 | 항상 0 또는 1을 반환하지 않는 정수 응답 | 델 , 부록 | |
| 더블 | 이중을 나타내는 벌크 문자열 응답 | incrbyfloat , zincrby | |
| std :: 쌍 | 정확히 2 개의 요소가있는 배열 응답 . 리턴 값은 항상 2 요소의 배열이므로 2 개의 요소를 std::pair 의 첫 번째 및 두 번째 요소로 반환합니다. | BLPOP | |
| std :: 튜플 | 배열 길이는 고정 길이로 응답 하고 2 개 이상의 요소가 있습니다. 반환 된 배열의 길이가 고정되어 있으므로 배열을 std::tuple 로 반환합니다. | bzpopmax | |
| 출력 반복자 | 고정되지 않은/동적 길이로 일반 배열 응답 . STL과 같은 인터페이스를 사용하여 이러한 종류의 배열 회신을 반환하여 반환 값을 STL 컨테이너에 쉽게 삽입 할 수 있습니다. | mget , lrange | 또한 때로는 출력 반복자 유형이 명령과 함께 보낼 옵션을 결정합니다. 자세한 내용은 예제 섹션을 참조하십시오 |
| 선택 사항 <T> | 널가 될 수있는 T 형의 답변에 대해 | Get , LPOP , BLPOP , BZPOPMAX | Optional<T> 에 대한 자세한 내용은 옵션 섹션을 참조하십시오 |
| 변형 <args ...> | 다른 유형 일 수있는 답장을 위해 | 메모리 통계 | 참고 : 지금 까지이 유형은 C ++ 17 표준으로 Redis-Plus-Plus를 컴파일 할 때만 지원됩니다. 일반적으로 일반 명령 인터페이스와 함께 사용됩니다. Variant<Args...> 에 대한 자세한 내용은 변형 섹션을 참조하십시오 |
| STL 컨테이너 | 일반 배열 응답 | 구성하십시오 | 출력 반복기 와 STL 컨테이너는 배열 응답에 사용됩니다. 차이점은 STL 컨테이너가 일반적으로 일반 명령 인터페이스와 함께 사용된다는 것입니다. 예를 들어 STL 컨테이너 섹션을 참조하십시오 |
일부 방법의 리턴 유형 (예 : EXPIRE HSET 은 bool 입니다. 메소드가 false 반환한다고해서 Redis 명령을 Redis 서버로 보내지 못했음을 의미하지는 않습니다. 대신 Redis Server가 정수 응답을 반환하고 응답 값이 0 임을 의미합니다. 따라서 메소드가 true 반환하면 Redis Server가 정수 답변을 반환하고 응답 값이 1 임을 의미합니다. 0 과 1 의 STAND에 대한 Redis 명령 매뉴얼을 확인할 수 있습니다.
예를 들어, Redis Server로 EXPIRE 명령을 보낼 때 시간 초과가 설정된 경우 1 반환하고 키가 존재하지 않으면 0 반환합니다. 따라서 타임 아웃이 설정되면 Redis::expire true 반환하고 키가 존재하지 않으면 Redis::expire false 반환합니다.
따라서 리턴 값을 사용하여 명령이 Redis 서버로 성공적으로 전송되었는지 확인하지 마십시오. 대신 Redis 서버로 명령을 보내지 못하면 유형 Error 제외하고 발생합니다. 예외에 대한 자세한 내용은 예외 섹션을 참조하십시오.
std :: 옵션은 Redis가 NULL REPLY를 반환 할 수있는 경우 리턴 유형에 좋은 옵션입니다. 그러나 std::optional C ++ 17 표준으로 소개되며 -std=c++11 표준으로 Redis -Plus -Plus를 구축하는 경우 (즉 -DREDIS_PLUS_PLUS_CXX_STANDARD=11 지정하여 CMAKE 명령을 지정 함), 우리는 고유 한 간단한 버전 인 IE template Optional<T> 구현합니다. 대신 -std=c++17 표준 (즉, 기본 동작)을 사용하여 Redis-Plus-Plus를 구축하면 std::optional 사용할 수 있으며 template <typename T> using Optional = std::optional<T> ampleas가 있습니다.
get 및 mget 명령을 예로 들어보십시오.
// Or just: auto val = redis.get("key");
Optional<std::string> val = redis.get( " key " );
// Optional<T> has a conversion to bool.
// If it's NOT a null Optional<T> object, it's converted to true.
// Otherwise, it's converted to false.
if (val) {
// Key exists. Dereference val to get the string result.
std::cout << *val << std::endl;
} else {
// Redis server returns a NULL Bulk String Reply.
// It's invalid to dereference a null Optional<T> object.
std::cout << " key doesn't exist. " << std::endl;
}
std::vector<Optional<std::string>> values;
redis.mget({ " key1 " , " key2 " , " key3 " }, std::back_inserter(values));
for ( const auto &val : values) {
if (val) {
// Key exist, process the value.
}
} 우리는 또한 일반적으로 사용되는 Optional<T> 에 대한 일부 typedef가 있습니다.
using OptionalString = Optional<std::string>;
using OptionalLongLong = Optional< long long >;
using OptionalDouble = Optional< double >;
using OptionalStringPair = Optional<std::pair<std::string, std::string>>; STD :: Variant는 회신이 다른 유형 일 수있는 경우 리턴 유형에 적합한 옵션입니다. 예를 들어, MEMORY STATS 명령은 배열 회신을 반환합니다. 실제로는 키 값 구성 쌍의 맵입니다.
127.0.0.1: 6379> memory stats
1) " peak.allocated "
2) (integer) 4471104
...
17) " db.0 "
18) 1) " overhead.hashtable.main "
2) (integer) 104
3) " overhead.hashtable.expires "
4) (integer) 32
...
27) " dataset.percentage "
28) " 9.70208740234375 "
... 그러나 볼 수 있듯이 결과의 값 부분은 길이가 길어질 수 있습니다 (키 : 피크. 합당 ), Double (키 : DataSet.percentage ) 또는 맵 (키 : DB.0 ). 따라서 결과를 std::unordered_map<std::string, long long> 또는 std::unordered_map<std::string, double> 에 구문 분석 할 수는 없습니다. 해결 방법은 결과를 tuple 로 구문 분석하는 것이지만,이 튜플 솔루션은 추악하고 오류가 발생하기 쉽습니다. 자세한 내용은이 문제를 확인하십시오.
이 경우 C ++ 17 표준으로 Redis-Plus-Plus를 구축하면 std::variant 의 typedef 인 Variant 매우 유용합니다. 결과를 std::unordered_map<std::string, Variant<double, long long, std::unordered_map<std::string, long long>>> 로 구문 분석 할 수 있습니다.
using Var = Variant< double , long long , std::unordered_map<std::string, long long >>;
auto r = Redis( " tcp://127.0.0.1 " );
auto v = r.command<std::unordered_map<std::string, Var>>( " memory " , " stats " ); Variant 지원에는 몇 가지 제한 사항이 있습니다.
Variant 의 유형 인수는 중복 항목 Variant<double, long long, double> 가질 수 없습니다.std::string 앞에 double 배치해야합니다. double 응답은 실제로 문자열 회신이며 변형을 구문 분석 할 때 왼쪽에서 오른쪽으로 인수 유형으로 지정된 첫 번째 일치 유형에 대한 회신을 구문 분석하려고합니다. 따라서 std::string 의 오른쪽에있는 std::string 이후에 double 배치되면, 회신은 항상 std::string 으로 구문 분석됩니다.또한 일반 명령 섹션에서 일반 명령 인터페이스에 대한 더 많은 예를 확인하십시오.
일반 명령 인터페이스를 사용하는 경우 출력 반복자에 대한 응답을 구문 분석하는 대신 STL 컨테이너로 구문 분석 할 수도 있습니다.
auto r = Redis( " tcp://127.0.0.1 " );
auto v = r.command<std::unordered_map<std::string, std::string>>( " config " , " get " , " * " );또한 일반 명령 섹션에서 일반 명령 인터페이스에 대한 더 많은 예를 확인하십시오.
Redis Server에 명령을 보내는 방법에 대한 몇 가지 예를 살펴 보겠습니다.
// ***** Parameters of StringView type *****
// Implicitly construct StringView with c-style string.
redis.set( " key " , " value " );
// Implicitly construct StringView with std::string.
std::string key ( " key " );
std::string val ( " value " );
redis.set(key, val);
// Explicitly pass StringView as parameter.
std::vector< char > large_data;
// Avoid copying.
redis.set( " key " , StringView(large_data.data(), large_data.size()));
// ***** Parameters of long long type *****
// For index.
redis.bitcount(key, 1 , 3 );
// For number.
redis.incrby( " num " , 100 );
// ***** Parameters of double type *****
// For score.
redis.zadd( " zset " , " m1 " , 2.5 );
redis.zadd( " zset " , " m2 " , 3.5 );
redis.zadd( " zset " , " m3 " , 5 );
// For (longitude, latitude).
redis.geoadd( " geo " , std::make_tuple( " member " , 13.5 , 15.6 ));
// ***** Time-related parameters *****
using namespace std ::chrono ;
redis.expire(key, seconds( 1000 ));
auto tp = time_point_cast<seconds>(system_clock::now() + seconds( 100 ));
redis.expireat(key, tp);
// ***** Some options for commands *****
if (redis.set(key, " value " , milliseconds( 100 ), UpdateType::NOT_EXIST)) {
std::cout << " set OK " << std::endl;
}
redis.linsert( " list " , InsertPosition::BEFORE, " pivot " , " val " );
std::vector<std::string> res;
// (-inf, inf)
redis.zrangebyscore( " zset " , UnboundedInterval< double >{}, std::back_inserter(res));
// [3, 6]
redis.zrangebyscore( " zset " ,
BoundedInterval< double >( 3 , 6 , BoundType::CLOSED),
std::back_inserter (res));
// (3, 6]
redis.zrangebyscore( " zset " ,
BoundedInterval< double >( 3 , 6 , BoundType::LEFT_OPEN),
std::back_inserter (res));
// (3, 6)
redis.zrangebyscore( " zset " ,
BoundedInterval< double >( 3 , 6 , BoundType::OPEN),
std::back_inserter (res));
// [3, 6)
redis.zrangebyscore( " zset " ,
BoundedInterval< double >( 3 , 6 , BoundType::RIGHT_OPEN),
std::back_inserter (res));
// [3, +inf)
redis.zrangebyscore( " zset " ,
LeftBoundedInterval< double >( 3 , BoundType::RIGHT_OPEN),
std::back_inserter (res));
// (3, +inf)
redis.zrangebyscore( " zset " ,
LeftBoundedInterval< double >( 3 , BoundType::OPEN),
std::back_inserter (res));
// (-inf, 6]
redis.zrangebyscore( " zset " ,
RightBoundedInterval< double >( 6 , BoundType::LEFT_OPEN),
std::back_inserter (res));
// (-inf, 6)
redis.zrangebyscore( " zset " ,
RightBoundedInterval< double >( 6 , BoundType::OPEN),
std::back_inserter (res));
// ***** Pair of iterators *****
std::vector<std::pair<std::string, std::string>> kvs = {{ " k1 " , " v1 " }, { " k2 " , " v2 " }, { " k3 " , " v3 " }};
redis.mset(kvs.begin(), kvs.end());
std::unordered_map<std::string, std::string> kv_map = {{ " k1 " , " v1 " }, { " k2 " , " v2 " }, { " k3 " , " v3 " }};
redis.mset(kv_map.begin(), kv_map.end());
std::unordered_map<std::string, std::string> str_map = {{ " f1 " , " v1 " }, { " f2 " , " v2 " }, { " f3 " , " v3 " }};
redis.hmset( " hash " , str_map.begin(), str_map.end());
std::unordered_map<std::string, double > score_map = {{ " m1 " , 20 }, { " m2 " , 12.5 }, { " m3 " , 3.14 }};
redis.zadd( " zset " , score_map.begin(), score_map.end());
std::vector<std::string> keys = { " k1 " , " k2 " , " k3 " };
redis.del(keys.begin(), keys.end());
// ***** Parameters of initializer_list type *****
redis.mset({
std::make_pair ( " k1 " , " v1 " ),
std::make_pair ( " k2 " , " v2 " ),
std::make_pair ( " k3 " , " v3 " )
});
redis.hmset( " hash " ,
{
std::make_pair ( " f1 " , " v1 " ),
std::make_pair ( " f2 " , " v2 " ),
std::make_pair ( " f3 " , " v3 " )
});
redis.zadd( " zset " ,
{
std::make_pair ( " m1 " , 20.0 ),
std::make_pair ( " m2 " , 34.5 ),
std::make_pair ( " m3 " , 23.4 )
});
redis.del({ " k1 " , " k2 " , " k3 " }); // ***** Return void *****
redis.save();
// ***** Return std::string *****
auto info = redis.info();
// ***** Return bool *****
if (!redis.expire( " nonexistent " , std::chrono::seconds( 100 ))) {
std::cerr << " key doesn't exist " << std::endl;
}
if (redis.setnx( " key " , " val " )) {
std::cout << " set OK " << std::endl;
}
// ***** Return long long *****
auto len = redis.strlen( " key " );
auto num = redis.del({ " a " , " b " , " c " });
num = redis.incr( " a " );
// ***** Return double *****
auto real = redis.incrbyfloat( " b " , 23.4 );
real = redis.hincrbyfloat( " c " , " f " , 34.5 );
// ***** Return Optional<std::string>, i.e. OptionalString *****
auto os = redis.get( " kk " );
if (os) {
std::cout << *os << std::endl;
} else {
std::cerr << " key doesn't exist " << std::endl;
}
os = redis.spop( " set " );
if (os) {
std::cout << *os << std::endl;
} else {
std::cerr << " set is empty " << std::endl;
}
// ***** Return Optional<long long>, i.e. OptionalLongLong *****
auto oll = redis.zrank( " zset " , " mem " );
if (oll) {
std::cout << " rank is " << *oll << std::endl;
} else {
std::cerr << " member doesn't exist " << std::endl;
}
// ***** Return Optional<double>, i.e. OptionalDouble *****
auto ob = redis.zscore( " zset " , " m1 " );
if (ob) {
std::cout << " score is " << *ob << std::endl;
} else {
std::cerr << " member doesn't exist " << std::endl;
}
// ***** Return Optional<pair<string, string>> *****
auto op = redis.blpop({ " list1 " , " list2 " }, std::chrono::seconds( 2 ));
if (op) {
std::cout << " key is " << op-> first << " , value is " << op-> second << std::endl;
} else {
std::cerr << " timeout " << std::endl;
}
// ***** Output iterators *****
std::vector<OptionalString> os_vec;
redis.mget({ " k1 " , " k2 " , " k3 " }, std::back_inserter(os_vec));
std::vector<std::string> s_vec;
redis.lrange( " list " , 0 , - 1 , std::back_inserter(s_vec));
std::unordered_map<std::string, std::string> hash;
redis.hgetall( " hash " , std::inserter(hash, hash.end()));
// You can also save the result in a vecotr of string pair.
std::vector<std::pair<std::string, std::string>> hash_vec;
redis.hgetall( " hash " , std::back_inserter(hash_vec));
std::unordered_set<std::string> str_set;
redis.smembers( " s1 " , std::inserter(str_set, str_set.end()));
// You can also save the result in a vecotr of string.
s_vec.clear();
redis.smembers( " s1 " , std::back_inserter(s_vec));sw::redis::Cursor cursor = 0 ;
auto pattern = " *pattern* " ;
auto count = 5 ;
std::unordered_set<std::string> keys;
while ( true ) {
cursor = redis. scan (cursor, pattern, count, std::inserter (keys, keys. begin ()));
// Default pattern is "*", and default count is 10
// cursor = redis.scan(cursor, std::inserter(keys, keys.begin()));
if (cursor == 0 ) {
break ;
}
}때로는 출력 반복자 유형이 명령과 함께 보낼 옵션을 결정합니다.
// If the output iterator is an iterator of a container of string,
// we send *ZRANGE* command without the *WITHSCORES* option.
std::vector<std::string> members;
redis.zrange( " list " , 0 , - 1 , std::back_inserter(members));
// If it's an iterator of a container of a <string, double> pair,
// we send *ZRANGE* command with *WITHSCORES* option.
std::vector<std::pair<std::string, double >> res_with_score;
redis.zrange( " list " , 0 , - 1 , std::back_inserter(res_with_score));
// The above examples also apply to other command with the *WITHSCORES* options,
// e.g. *ZRANGEBYSCORE*, *ZREVRANGE*, *ZREVRANGEBYSCORE*.
// Another example is the *GEORADIUS* command.
// Only get members.
members.clear();
redis.georadius( " geo " ,
std::make_pair ( 10.1 , 11.1 ),
100,
GeoUnit::KM,
10,
true,
std::back_inserter(members));
// If the iterator is an iterator of a container of tuple<string, double>,
// we send the *GEORADIUS* command with *WITHDIST* option.
std::vector<std::tuple<std::string, double >> mem_with_dist;
redis.georadius( " geo " ,
std::make_pair ( 10.1 , 11.1 ),
100,
GeoUnit::KM,
10,
true,
std::back_inserter(mem_with_dist));
// If the iterator is an iterator of a container of tuple<string, double, string>,
// we send the *GEORADIUS* command with *WITHDIST* and *WITHHASH* options.
std::vector<std::tuple<std::string, double , std::string>> mem_with_dist_hash;
redis.georadius( " geo " ,
std::make_pair ( 10.1 , 11.1 ),
100,
GeoUnit::KM,
10,
true,
std::back_inserter(mem_with_dist_hash));
// If the iterator is an iterator of a container of
// tuple<string, string, pair<double, double>, double>,
// we send the *GEORADIUS* command with *WITHHASH*, *WITHCOORD* and *WITHDIST* options.
std::vector<std::tuple<std::string, double , std::string>> mem_with_hash_coord_dist;
redis.georadius( " geo " ,
std::make_pair ( 10.1 , 11.1 ),
100,
GeoUnit::KM,
10,
true,
std::back_inserter(mem_with_hash_coord_dist));Doxygen Style API 참조 및 예는 redis.h를 참조하고 다른 예제 테스트를 참조하십시오.
Redis 오류 응답을 받거나 나쁜 일이 발생하면 예외를 제외합니다. Error 클래스에서 파생 된 모든 예외. 자세한 내용은 errors.h를 참조하십시오.
Error : 일반 오류. 그것은 std::exception 에서 파생되며 다른 예외의 기본 클래스이기도합니다.IoError : 연결에 약간의 IO 오류가 있습니다.TimeoutError : 읽기 또는 쓰기 작업이 시간이 초과되었습니다. 그것은 파생 된 클래스의 IoError 입니다.ClosedError : Redis Server가 연결을 닫았습니다.ProtoError : 명령 또는 답변은 유효하지 않으며 Redis 프로토콜로 처리 할 수 없습니다.OomError : Hiredis Library는 메모리가 아닌 오류가 발생했습니다.ReplyError : Redis Server는 오류 답변을 반환했습니다. 예 : Redis Hash에서 redis::lrange 호출하려고합니다.WatchError : 시계 키가 수정되었습니다. 자세한 내용은 Watch 섹션을 참조하십시오. 참고 : NULL 답변은 예외로 간주되지 않습니다. 예를 들어, 존재하지 않는 키를 GET 려고하면 Null Bulk String Reply가 나타납니다. 예외를 던지는 대신 NULL 응답을 NULL Optional<T> 객체로 반환합니다. 또한 옵션 섹션을 참조하십시오.
일반적으로 예외가 발생하면 Redis 객체를 만들 필요가 없습니다. 예외는 안전하며 Redis 객체를 재사용 할 수 있습니다. Redis 서버에 대한 연결이 고장 나서 IoError 예외가 발생하더라도. 다음에 Redis 객체로 명령을 보낼 때 Redis 서버에 자동으로 다시 연결하려고합니다. 이 규칙은 RedisCluster 에도 적용됩니다. 그러나 Pipeline , Transcation 및 Subscriber 예외를 던지면 객체를 파괴하고 새 개체를 만들어야합니다. 자세한 내용은 해당 문서를 참조하십시오.
다음은 이러한 예외를 포착하는 방법에 대한 예입니다.
try {
redis. set ( " key " , " value " );
// Wrong type error
redis. lpush ( " key " , { " a " , " b " , " c " });
} catch ( const ReplyError &err) {
// WRONGTYPE Operation against a key holding the wrong kind of value
cout << err. what () << endl;
} catch ( const TimeoutError &err) {
// reading or writing timeout
} catch ( const ClosedError &err) {
// the connection has been closed.
} catch ( const IoError &err) {
// there's an IO error on the connection.
} catch ( const Error &err) {
// other errors
} Redis 명령이 너무 많아서 모든 명령을 구현하지는 않았습니다. 그러나 일반적인 Redis::command 메소드를 사용하여 명령을 Redis에 보낼 수 있습니다. 다른 클라이언트 라이브러리와 달리 Redis::command 형식 문자열을 사용하여 명령 인수를 명령 문자열로 결합하지 않습니다. 대신 StringView 유형 또는 산술 유형의 명령 인수를 Redis::command 의 매개 변수로 직접 전달할 수 있습니다. 형식 문자열을 사용하지 않는 이유는이 토론을 참조하십시오.
auto redis = Redis( " tcp://127.0.0.1 " );
// Redis class doesn't have built-in *CLIENT SETNAME* method.
// However, you can use Redis::command to send the command manually.
redis.command< void >( " client " , " setname " , " name " );
auto val = redis.command<OptionalString>( " client " , " getname " );
if (val) {
std::cout << *val << std::endl;
}
// NOTE: the following code is for example only. In fact, Redis has built-in
// methods for the following commands.
// Arguments of the command can be strings.
// NOTE: for SET command, the return value is NOT always void, I'll explain latter.
redis.command< void >( " set " , " key " , " 100 " );
// Arguments of the command can be a combination of strings and integers.
auto num = redis.command< long long >( " incrby " , " key " , 1 );
// Argument can also be double.
auto real = redis.command< double >( " incrbyfloat " , " key " , 2.3 );
// Even the key of the command can be of arithmetic type.
redis.command< void >( " set " , 100 , " value " );
val = redis.command<OptionalString>( " get " , 100 );
// If the command returns an array of elements.
std::vector<OptionalString> result;
redis.command( " mget " , " k1 " , " k2 " , " k3 " , std::back_inserter(result));
// Or just parse it into a vector.
result = redis.command<std::vector<OptionalString>>( " mget " , " k1 " , " k2 " , " k3 " );
// Arguments of the command can be a range of strings.
auto set_cmd_strs = { " set " , " key " , " value " };
redis.command< void >(set_cmd_strs.begin(), set_cmd_strs.end());
auto get_cmd_strs = { " get " , " key " };
val = redis.command<OptionalString>(get_cmd_strs.begin(), get_cmd_strs.end());
// If it returns an array of elements.
result.clear();
auto mget_cmd_strs = { " mget " , " key1 " , " key2 " };
redis.command(mget_cmd_strs.begin(), mget_cmd_strs.end(), std::back_inserter(result)); 참고 : 일부 redis 명령의 이름은 두 개의 문자열 (예 : 클라이언트 setName) 으로 구성됩니다. 이 경우이 두 줄을 Redis::command 에 대한 두 가지 인수로 전달해야합니다.
// This is GOOD.
redis.command< void >( " client " , " setname " , " name " );
// This is BAD, and will fail to send command to Redis server.
// redis.command<void>("client setname", "name"); 주석에서 언급했듯이 SET 명령이 항상 void 반환하는 것은 아닙니다. Because if you try to set a (key, value) pair with NX or XX option, you might fail, and Redis will return a NULL REPLY . Besides the SET command, there're other commands whose return value is NOT a fixed type, you need to parse it by yourself. For example, Redis::set method rewrite the reply of SET command, and make it return bool type, ie if no NX or XX option specified, Redis server will always return an "OK" string, and Redis::set returns true ; if NX or XX specified, and Redis server returns a NULL REPLY , Redis::set returns false .
So Redis class also has other overloaded command methods, these methods return a ReplyUPtr , ie std::unique_ptr<redisReply, ReplyDeleter> , object. Normally you don't need to parse it manually. Instead, you only need to pass the reply to template <typename T> T reply::parse(redisReply &) to get a value of type T . Check the Return Type section for valid T types. If the command returns an array of elements, besides calling reply::parse to parse the reply to an STL container, you can also call template <typename Output> reply::to_array(redisReply &reply, Output output) to parse the result into an array or STL container with an output iterator.
Let's rewrite the above examples:
auto redis = Redis( " tcp://127.0.0.1 " );
redis.command( " client " , " setname " , " name " );
auto r = redis.command( " client " , " getname " );
assert (r);
// If the command returns a single element,
// use `reply::parse<T>(redisReply&)` to parse it.
auto val = reply::parse<OptionalString>(*r);
if (val) {
std::cout << *val << std::endl;
}
// Arguments of the command can be strings.
redis.command( " set " , " key " , " 100 " );
// Arguments of the command can be a combination of strings and integers.
r = redis.command( " incrby " , " key " , 1 );
auto num = reply::parse< long long >(*r);
// Argument can also be double.
r = redis.command( " incrbyfloat " , " key " , 2.3 );
auto real = reply::parse< double >(*r);
// Even the key of the command can be of arithmetic type.
redis.command( " set " , 100 , " value " );
r = redis.command( " get " , 100 );
val = reply::parse<OptionalString>(*r);
// If the command returns an array of elements.
r = redis.command( " mget " , " k1 " , " k2 " , " k3 " );
// Use `reply::to_array(redisReply&, OutputIterator)` to parse the result into an STL container.
std::vector<OptionalString> result;
reply::to_array (*r, std::back_inserter(result));
// Or just call `reply::parse` to parse it into vector.
result = reply::parse<std::vector<OptionalString>>(*r);
// Arguments of the command can be a range of strings.
auto get_cmd_strs = { " get " , " key " };
r = redis.command(get_cmd_strs.begin(), get_cmd_strs.end());
val = reply::parse<OptionalString>(*r);
// If it returns an array of elements.
result.clear();
auto mget_cmd_strs = { " mget " , " key1 " , " key2 " };
r = redis.command(mget_cmd_strs.begin(), mget_cmd_strs.end());
reply::to_array (*r, std::back_inserter(result)); In fact, there's one more Redis::command method:
template < typename Cmd, typename ...Args>
auto command (Cmd cmd, Args &&...args)
-> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type;However, this method exposes some implementation details, and is only for internal use. You should NOT use this method.
You can use Redis::publish to publish messages to channels. Redis randomly picks a connection from the underlying connection pool, and publishes message with that connection. So you might publish two messages with two different connections.
When you subscribe to a channel with a connection, all messages published to the channel are sent back to that connection. So there's NO Redis::subscribe method. Instead, you can call Redis::subscriber to create a Subscriber and the Subscriber maintains a connection to Redis. The underlying connection is a new connection, NOT picked from the connection pool. This new connection has the same ConnectionOptions as the Redis object.
If you want to have different connection options, eg ConnectionOptions::socket_timeout , for different channels, you should create Redis objects with different connection options, then you can create Subscriber objects with these Redis objects. Check this issue for a use case.
ConnectionOptions opts1;
opts1.host = " 127.0.0.1 " ;
opts1.port = 6379 ;
opts1.socket_timeout = std::chrono::milliseconds( 100 );
auto redis1 = Redis(opts1);
// sub1's socket_timeout is 100ms.
auto sub1 = redis1.subscriber();
ConnectionOptions opts2;
opts2.host = " 127.0.0.1 " ;
opts2.port = 6379 ;
opts2.socket_timeout = std::chrono::milliseconds( 300 );
auto redis2 = Redis(opts2);
// sub2's socket_timeout is 300ms.
auto sub2 = redis2.subscriber(); NOTE : Although the above code creates two Redis objects, it has no performance penalty. Because Redis object creates connections lazily, ie no connection will be created until we send some command with Redis object, and the connection is created only when we call Redis::subscriber to create Subscriber object.
With Subscriber , you can call Subscriber::subscribe , Subscriber::unsubscribe , Subscriber::psubscribe and Subscriber::punsubscribe to send SUBSCRIBE , UNSUBSCRIBE , PSUBSCRIBE and PUNSUBSCRIBE commands to Redis.
Subscriber is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.
If any of the Subscriber 's method throws an exception other than ReplyError or TimeoutError , you CANNOT use it any more. Instead, you have to destroy the Subscriber object, and create a new one.
There are 6 kinds of messages:
We call messages of SUBSCRIBE , UNSUBSCRIBE , PSUBSCRIBE and PUNSUBSCRIBE types as META MESSAGE s.
In order to process these messages, you can set callback functions on Subscriber :
Subscriber::on_message(MsgCallback) : set callback function for messages of MESSAGE type, and the callback interface is: void (std::string channel, std::string msg) .Subscriber::on_pmessage(PatternMsgCallback) : set the callback function for messages of PMESSAGE type, and the callback interface is: void (std::string pattern, std::string channel, std::string msg) .Subscriber::on_meta(MetaCallback) : set callback function for messages of META MESSAGE type, and the callback interface is: void (Subscriber::MsgType type, OptionalString channel, long long num) . type is an enum, it can be one of the following enum: Subscriber::MsgType::SUBSCRIBE , Subscriber::MsgType::UNSUBSCRIBE , Subscriber::MsgType::PSUBSCRIBE , Subscriber::MsgType::PUNSUBSCRIBE , Subscriber::MsgType::MESSAGE , and Subscriber::MsgType::PMESSAGE . If you haven't subscribe/psubscribe to any channel/pattern, and try to unsubscribe/punsubscribe without any parameter, ie unsubscribe/punsubscribe all channels/patterns, channel will be null. So the second parameter of meta callback is of type OptionalString . All these callback interfaces pass std::string by value, and you can take their ownership (ie std::move ) safely.
You can call Subscriber::consume to consume messages published to channels/patterns that the Subscriber has been subscribed.
Subscriber::consume waits for message from the underlying connection. If the ConnectionOptions::socket_timeout is reached, and there's no message sent to this connection, Subscriber::consume throws a TimeoutError exception. If ConnectionOptions::socket_timeout is 0ms , Subscriber::consume blocks until it receives a message.
After receiving the message, Subscriber::consume calls the callback function to process the message based on message type. However, if you don't set callback for a specific kind of message, Subscriber::consume will consume the received message and discard it, ie Subscriber::consume returns without running the callback.
The following example is a common pattern for using Subscriber :
// Create a Subscriber.
auto sub = redis.subscriber();
// Set callback functions.
sub.on_message([](std::string channel, std::string msg) {
// Process message of MESSAGE type.
});
sub.on_pmessage([](std::string pattern, std::string channel, std::string msg) {
// Process message of PMESSAGE type.
});
sub.on_meta([](Subscriber::MsgType type, OptionalString channel, long long num) {
// Process message of META type.
});
// Subscribe to channels and patterns.
sub.subscribe( " channel1 " );
sub.subscribe({ " channel2 " , " channel3 " });
sub.psubscribe( " pattern1* " );
// Consume messages in a loop.
while ( true ) {
try {
sub. consume ();
} catch ( const Error &err) {
// Handle exceptions.
}
} If ConnectionOptions::socket_timeout is set, you might get TimeoutError exception before receiving a message:
while ( true ) {
try {
sub. consume ();
} catch ( const TimeoutError &e) {
// Try again.
continue ;
} catch ( const Error &err) {
// Handle other exceptions.
}
}The above examples use lambda as callback. If you're not familiar with lambda, you can also set a free function as callback. Check this issue for detail.
Pipeline is used to reduce RTT (Round Trip Time), and speed up Redis queries. redis-plus-plus supports pipeline with the Pipeline class.
You can create a pipeline with Redis::pipeline method, which returns a Pipeline object.
ConnectionOptions connection_options;
ConnectionPoolOptions pool_options;
Redis redis (connection_options, pool_options);
auto pipe = redis.pipeline(); When creating a Pipeline object, by default, Redis::pipeline method creates a new connection to Redis server. This connection is NOT picked from the connection pool, but a newly created connection. This connection has the same ConnectionOptions as other connections in the connection pool. Pipeline object maintains the new connection, and all piped commands are sent through this connection.
NOTE : By default, creating a Pipeline object is NOT cheap, since it creates a new connection. So you'd better reuse the Pipeline object as much as possible. Check this to see how to create a Pipeline object without creating a new connection.
You can send Redis commands through the Pipeline object. Just like the Redis class, Pipeline has one or more (overloaded) methods for each Redis command. However, you CANNOT get the replies until you call Pipeline::exec . So these methods do NOT return the reply, instead they return the Pipeline object itself. And you can chain these methods calls.
pipe.set( " key " , " val " ).incr( " num " ).rpush( " list " , { 0 , 1 , 2 }).command( " hset " , " key " , " field " , " value " ); Once you finish sending commands to Redis, you can call Pipeline::exec to get replies of these commands. You can also chain Pipeline::exec with other commands.
pipe.set( " key " , " val " ).incr( " num " );
auto replies = pipe.exec();
// The same as:
replies = pipe.set( " key " , " val " ).incr( " num " ).exec(); In fact, these commands won't be sent to Redis, until you call Pipeline::exec . So Pipeline::exec does 2 work in order: send all piped commands, then get all replies from Redis.
Also you can call Pipeline::discard to discard those piped commands.
pipe.set( " key " , " val " ).incr( " num " );
pipe.discard(); Pipeline::exec returns a QueuedReplies object, which contains replies of all commands that have been sent to Redis. You can use QueuedReplies::get method to get and parse the ith reply. It has 3 overloads:
template <typename Result> Result get(std::size_t idx) : Return the ith reply as a return value, and you need to specify the return type as tempalte parameter.template <typename Output> void get(std::size_t idx, Output output) : If the reply is of type Array Reply , you can call this method to write the ith reply to an output iterator. Normally, compiler will deduce the type of the output iterator, and you don't need to specify the type parameter explicitly.redisReply& get(std::size_t idx) : If the reply is NOT a fixed type, call this method to get a reference to redisReply object. In this case, you need to call template <typename T> T reply::parse(redisReply &) to parse the reply manually.Check the Return Type section for details on the return types of the result.
auto replies = pipe.set( " key " , " val " ).incr( " num " ).lrange( " list " , 0 , - 1 ).exec();
auto set_cmd_result = replies.get< bool >( 0 );
auto incr_cmd_result = replies.get< long long >( 1 );
std::vector<std::string> list_cmd_result;
replies.get( 2 , std::back_inserter(list_cmd_result)); If any of Pipeline 's method throws an exception other than ReplyError , the Pipeline object enters an invalid state. You CANNOT use it any more, but only destroy the object, and create a new one.
Pipeline is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.
YOU MUST CAREFULLY READ ALL WORDS IN THIS SECTION AND THE VERY IMPORTANT NOTES BEFORE USING THIS FEATURE!!!
In fact, you can also create a Pipeline object with a connection from the underlying connection pool, so that calling Redis::pipeline method can be much cheaper (since it doesn't need to create a new connection).
The prototype of Redis::pipeline is as follows: Pipeline pipeline(bool new_connection = true); . If new_connection is false, the Pipeline object will be created with a connection from the underlying pool.
ConnectionOptions connection_options;
ConnectionPoolOptions pool_options;
Redis redis (connection_options, pool_options);
// Create a Pipeline without creating a new connection.
auto pipe = redis.pipeline( false ); However, in this case, you MUST be very careful, otherwise, you might get bad performance or even dead lock. Because when you run command with Pipeline object, it will hold the connection until Pipeline::exec , Pipeline::discard or Pipeline 's destructor is called (the connection will also be released if any method of Pipeline throws Exception ). If the Pipeline object holds the connection for a long time, other Redis methods might not be able to get a connection from the underlying pool.
Check the following dead lock example:
// By defaul, create a `Redis` object with only ONE connection in pool.
// Also by default, the `ConnectionPoolOptions::wait_timeout` is 0ms,
// which means if the pool is empty, `Redis` method will be blocked until
// the pool is not empty.
Redis redis ( " tcp://127.0.0.1 " );
// Create a `Pipeline` with a connection in the underlying pool.
// In fact, the connection hasn't been fetched from the pool
// until some method of `Pipeline` has been called.
auto pipe = redis.pipeline( false );
// Now the `Pipeline` object fetches a connection from the pool.
pipe.set( " key1 " , " val " );
// `Pipeline` object still holds the connection until `Pipeline::exec`,
// `Pipeline::discard` or the destructor is called.
pipe.set( " key2 " , " val " );
// Try to send a command with `Redis` object.
// However, the pool is empty, since the `Pipeline` object still holds
// the connection, and this call will be blocked forever.
// DEAD LOCK!!!
redis.get( " key " );
// NEVER goes here.
pipe.exec();BEST PRACTICE :
When creating Pipeline without creating a new connection:
ConnectionPoolOptions::wait_timeout larger than 0ms (ie when pool is empty, never block forever).Pipeline 's methods.Pipeline methods and the Pipeline::exec in one statements.Pipeline related code in a block scope. ConnectionOptions opts;
opts.host = " 127.0.0.1 " ;
opts.port = 6379 ;
opts.socket_timeout = std::chrono::milliseconds( 50 );
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ;
// Always set `wait_timeout` larger than 0ms.
pool_opts.wait_timeout = std::chrono::milliseconds( 50 );
auto redis = Redis(opts, pool_opts);
{
// Better put `Pipeline` related code in a block scope.
auto pipe = redis. pipeline ( false );
pipe . set ( " key1 " , " val " );
// DON'T run slow operations here, since `Pipeline` object still holds
// the connection, other threads using this `Redis` object, might be blocked.
pipe . set ( " key2 " , " val " );
// When `Pipeline::exec` finishes, `Pipeline` releases the connection, and returns it to pool.
auto replies = pipe . exec ();
// This is even better, i.e. chain `Pipeline` methods with `Pipeline::exec`.
replies = pipe . set ( " key1 " , " val " ). set ( " key2 " , " val " ). exec ();
}
for ( auto i = 0 ; i < 10 ; ++i) {
// This operation, i.e. creating a `Pipeline` object with connection in pool, is cheap
auto pipe = redis. pipeline ( false );
// Fetch a connection from the underlying pool, and hold it.
pipe . set ( " key1 " , " val " ). set ( " key2 " , " val " );
// Although `Pipeline::exec` and `Pipeline::discard` haven't been called,
// when `Pipeline`'s destructor is called, the connection will also be
// returned to the pool.
}Transaction is used to make multiple commands runs atomically.
You can create a transaction with Redis::transaction method, which returns a Transaction object.
ConnectionOptions connection_options;
ConnectionPoolOptions pool_options;
Redis redis (connection_options, pool_options);
auto tx = redis.transaction(); As the Pipeline class, Transaction maintains a newly created connection to Redis. This connection has the same ConnectionOptions as the Redis object.
NOTE : Creating a Transaction object is NOT cheap, since it creates a new connection. So you'd better reuse the Transaction as much as possible. Check this to see how to create a Transaction object without creating a new connection.
Also you don't need to send MULTI command to Redis. Transaction will do that for you automatically.
Transaction shares most of implementation with Pipeline . It has the same interfaces as Pipeline . You can send commands as what you do with Pipeline object.
tx.set( " key " , " val " ).incr( " num " ).lpush( " list " , { 0 , 1 , 2 }).command( " hset " , " key " , " field " , " val " ); When you call Transaction::exec , you explicitly ask Redis to execute those queued commands, and return the replies. Otherwise, these commands won't be executed. Also, you can call Transaction::discard to discard the execution, ie no command will be executed. Both Transaction::exec and Transaction::discard can be chained with other commands.
auto replies = tx.set( " key " , " val " ).incr( " num " ).exec();
tx.set( " key " , " val " ).incr( " num " );
// Discard the transaction.
tx.discard();See Pipeline's Parse Replies section for how to parse the replies.
Normally, we always send multiple commnds in a transaction. In order to improve the performance, you can send these commands in a pipeline. You can create a piped transaction by passing true as parameter of Redis::transaction method.
// Create a piped transaction
auto tx = redis.transaction( true );With this piped transaction, all commands are sent to Redis in a pipeline.
If any of Transaction 's method throws an exception other than WatchError or ReplyError , the Transaction object enters an invalid state. You CANNOT use it any more, but only destroy the object and create a new one.
Transacation is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.
WATCH is used to provide a check-and-set(CAS) behavior to Redis transactions.
The WATCH command must be sent in the same connection as the transaction. And normally after the WATCH command, we also need to send some other commands to get data from Redis before executing the transaction. Take the following check-and-set case as an example:
WATCH key // watch a key
val = GET key // get value of the key
new_val = val + 1 // incr the value
MULTI // begin the transaction
SET key new_val // set value only if the value is NOT modified by others
EXEC // try to execute the transaction.
// if val has been modified, the transaction won't be executed.
However, with Transaction object, you CANNOT get the result of commands until the whole transaction has been finished. Instead, you need to create a Redis object from the Transaction object. The created Redis object shares the connection with Transaction object. With this created Redis object, you can send WATCH command and any other Redis commands to Redis server, and get the result immediately.
Let's see how to implement the above example with redis-plus-plus :
auto redis = Redis( " tcp://127.0.0.1 " );
// Create a transaction.
auto tx = redis.transaction();
// Create a Redis object from the Transaction object. Both objects share the same connection.
auto r = tx.redis();
// If the watched key has been modified by other clients, the transaction might fail.
// So we need to retry the transaction in a loop.
while ( true ) {
try {
// Watch a key.
r. watch ( " key " );
// Get the old value.
auto val = r. get ( " key " );
auto num = 0 ;
if (val) {
num = std::stoi (*val);
} // else use default value, i.e. 0.
// Incr value.
++num;
// Execute the transaction.
auto replies = tx. set ( " key " , std::to_string (num)). exec ();
// Transaction has been executed successfully. Check the result and break.
assert (replies. size () == 1 && replies. get < bool >( 0 ) == true );
break ;
} catch ( const WatchError &err) {
// Key has been modified by other clients, retry.
continue ;
} catch ( const Error &err) {
// Something bad happens, and the Transaction object is no longer valid.
throw ;
}
} NOTE : in the example above, we create Transaction object outside the while loop, in order to avoid creating new connection again and again.
NOTE : YOU MUST CAREFULLY READ ALL WORDS AND THE VERY IMPORTANT NOTES LINK IN THIS SECTION BEFORE USING THIS FEATURE!!!
In fact, you can also create a transaction object with a connection from the underlying connection pool, so that calling Redis::transaction method can be much cheaper (since it doesn't need to create a new connection).
The prototype of Redis::transaction is as follows: Transaction transaction(bool piped = false, bool new_connection = true); . If new_connection is false, the Transaction object will be created with a connection from the underlying pool.
ConnectionOptions connection_options;
ConnectionPoolOptions pool_options;
Redis redis (connection_options, pool_options);
// Create a Transaction without creating a new connection.
auto tx = redis.transaction( false , false );However, in this case, you MUST be very careful, otherwise, you might get bad performance or even dead lock. Please carefully check the similar pipeline's VERY IMPORTANT NOTES section, before you use it!
Besides those very important notes, there's another important note for Transaction :
Redis object created by Transaction::Redis , ie destroy it ASAP.Check the following example:
auto redis = Redis(opts, pool_opts);
// Create a `Transaction` object without creating a new connection.
auto tx = redis.Transaction( false , false );
// Create a `Redis`, and this `Redis` object shares the same connection with the `Transaction` object.
auto r = tx.redis();
// Other code here...
// Execute the transaction.
auto replies = tx.set( " key " , " val " ).exec();
// Although `Transaction::exec` has been called, the connection has not been returned to pool.
// Because the `Redis` object, i.e. `r`, still holds the connection.So the above watch example should be modified as follows:
auto redis = Redis(opts, pool_opts);
// If the watched key has been modified by other clients, the transaction might fail.
// So we need to retry the transaction in a loop.
while ( true ) {
try {
// Create a transaction without creating a new connection.
auto tx = redis. transaction ( false , false );
// Create a Redis object from the Transaction object. Both objects share the same connection.
auto r = tx. redis ();
// Watch a key.
r. watch ( " key " );
// Get the old value.
auto val = r. get ( " key " );
auto num = 0 ;
if (val) {
num = std::stoi (*val);
} // else use default value, i.e. 0.
// Incr value.
++num;
// Execute the transaction.
auto replies = tx. set ( " key " , std::to_string (num)). exec ();
// Transaction has been executed successfully. Check the result and break.
assert (replies. size () == 1 && replies. get < bool >( 0 ) == true );
break ;
} catch ( const WatchError &err) {
// Key has been modified by other clients, retry.
continue ;
} catch ( const Error &err) {
// Something bad happens, and the Transaction object is no longer valid.
throw ;
}
} NOTE : The difference is that we create the Transaction object in the while loop (it's cheap, since it doesn't need to create a new connection). When the Transaction object and the Redis object created by Transaction::redis have been destroyed, the connection will be return to pool.
redis-plus-plus supports Redis Cluster. You can use RedisCluster class to send commands to Redis Cluster. It has similar interfaces as Redis class.
By default, RedisCluster connects to all master nodes in the cluster. For each master node, it maintains a connection pool. If you want to read from slave nodes, you need to explicitly set an option (see below for reference).
You can initialize a RedisCluster instance with ConnectionOptions and ConnectionPoolOptions . You only need to set one master node's host & port in ConnectionOptions , and RedisCluster will get other nodes' info automatically (with the CLUSTER SLOTS command). For each master node, it creates a connection pool with the specified ConnectionPoolOptions . If ConnectionPoolOptions is not specified, RedisCluster maintains a single connection to every master node.
// Set a master node's host & port.
ConnectionOptions connection_options;
connection_options.host = " 127.0.0.1 " ; // Required.
connection_options.port = 7000 ; // Optional. The default port is 6379.
connection_options.password = " auth " ; // Optional. No password by default.
// Automatically get other nodes' info,
// and connect to every master node with a single connection.
RedisCluster cluster1 (connection_options);
ConnectionPoolOptions pool_options;
pool_options.size = 3 ;
// For each master node, maintains a connection pool of size 3.
RedisCluster cluster2 (connection_options, pool_options); You can also specify connection option with an URI. However, in this way, you can only use default ConnectionPoolOptions , ie pool of size 1, and CANNOT specify password.
// Specify a master node's host & port.
RedisCluster cluster3 ( " tcp://127.0.0.1:7000 " );
// Use default port, i.e. 6379.
RedisCluster cluster4 ( " tcp://127.0.0.1 " ); If you want to scale read by reading (possible stale) data from slave nodes, you can specifiy Role::SLAVE as the third parameter of RedisCluster 's constructor. In this case, redis-plus-plus will randomly pick a replica node for each master node of the cluster, and create a connection pool for the replica node.
RedisCluster cluster (connection_options, pool_options, Role::SLAVE);
auto val = cluster.get( " key " ); In this case, you can only send readonly commands to Redis Cluster. If you try to send a write command, eg set , hset , redis-plus-plus will throw an exception. Currently, redis-plus-plus doesn't handle this case, ie sending write command in Role::SLAVE mode, elegantly, and you might get some performance problem. So, NEVER send write command in Role::SLAVE mode. I'll fix this issue in the future.
NOTE : In Role::SLAVE mode, you don't need to manually send READONLY command to slave nodes. Instead, redis-plus-plus will send READONLY command to slave nodes automatically.
RedisCluster only works with tcp connection. It CANNOT connect to Unix Domain Socket. If you specify Unix Domain Socket in ConnectionOptions , it throws an exception.ConnectionOptions::db is ignored. As we mentioned above, RedisCluster 's interfaces are similar to Redis . It supports most of Redis ' interfaces, including the generic command interface (see Redis ' API Reference section for details), except the following:
PING , INFO . Since there's no key parameter, RedisCluster has no idea on to which node these commands should be sent. However there're 2 workarounds for this problem:
Redis object with that node's host and port, and use the Redis object to do the work.Redis RedisCluster::redis(const StringView &hash_tag) to create a Redis object with a hash-tag specifying the node. In this case, the returned Redis object creates a new connection to Redis server. NOTE : the returned Redis object, IS NOT THREAD SAFE! . Also, when using the returned Redis object, if it throws exception, you need to destroy it, and create a new one with the RedisCluster::redis method.Also you can use the hash tags to send multiple-key commands.
See the example section for details.
You can publish and subscribe messages with RedisCluster . The interfaces are exactly the same as Redis , ie use RedisCluster::publish to publish messages, and use RedisCluster::subscriber to create a subscriber to consume messages. See Publish/Subscribe section for details.
You can also create Pipeline and Transaction objects with RedisCluster , but the interfaces are different from Redis . Since all commands in the pipeline and transaction should be sent to a single node in a single connection, we need to tell RedisCluster with which node the pipeline or transaction should be created.
Instead of specifying the node's IP and port, RedisCluster 's pipeline and transaction interfaces allow you to specify the node with a hash tag . RedisCluster will calculate the slot number with the given hash tag , and create a pipeline or transaction with the node holding the slot.
Pipeline RedisCluster::pipeline ( const StringView &hash_tag, bool new_connection = true );
Transaction RedisCluster::transaction ( const StringView &hash_tag, bool piped = false , bool new_connection = true ); With the created Pipeline or Transaction object, you can send commands with keys located on the same node as the given hash_tag . See Examples section for an example.
NOTE : By default, Pipeline and Transaction will be created with a new connection. In order to avoid creating new connection, you can pass false as the last parameter. However, in this case, you MUST be very careful, otherwise, you might get bad performance or even dead lock. Please carefully check the related pipeline section before using this feature.
# include < sw/redis++/redis++.h >
using namespace sw ::redis ;
auto redis_cluster = RedisCluster( " tcp://127.0.0.1:7000 " );
redis_cluster.set( " key " , " value " );
auto val = redis_cluster.get( " key " );
if (val) {
std::cout << *val << std::endl;
}
// With hash-tag.
redis_cluster.set( " key{tag}1 " , " val1 " );
redis_cluster.set( " key{tag}2 " , " val2 " );
redis_cluster.set( " key{tag}3 " , " val3 " );
std::vector<OptionalString> hash_tag_res;
redis_cluster.mget({ " key{tag}1 " , " key{tag}2 " , " key{tag}3 " },
std::back_inserter (hash_tag_res));
redis_cluster.lpush( " list " , { " 1 " , " 2 " , " 3 " });
std::vector<std::string> list;
redis_cluster.lrange( " list " , 0 , - 1 , std::back_inserter(list));
// Pipeline.
auto pipe = redis_cluster.pipeline( " counter " );
auto replies = pipe.incr( " {counter}:1 " ).incr( " {counter}:2 " ).exec();
// Transaction.
auto tx = redis_cluster.transaction( " key " );
replies = tx.incr( " key " ).get( " key " ).exec();
// Create a Redis object with hash-tag.
// It connects to the Redis instance that holds the given key, i.e. hash-tag.
auto r = redis_cluster.redis( " hash-tag " );
// And send command without key parameter to the server.
r.command( " client " , " setname " , " connection-name " ); NOTE : By default, when you use RedisCluster::redis(const StringView &hash_tag, bool new_connection = true) to create a Redis object, instead of picking a connection from the underlying connection pool, it creates a new connection to the corresponding Redis server. So this is NOT a cheap operation, and you should try to reuse this newly created Redis object as much as possible. If you pass false as the second parameter, you can create a Redis object without creating a new connection. However, in this case, you should be very careful, otherwise, you might get bad performance or even dead lock. Please carefully check the related pipeline section before using this feature.
// This is BAD! It's very inefficient.
// NEVER DO IT!!!
// After sending PING command, the newly created Redis object will be destroied.
cluster.redis( " key " ).ping();
// Then it creates a connection to Redis, and closes the connection after sending the command.
cluster.redis( " key " ).command( " client " , " setname " , " hello " );
// Instead you should reuse the Redis object.
// This is GOOD!
auto redis = cluster.redis( " key " );
redis.ping();
redis.command( " client " , " setname " , " hello " );
// This is GOOD! Create `Redis` object without creating a new connection. Use it, and destroy it ASAP.
cluster.redis( " key " , false ).ping(); RedisCluster maintains the newest slot-node mapping, and sends command directly to the right node. Normally it works as fast as Redis . If the cluster reshards, RedisCluster will follow the redirection, and it will finally update the slot-node mapping. It can correctly handle the following resharding cases:
redis-plus-plus is able to handle both MOVED and ASK redirections, so it's a complete Redis Cluster client.
If master is down, the cluster will promote one of its replicas to be the new master. redis-plus-plus can also handle this case:
Since redis-plus-plus 1.3.13, it also updates the slot-node mapping every ClusterOptions::slot_map_refresh_interval time interval (by default, it updates every 10 seconds).
Redis Sentinel provides high availability for Redis. If Redis master is down, Redis Sentinels will elect a new master from slaves, ie failover. Besides, Redis Sentinel can also act like a configuration provider for clients, and clients can query master or slave address from Redis Sentinel. So that if a failover occurs, clients can ask the new master address from Redis Sentinel.
redis-plus-plus supports getting Redis master or slave's IP and port from Redis Sentinel. In order to use this feature, you only need to initialize Redis object with Redis Sentinel info, which is composed with 3 parts: std::shared_ptr<Sentinel> , master name and role (master or slave).
Before using Redis Sentinel with redis-plus-plus , ensure that you have read Redis Sentinel's doc.
You can create a std::shared_ptr<Sentinel> object with SentinelOptions .
SentinelOptions sentinel_opts;
sentinel_opts.nodes = {{ " 127.0.0.1 " , 9000 },
{ " 127.0.0.1 " , 9001 },
{ " 127.0.0.1 " , 9002 }}; // Required. List of Redis Sentinel nodes.
// Optional. Timeout before we successfully connect to Redis Sentinel.
// By default, the timeout is 100ms.
sentinel_opts.connect_timeout = std::chrono::milliseconds( 200 );
// Optional. Timeout before we successfully send request to or receive response from Redis Sentinel.
// By default, the timeout is 100ms.
sentinel_opts.socket_timeout = std::chrono::milliseconds( 200 );
auto sentinel = std::make_shared<Sentinel>(sentinel_opts); SentinelOptions::connect_timeout and SentinelOptions::socket_timeout CANNOT be 0ms, ie no timeout and block forever. Otherwise, redis-plus-plus will throw an exception.
See SentinelOptions for more options.
Besides std::shared_ptr<Sentinel> and master name, you also need to specify a role. There are two roles: Role::MASTER , and Role::SLAVE .
With Role::MASTER , redis-plus-plus will always connect to current master instance, even if a failover occurs. Each time when redis-plus-plus needs to create a new connection to master, or a connection is broken, and it needs to reconnect to master, redis-plus-plus will ask master address from Redis Sentinel, and connects to current master. If a failover occurs, redis-plus-plus can automatically get the address of the new master, and refresh all connections in the underlying connection pool.
Similarly, with Role::SLAVE , redis-plus-plus will always connect to a slave instance. A master might have several slaves, redis-plus-plus will randomly pick one, and connect to it, ie all connections in the underlying connection pool, connect to the same slave instance (check this discussion on why redis-plus-plus not connect to all slaves). If the connection is broken, while this slave instance is still an alive slave, redis-plus-plus will reconnect to this slave. However, if this slave instance is down, or it has been promoted to be the master, redis-plus-plus will randomly connect to another slave. If there's no slave alive, it throws an exception.
When creating a Redis object with sentinel, besides the sentinel info, you should also provide ConnectionOptions and ConnectionPoolOptions . These two options are used to connect to Redis instance. ConnectionPoolOptions is optional, if not specified, it creates a single connection the instance.
ConnectionOptions connection_opts;
connection_opts.password = " auth " ; // Optional. No password by default.
connection_opts.connect_timeout = std::chrono::milliseconds( 100 ); // Required.
connection_opts.socket_timeout = std::chrono::milliseconds( 100 ); // Required.
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ; // Optional. The default size is 1.
auto redis = Redis(sentinel, " master_name " , Role::MASTER, connection_opts, pool_opts); You might have noticed that we didn't specify the host and port fields for ConnectionOptions . Because, Redis will get these info from Redis Sentinel. Also, in this case, ConnectionOptions::connect_timeout and ConnectionOptions::socket_timeout CANNOT be 0ms, otherwise, it throws an exception. So you always need to specify these two timeouts manually.
After creating the Redis object with sentinel, you can send commands with it, just like an ordinary Redis object.
If you want to write to master, and scale read with slaves. You can use the following pattern:
auto sentinel = std::make_shared<Sentinel>(sentinel_opts);
auto master = Redis(sentinel, " master_name " , Role::MASTER, connection_opts, pool_opts);
auto slave = Redis(sentinel, " master_name " , Role::SLAVE, connection_opts, pool_opts);
// Write to master.
master.set( " key " , " value " );
// Read from slave.
slave.get( " key " );Since Redis 5.0, it introduces a new data type: Redis Stream . redis-plus-plus has built-in methods for all stream commands except the XINFO command (of course, you can use the Generic Command Interface to send XINFO command).
However, the replies of some streams commands, ie XPENDING , XREAD , are complex. So I'll give some examples to show you how to work with these built-in methods.
auto redis = Redis( " tcp://127.0.0.1 " );
using Attrs = std::vector<std::pair<std::string, std::string>>;
// You can also use std::unordered_map, if you don't care the order of attributes:
// using Attrs = std::unordered_map<std::string, std::string>;
Attrs attrs = { { " f1 " , " v1 " }, { " f2 " , " v2 " } };
// Add an item into the stream. This method returns the auto generated id.
auto id = redis.xadd( " key " , " * " , attrs.begin(), attrs.end());
// Each item is assigned with an id: pair<id, Optional<attributes>>.
// NOTE: the attribute part might be nil reply, check [this issue](https://github.com/sewenew/redis-plus-plus/issues/283) for detail.
using Item = std::pair<std::string, Optional<Attrs>>;
using ItemStream = std::vector<Item>;
// If you don't care the order of items in the stream, you can also use unordered_map:
// using ItemStream = std::unordered_map<std::string, Attrs>;
// Read items from a stream, and return at most 10 items.
// You need to specify a key and an id (timestamp + offset).
std::unordered_map<std::string, ItemStream> result;
redis.xread( " key " , id, 10 , std::inserter(result, result.end()));
// Read from multiple streams. For each stream, you need to specify a key and an id.
std::unordered_map<std::string, std::string> keys = { { " key " , id}, { " another-key " , " 0-0 " } };
redis.xread(keys.begin(), keys.end(), 10 , std::inserter(result, result.end()));
// Block for at most 1 second if currently there's no data in the stream.
redis.xread( " key " , id, std::chrono::seconds( 1 ), 10 , std::inserter(result, result.end()));
// Block for multiple streams.
redis.xread(keys.begin(), keys.end(), std::chrono::seconds( 1 ), 10 , std::inserter(result, result.end()));
// Read items in a range:
ItemStream item_stream;
redis.xrange( " key " , " - " , " + " , std::back_inserter(item_stream));
// Trim the stream to a given number of items. After the operation, the stream length is NOT exactly
// 10. Instead, it might be much larger than 10.
// `XTRIM key MAXLEN 10`
redis.xtrim( " key " , 10 );
// In order to trim the stream to exactly 10 items, specify the third argument, i.e. approx, as false.
// `XTRIM key MAXLEN ~ 10`
redis.xtrim( " key " , 10 , false );
// Delete an item from the stream.
redis.xdel( " key " , id);
// Create a consumer group.
redis.xgroup_create( " key " , " group " , " $ " );
// If the stream doesn't exist, you can set the fourth argument, i.e. MKSTREAM, to be true.
// redis.xgroup_create("key", "group", "$", true);
id = redis.xadd( " key " , " * " , attrs.begin(), attrs.end());
// Read item by a consumer of a consumer group.
redis.xreadgroup( " group " , " consumer " , " key " , " > " , 1 , std::inserter(result, result.end()));
using PendingItem = std::tuple<std::string, std::string, long long , long long >;
std::vector<PendingItem> pending_items;
// Get pending items of a speicified consumer.
redis.xpending( " key " , " group " , " - " , " + " , 1 , " consumer " , std::back_inserter(pending_items));
redis.xack( " key " , " group " , id);
redis.xgroup_delconsumer( " key " , " group " , " consumer " );
redis.xgroup_destroy( " key " , " group " );If you have any problem on sending stream commands to Redis, please feel free to let me know.
Redis Modules enrich Redis. However, redis-plus-plus does not have built-in support/method for these modules, although you can use the generic interface to send commands related to these modules.
The generic command interface uses the second argument as the key for hashing. If your custom command places the key at a different argument (ie: module-name create key1 arg1 arg2 ), and you are using the RedisCluster client, then it will fail to send the command to the correct Redis instance. In this case you could use the following work-around:
auto redis_cluster = RedisCluster( " tcp://127.0.0.1:6379 " );
std::vector<std::string> raw_cmd;
raw_cmd.push_back( " module-name " );
raw_cmd.push_back( " create " );
raw_cmd.push_back( " key1 " );
raw_cmd.push_back( " arg1 " );
raw_cmd.push_back( " arg2 " );
// create it with a connection from the underlying connection pool
auto redis = redis_cluster.redis( " key1 " , false );
redis.command< void >(raw_cmd.begin(), raw_cmd.end());Fortunately, @wingunder did a great job to make the work easier. He wrote redis-plus-plus-modules, which is a header only project that has built-in support for some popular modules. If you need to work with Redis Modules, you should have a try.
@wingunder also contributes a lot to redis-plus-plus . Many thanks to @wingunder!
redis-plus-plus also supports async interface, however, async support for Transaction is still on the way.
The async interface depends on third-party event library, and so far, only libuv is supported.
You must install libuv (eg apt-get install libuv1-dev ) before install hiredis and redis-plus-plus . The required libuv version is 1.x .
hiredis v1.0.0's async interface is different from older version, and redis-plus-plus only supports hiredis v1.0.0 or later. So you need to ensure you've installed the right version of hiredis before installing redis-plus-plus . Also, you should NEVER install multiple versions of hiredis , otherwise, you'll get some wired problems. If you already installed an older version, remove it, and install a newer version.
When installing redis-plus-plus , you should specify the following command line option: -DREDIS_PLUS_PLUS_BUILD_ASYNC=libuv .
cmake -DCMAKE_PREFIX_PATH=/installation/path/to/libuv/and/hiredis -DREDIS_PLUS_PLUS_BUILD_ASYNC=libuv ..
make
make install The async interface is similar to sync interface, except that you should include sw/redis++/async_redis++.h , and define an object of sw::redis::AsyncRedis , and the related methods return Future object (so far, only std::future and boost::future are supported, support for other implementations of future is on the way).
However, C++'s support for continuation and executor is not done yet, so the async interface also supports the old callback way. The following is the callback interface:
template <typename ReplyType>
void (sw::redis::Future<ReplyType> &&fut);
In the callback, in order to get the reply, you need to call sw::redis::Future<ReplyType>::get() . If something bad happened, get throws exception. So you need to catch possible exception in the callback. The callback runs in the underlying event loop thread, so DO NOT do slow operations in the callback, otherwise, it blocks the event loop and hurts performance.
메모 :
AsyncRedis alive before all callbacks have been executed (with some synchronization work). Because, once AsyncRedis is destroyed, it will stop the underlying event loop. And any commands that haven't sent to Redis yet, might fail. These notes also work with AsyncRedisCluster .
# include < sw/redis++/async_redis++.h >
ConnectionOptions opts;
opts.host = " 127.0.0.1 " ;
opts.port = 6379 ;
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ;
auto async_redis = AsyncRedis(opts, pool_opts);
Future<string> ping_res = async_redis.ping();
// Async interface returning Future object.
Future< bool > set_res = async_redis.set( " key " , " val " );
// Async interface with callback.
async_redis.set( " key " , " val " ,
[](Future< bool > &&fut) {
try {
auto set_res = fut. get ();
} catch ( const Error &err) {
// handle error
}
});
Future<Optional<string>> get_res = async_redis.get( " key " );
async_redis.get( " key " , [](Future<OptionalString> &&fut) {
try {
auto val = fut. get ();
if (val)
cout << *val << endl;
else
cout << " not exist " << endl;
} catch ( const Error &err) {
// handle error
}
});
unordered_map<string, string> m = {{ " a " , " b " }, { " c " , " d " }};
Future< void > hmset_res = async_redis.hmset( " hash " , m.begin(), m.end());
auto hgetall_res = async_redis.hgetall<std::unordered_map<std::string, std::string>>( " hash " );
cout << ping_res.get() << endl;
cout << set_res.get() << endl;
auto val = get_res.get();
if (val)
cout << *val << endl;
else
cout << " not exist " << endl;
hmset_res.get();
for ( const auto &ele : hgetall_res.get())
cout << ele.first << " t " << ele.second << endl;
// Generic interface.
// There's no *AsyncRedis::client_getname* interface.
// But you can use *Redis::command* to get the client name.
auto getname_res = async_redis.command<OptionalString>( " client " , " getname " );
val = getname_res.get();
if (val) {
std::cout << *val << std::endl;
}
async_redis.command<OptionalString>( " client " , " getname " ,
[](Future<OptionalString> &&fut) {
try {
auto val = fut. get ();
} catch ( const Error &e) {
// handle error
}
});
async_redis.command< long long >( " incr " , " number " ,
[](Future< long long > &&fut) {
try {
cout << fut. get () << endl;
} catch ( const Error &e) {
// handle error
}
});Aysnc interface also supports Redis Sentinel.
# include < sw/redis++/async_redis++.h >
SentinelOptions sentinel_opts;
sentinel_opts.nodes = {
{ " 127.0.0.1 " , 8000 },
{ " 127.0.0.1 " , 8001 },
{ " 127.0.0.1 " , 8002 }
};
sentinel_opts.connect_timeout = std::chrono::milliseconds( 100 );
sentinel_opts.socket_timeout = std::chrono::milliseconds( 100 );
auto sentinel = std::make_shared<AsyncSentinel>(sentinel_opts);
onnectionOptions connection_opts;
connection_opts.connect_timeout = std::chrono::milliseconds( 100 ); // Required.
connection_opts.socket_timeout = std::chrono::milliseconds( 100 ); // Required.
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ; // Optional. The default size is 1.
// Connect to master node.
AsyncRedis redis (sentinel, " mymaster " , Role::MASTER, connection_opts, pool_opts);
// The following code randomly connects to one of the slave nodes.
// AsyncRedis redis(sentinel, "mymaster", Role::SLAVE, connection_opts, pool_opts);
redis.set( " key " , " value " );
auto value = redis.get( " key " ).get(); The async support for sentinel is similar with the sync one, except that you need to create an AsyncSentinel object instead of a Sentinel object. Check Redis Sentinel for more details on SentinelOptions , ConnectionOptions and Role .
Aysnc interface also supports Redis Cluster. Instead of AsyncRedis , you need to create an AsyncRedisCluster object.
ConnectionOptions opts;
opts.host = " 127.0.0.1 " ;
opts.port = 6379 ;
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ;
auto async_cluster = AsyncRedisCluster(opts, pool_opts);
Future< bool > set_res = async_cluster.set( " key " , " val " );
Future<Optional<string>> get_res = async_cluster.get( " key " );
auto mget_res = async_cluster.mget<std::vector<OptionalString>>({ " {hashtag}key1 " , " {hashhag}key2 " , " {hashtag}key3 " });
unordered_map<string, string> m = {{ " a " , " b " }, { " c " , " d " }};
Future< void > hmset_res = async_redis.hmset( " hash " , m.begin(), m.end());
// Create an AsyncRedis object with hash-tag, so that we can send commands that has no key.
// It connects to Redis instance that holds the given key, i.e. hash-tag.
auto r = async_cluster.redis( " hash-tag " );
Future<string> ping_res = r.command<string>( " ping " ); NOTE : By default, when you use AsyncRedisCluster::redis(const StringView &hash_tag, bool new_connection = true) to create an AsyncRedis object, instead of picking a connection from the underlying connection pool, it creates a new connection to the corresponding Redis server. So this is NOT a cheap operation, and you should try to reuse this newly created AsyncRedis object as much as possible. If you pass false as the second parameter, you can create a AsyncRedis object without creating a new connection. However, in this case, you should be very careful, otherwise, you might get bad performance or even dead lock. Please carefully check the related pipeline section before using this feature. Also the returned AsyncRedis object is NOT thread-safe, and if it throws exception, you need to destroy it, and create a new one with the AsyncRedisCluster::redis method.
NOTE : I'm not quite satisfied with the interface of AsyncSubscriber . If you have a better idea, feel free to open an issue for discussion.
You can use AsyncSubscriber to subscribe to channels or patterns asynchronously. The interface is similar to Subscriber , except a few differences (please read Publish/Subscribe section first):
consume method for AsyncSubscriber . Once you setup callbacks, and subscribe to some channel, redis-plus-plus will run callbacks with received messages in the underlying event loop.AsyncSubscriber::subscribe , AsyncSubscriber::psubscriber and other related methods return Future<void> . You can use it to check if the subscription has been sent.AsyncSubscriber::on_error(ErrCallback &&) to handle possible errors. The error callback interface is: void (std::exception_ptr err) , and you can get the exception with given exception pointer. AsyncSubscriber is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.AsyncSubscriber is destroyed, the underlying connection will be closed. If there're still channels or patterns not unsubscribed, the error callback will be called. In order to avoid it, you need to call AsyncSubscriber::unsubscribe() or AsyncSubscriber::punsubscribe() to unsubscribe all channels or patterns before destroying AsyncSubscriber . NOTE: this behavior might be changed in the future, ie we'll unsubscribe channels and patterns in the destructor of AsyncSubscriber . The following example is a common pattern to use AsyncSubscriber :
// Create an `AsyncSubscriber`. You can create it with either an `AsyncRedis` or `AsyncRedisCluster` object.
auto sub = async_redis.subscriber();
// Set callbacks.
sub.on_message([](std::string channel, std::string msg) {
// Process message of MESSAGE type.
});
sub.on_pmessage([](std::string pattern, std::string channel, std::string msg) {
// Process message of PMESSAGE type.
});
sub.on_meta([](Subscriber::MsgType type, OptionalString channel, long long num) {
// Process message of META type.
});
// You need to set error callback to handle error.
sub.on_error([](std::exception_ptr e) {
try {
std::rethrow_exception (e);
} catch ( const std:: exception &err) {
std::cerr << " err: " << err. what () << std::endl;
}
});
// Subscribe to channels and patterns.
Future< void > fut1 = sub.subscribe( " channel " );
Future< void > fut2 = sub.psubscribe( " pattern1* " );
// Once you call `subscribe` or `psubscribe`, callbacks will be run in the underlying
// event loop automatically. NOTE : The following is an experimental feature, and might be modified or abandaned in the future.
By default, AsyncRedis and AsyncRedisCluster create a default event loop, and runs the loop in a dedicated thread to handle read and write operations. However, you can also share the underlying event loop with multiple AsyncRedis and AsyncRedisCluster objects. In order to do that, you need to create a std::shared_ptr<EventLoop> , and pass it to the constructors of AsyncRedis and AsyncRedisCluster .
auto event_loop = std::make_shared<EventLoop>();
auto redis = AsyncRedis(connection_opts, pool_opts, loop);
auto cluster = AsyncRedisCluster(connection_opts, pool_opts, Role::MASTER, loop); NOTE : You must ensure event_loop lives longer than AsyncRedis and AsyncRedisCluster objects.
Unfortunately, std::future doesn't support continuation so far, which is inconvenient. However, some other libraries, eg boost and folly, have continuation support.
By default, redis-plus-plus returns std::future for async interface. However, you can also make it return boost::future by specifying -DREDIS_PLUS_PLUS_ASYNC_FUTURE=boost when running cmake ( folly and other libraries might be supported in the future). Of course, in this case, you need to install Boost first (the minimum version requirement for Boost is 1.55.0 ).
cmake -DREDIS_PLUS_PLUS_BUILD_ASYNC=libuv -DREDIS_PLUS_PLUS_ASYNC_FUTURE=boost ..NOTE : When building your application code, don't forget to link boost related libs, eg -lboost_thread, -lboost_system.
Then you can take advantage of boost::future 's continuation support:
# include < sw/redis++/async_redis++.h >
ConnectionOptions opts;
opts.host = " 127.0.0.1 " ;
opts.port = 6379 ;
auto redis = AsyncRedis(opts);
auto fut = redis.get( " key " ).then([](sw::redis::Future<sw::redis::Optional<std::string>> fut) {
auto val = fut. get ();
if (val) cout << *val << endl;
});
// Do other things
// Wait for the continuation finishes.
fut.get();You can also use a thread pool to run the continuation:
# define BOOST_THREAD_PROVIDES_EXECUTORS
// You might also need to `#define BOOST_THREAD_USES_MOVE` with some version of Boost.
// See [this issue](https://github.com/sewenew/redis-plus-plus/issues/272) for detail.
# include < sw/redis++/async_redis++.h >
# include < boost/thread/executors/basic_thread_pool.hpp >
boost::executors::basic_thread_pool pool ( 3 );
auto fut = redis.get( " key " ).then(pool,
[](sw::redis::Future<sw::redis::Optional<std::string>> fut) {
auto val = fut. get ();
if (val) cout << *val << endl;
});
// Do other things
fut.get();redis-plus-plus also supports coroutine interface, however, coroutine support for Subscriber and Transaction is still on the way.
NOTE : Coroutine support is still experimental, and the interface might be changed in the future.
The coroutine interface depends on async interface, which depends on third-party event library. So you need to install libuv first, and hiredis v1.0.0 or later. Check async interface for detail.
When installing redis-plus-plus , you should specify the following command line options: -DREDIS_PLUS_PLUS_BUILD_ASYNC=libuv , -DREDIS_PLUS_PLUS_BUILD_CORO=ON and -DREDIS_PLUS_PLUS_CXX_STANDARD=20 .
cmake -DCMAKE_PREFIX_PATH=/installation/path/to/libuv/and/hiredis -DREDIS_PLUS_PLUS_CXX_STANDARD=20 -DREDIS_PLUS_PLUS_BUILD_ASYNC=libuv -DREDIS_PLUS_PLUS_BUILD_CORO=ON ..
make
make install The coroutine interface is similar to sync interface, except that you should include sw/redis++/co_redis++.h , and define an object of sw::redis::CoRedis or sw::redis::CoRedisCluster , and the related methods return sw::redis::CoRedis::Awaiter<Result> or sw::redis::CoRedisCluster::Awaiter<Result> object.
메모 :
# include < sw/redis++/co_redis++.h >
# include < cppcoro/task.hpp >
# include < cppcoro/sync_wait.hpp >
ConnectionOptions opts;
opts.host = " 127.0.0.1 " ;
opts.port = 6379 ;
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ;
// `CoRedisCluster` has similar inteface as `CoRedis`.
// auto co_redis_cluster = CoRedisCluster(opts, pool_opts);
auto co_redis = CoRedis(opts, pool_opts);
cppcoro::sync_wait ([&co_redis]() -> cppcoro::task<> {
try {
co_await co_redis. set ( " key " , " val " );
auto val = co_await co_redis. get ( " key " );
if (val)
cout << *val << endl;
else
cout << " not exist " << endl;
co_await co_redis. command < long long >( " incr " , " num " );
val = co_await co_redis. command <OptionalString>( " get " , " num " );
} catch ( const Error &e) {
cout << e. what () << endl;
}
}());Coroutine interface also supports Redis Sentinel.
# include < sw/redis++/co_redis++.h >
SentinelOptions sentinel_opts;
sentinel_opts.nodes = {
{ " 127.0.0.1 " , 8000 },
{ " 127.0.0.1 " , 8001 },
{ " 127.0.0.1 " , 8002 }
};
sentinel_opts.connect_timeout = std::chrono::milliseconds( 100 );
sentinel_opts.socket_timeout = std::chrono::milliseconds( 100 );
auto sentinel = std::make_shared<CoSentinel>(sentinel_opts);
onnectionOptions connection_opts;
connection_opts.connect_timeout = std::chrono::milliseconds( 100 ); // Required.
connection_opts.socket_timeout = std::chrono::milliseconds( 100 ); // Required.
ConnectionPoolOptions pool_opts;
pool_opts.size = 3 ; // Optional. The default size is 1.
// Connect to master node.
CoRedis co_redis (sentinel, " mymaster " , Role::MASTER, connection_opts, pool_opts);
// The following code randomly connects to one of the slave nodes.
// CoRedis co_redis(sentinel, "mymaster", Role::SLAVE, connection_opts, pool_opts);
cppcoro::sync_wait ([&co_redis]() -> cppcoro::task<> {
try {
auto val = co_await co_redis. get ( " key " );
if (val)
cout << *val << endl;
else
cout << " not exist " << endl;
} catch ( const Error &e) {
cout << e. what () << endl;
}
}()); The coroutine support for sentinel is similar with the sync one, except that you need to create an CoSentinel object instead of a Sentinel object. Check Redis Sentinel for more details on SentinelOptions , ConnectionOptions and Role .
We can create many interesting data structures and algorithms based on Redis, such as Redlock. We call these data structures and algorithms as Redis Patterns . redis-plus-plus will support some of these patterns.
NOTE : These patterns will be first implemented on the patterns branch. I'd like to hear your feedback on the API of these patterns, and when these APIs become stable, I'll merge the code into the master branch. So APIs on the patterns branch are NOT stable, and might be changed in the future.
Redlock is a distributed lock based on Redis. Thanks to @wingunder's suggestion, redis-plus-plus supports Redlock now. @wingunder and I made two different implementation of Redlock: one based on Lua script, and the other based on transaction. The Lua script version should be faster, and also it has many other parameters to control the behavior. However, if you are not allowed to, or don't want to run Lua scripts inside Redis, you could try using the transaction version.
Also there's a high level API, which works like std::mutex . With this high level API, you don't need to manually extend the lock, instead, the lock will be automatically extened by redis-plus-plus.
The basic idea of acquiring a Redlock is setting a key in Redis if the key does not exist. Since Redis operation is atomic, when mutiple clients acquire the same lock, ie setting the same key if it does not exist, only one client wins, and others will find the key has already been set. So only one client can acquire the lock, and others have to wait and try again.
When setting the key, we also need to set a TTL/expireation for the key. Otherwise, if the winning client crashes, the lock cannot be acquired by others forever. However, it also brings a new problem. Since the key has a TTL, once you acquire the lock, you must ensure all code in critical section must be finished before the key expires. Otherwise, other clients might acquire the lock successfully when you are still running critical section code (ie more than one clients acquire the lock successfully). So when you run critical section code, you have to check if the key is going to be expired and extend the lock (ie extending the TTL) before key expires, from time to time.
Also, in order to make the algorithm more robust, normally we need to set key on multiple independent stand-alone Redis (not Redis Cluster).
There're still more details on the mechanism of Redlock. Please read Redlock's doc for more info, before using it.
The high level API is quite simple. It works like a std::mutex , and can be used with std::lock_guard and std::unique_lock . Also it can automatically extend the lock before the key expires. So that user code doesn't need to extend the lock manually. In order to use Redlock, you can create a RedMutex object with the following parameters:
Redis instances: There're two versions of Redlock, ie single instance version and multiple instances version. The multiple instances version is more robust.RedMutex should be created with the same resource id.RedMutexOptions (optional): Some options to control the behavior of RedMutex . If not specified, default options will be used. Check below for more detail.LockWatcher (optional): A watcher which will automatically extend the lock before it expires. So that you don't need to manually check if the lock has been expired. If no watcher is specified (the default behavior), redis-plus-plus will create a one for this Redlock. Check below for more detail. class RedMutex {
public:
RedMutex(std::initializer_list<std::shared_ptr<Redis>> masters,
const std::string &resource,
std::function<void (std::exception_ptr)> auto_extend_err_callback = nullptr,
const RedMutexOptions &opts = {},
const std::shared_ptr<LockWatcher> &watcher = nullptr);
void lock();
bool try_lock();
void unlock();
};
As we mentioned the high level API can automatically extend the lock. However, we might fail to extend the lock, eg connection to Redis is broken. In that case, the auto_extend_err_callback will be called, so that the application can be notified that the lock might no longer be locked, and stop running code in critical section.
The following is the prototype of error callback.
void (std::exception_ptr err);
If error callback is not set (the default behavior), the error will be ignored. And you're on risk of running critical section code with multiple clients.
struct RedMutexOptions {
std::chrono::milliseconds ttl;
std::chrono::milliseconds retry_delay;
bool scripting = true;
};
ttl , otherwise, you might fail to lock or fail to extend the lock.RedMutex::lock repeat trying to lock until it acquires the lock. If it fails, it wait retry_delay before the next retrying. 100 milliseconds by default. LockWatcher watches RedMutex , and try to extend the lock from time to time. You can construct RedMutex with a std::shared_ptr<LockWatcher> , so that it will watch the corresponding Redlock. LockWatcher does the work in a background thread. So creating a LockWatcher object also creates a std::thread . If you want to avoid creating multiple threads, you can construct multiple RedMutex with the same std::shared_ptr<LockWatcher> .
If you don't specify LockWatcher , RedMutex will create one (the default behavior), and start a thread. Although it's expensive to create thread, it's still quite cheap compared to acquiring a distributed lock.
RedMutex is NOT reentrant. If you try to lock a mutex which has already been locked by the current thread, the behavior is undefined.# include < memory >
# include < sw/redis++/redis++.h >
# include < sw/redis++/patterns/redlock.h >
auto redis = std::make_shared<Redis>( " tcp://127.0.0.1 " );
auto redis1 = std::make_shared<Redis>( " tcp://127.0.0.1:7000 " );
auto redis2 = std::make_shared<Redis>( " tcp://127.0.0.1:7001 " );
auto redis3 = std::make_shared<Redis>( " tcp://127.0.0.1:7002 " );
try {
{
// Create a `RedMutex` with a single stand-alone Redis and default settings.
RedMutex mtx (redis, " resource " );
std::lock_guard<RedMutex> lock (mtx);
}
{
// Create a `RedMutex` with multiple stand-alone Redis and default settings.
RedMutex mtx ({redis1, redis2, redis3}, " resource " );
std::lock_guard<RedMutex> lock (mtx);
}
{
RedMutexOptions opts;
opts. ttl = std::chrono::seconds ( 5 );
auto watcher = std::make_shared<LockWatcher>();
// Create a `RedMutex` with auto_extend_err_callback and other options.
RedMutex mtx ({redis1, redis2, redis3}, " resource " ,
[](std::exception_ptr err) {
try {
std::rethrow_exception (err);
} catch ( const Error &e) {
// Notify application code that the lock might no longer be locked.
}
},
opts, watcher);
std::unique_lock<RedMutex> lock (mtx, std::defer_lock);
lock. lock ();
lock. unlock ();
lock. try_lock ();
}
} catch ( const Error &err) {
// handle error.
} // Lua script version:
{
RedLockMutex mtx({redis1, redis2, redis3}, "resource");
// Not locked.
RedLock<RedLockMutex> lock(mtx, std::defer_lock);
// Try to get the lock, and keep 30 seconds.
// It returns the validity time of the lock, i.e. the lock is only
// valid in *validity_time*, after that the lock might be acquired by others.
// If failed to acquire the lock, throw an exception of Error type.
auto validity_time = lock.try_lock(std::chrono::seconds(30));
// Extend the lock before the lock expired.
validity_time = lock.extend_lock(std::chrono::seconds(10));
// You can unlock explicitly.
lock.unlock();
} // If unlock() is not called, the lock will be unlocked automatically when it's destroied.
// Transaction version:
{
RedMutex mtx({redis1, redis2, redis3}, "resource");
RedLock<RedMutex> lock(mtx, std::defer_lock);
auto validity_time = lock.try_lock(std::chrono::seconds(30));
validity_time = lock.extend_lock(std::chrono::seconds(30));
// You can unlock explicitly.
lock.unlock();
}
hset related methods return long long instead of bool . redis-plus-plus is written by sewenew, who is also active on StackOverflow.
Many thanks to all contributors of redis-plus-plus , especially @wingunder.