封装Redis使用的初步探索
开发环境
基于C++提出一套接口,并使用redis源码实现,对外提供一个CRedisHandler的方法类,将redis的相关源码和依赖编译入IxRedisHandler-Static静态库内。
即对外提供的内容:
- IxRedisHandler.h
- IxRedisHandler-Static.lib
源码下载地址:https://github.com/redis/hiredis
服务器下载地址:https://redis.io/download
本文使用的服务器版本号为:5.0.10
在验证redis功能之前,记住要启动redis-server服务器。
在Windows上的操作示例如下:
工程建立
使用CMakeLists.txt组织工程,编译出以上支持的在VS平台上使用的库。
CMakeLists.txt组织工程:
根CMakeLists.txt的关键部分,即关联RedisClientApp和IxRedisHandler-Static,以及hiredis源码
#--------------------------------------------------------------------
# 添加子目录
#--------------------------------------------------------------------
add_subdirectory(hiredis)
add_subdirectory(IxRedisHandler-Static)
add_subdirectory(RedisClientApp)
IxRedisHandler-Static的CMakeLists.txt
hiredis的源码直接提供了CMakeLists.txt文件,所以可以直接加入,里面会有三个工程建立:hiredis-test、hiredis_static、hiredis,如果不需要可以手动改hiredis提供的CMakeLists.txt。
不过对于想要达到的目的来说,hiredis提供的CMakeLists.txt并不重要,因为我们只需要编出将redis相关方法编入了的 IxRedisHandler-Static即可。
IxRedisHandler-Static的CMakeLists.txt实现是直接将redis需要的源码全部编入的方法,然后对外只提供CRedisHandler类接口调用:
# 工程名称
set(PROJECT_NAME IxRedisHandler-Static)
project(${PROJECT_NAME}
VERSION ${CMAKE_PROJECT_VERSION})
# Hiredis requires C99
INCLUDE(GNUInstallDirs)
SET(CMAKE_C_STANDARD 99)
SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
SET(CMAKE_DEBUG_POSTFIX d)
#------------------------------------------------
# 依赖库/框架
#------------------------------------------------
#--------------------------------------------------------------------
# 源代码文件
#--------------------------------------------------------------------
# 生成target
add_library(${PROJECT_NAME} STATIC)
file(GLOB INC_FILES Include/*.h)
file(GLOB INC_FILES1 Src/*.cpp)
set(${PROJECT_NAME}_Includes
${INC_FILES}
${INC_FILES1}
)
aux_source_directory(Src CPP_FILES)
set(${PROJECT_NAME}_SRCS
${CPP_FILES}
)
SET(hiredis_sources
../../hiredis/alloc.c
../../hiredis/async.c
../../hiredis/dict.c
../../hiredis/hiredis.c
../../hiredis/net.c
../../hiredis/read.c
../../hiredis/sds.c
../../hiredis/sockcompat.c
../../hiredis/hiredis.h
../../hiredis/read.h
../../hiredis/sds.h
../../hiredis/async.h
../../hiredis/alloc.h
)
target_sources(${PROJECT_NAME}
PRIVATE
${${PROJECT_NAME}_Includes}
${${PROJECT_NAME}_SRCS}
${hiredis_sources}
)
#--------------------------------------------------------------------
# 工程依赖
#--------------------------------------------------------------------
# 依赖库
#target_link_libraries(${PROJECT_NAME}
# PRIVATE
# hiredis
#)
IF(WIN32 OR MINGW)
TARGET_LINK_LIBRARIES(IxRedisHandler-Static PUBLIC ws2_32 crypt32)
ENDIF()
# 路径寻址
target_include_directories(${PROJECT_NAME}
PRIVATE
./Include
)
#--------------------------------------------------------------------
# 事件
#--------------------------------------------------------------------
如果用CMake工具Build出一个VS2015的工程,界面大概呈现方式是这样的:
源码
redis命令
redis主要的调用方式主要在hiredis提供的源码test.c中也写得比较详细了,同时也可以直接启动hiredis-test编出的可执行文件进行验证。
如下是总结的Redis支持的数据类型相应的执行命令,也可以先用redis-cli.exe启动后,用命令行执行看结果:
因为最终封装也是基于Redis的命令提供接口。
Redis for C++源码调用
以下展示几个关于操作数据库最关键的几个调用示例,以及主要验证了redis支持Binary的效果:
连接
redisContext* pConnect = redisConnect("127.0.0.1", 6379);
if (NULL == pConnect)
{
std::cout << "redis build handler failed" << std::endl;
return 0;
}
if (pConnect->err)
{
std::cout << "connect error: %s\n" << pConnect->errstr << std::endl;
return 0;
}
写入
// 方法一
redisReply* reply = (redisReply*)redisCommand(pConnect, "HMSET %s %d %s", "baidu-building", 2, "xxx23");
reply = (redisReply*)redisCommand(pConnect, "HMSET %s %d %s", "baidu-building", 3, "xxx33");
freeReplyObject(reply);
// 方法二
std::vector<int> vTest;
vTest.push_back(1);
vTest.push_back(22);
vTest.push_back(333);
int nLen = sizeof(int) * vTest.size();
char* pBuffer = new char[nLen];
memcpy(pBuffer, &vTest[0], nLen);
int paramN = 4; // 命令参数个数
std::string cmd = "HMSET";
std::string key = "baidu-building";
std::string strField = "1056";
const char* parms[] = { cmd.data() , key.data(), strField.data(), pBuffer }; // 命令
size_t paramsLens[] = { cmd.size() , key.size(), strField.size(), nLen }; // 命令中每个参数的个数
redisReply* r = (redisReply*)redisCommandArgv(pConnect, paramN, parms, paramsLens);
freeReplyObject(r);
delete[] pBuffer;
读取
// 方法一
redisReply* reply = (redisReply*)redisCommand(pConnect, "keys *", nMajor);
for (int i = 0; i < reply->elements; i++)
{
std::cout << reply->element[i]->str << std::endl;
}
freeReplyObject(reply);
// 方法二
int paramN = 3; // 命令参数个数
std::string cmd = "HMGET";
std::string key = "baidu-building";
std::string strField = "1056";
const char* parms[] = { cmd.data() , key.data(), strField.data() }; // 命令
size_t paramsLens[] = { cmd.size() , key.size(), strField.size() }; // 命令中每个参数的个数
redisReply* r = (redisReply*)redisCommandArgv(pConnect, paramN, parms, paramsLens);
if (r->elements > 0)
{
std::vector<int> vTest;
vTest.resize(3);
memcpy(&vTest[0], r->element[0]->str, r->element[0]->len);
for (int i = 0; i < vTest.size(); i++)
{
std::cout << vTest[i] << "\t";
}
}
freeReplyObject(r);
Redis返回结果
redisReply是redis的命令执行后的返回结果:
解析内容需要首先识别type类型,然后从integer/dval/len+str获取解析出的值。
接口提供
Redis作为非关系型数据库,其基本的封装肯定少不了连接和断开,包括数据请求,参数获取,事务支持等的操作,因此,这一层的封装主要是为屏蔽redis依赖库,避免C风格的调用方式。对于上层理解Redis,是作为一个数据库操作器使用,而这个操作器的每一个调用都不应该有副作用(没有指针未释放的细节处理),返回的结果是常规意义上的理解方式。
/**
* @file IxRedisHandler.h
* @brief Redis访问的Client操作器
* @note 使用方法: 注意Redis持久化的方式:snapshotting和aof
* @author Being
* @date 2021/8/23
* @version V00.00.01
* @CopyRight swiet.com
*/
#ifndef IxRedisHandler_h__
#define IxRedisHandler_h__
#include <string>
#include <vector>
struct redisContext;
struct redisReply;
class CRedisHandler
{
public:
CRedisHandler();
~CRedisHandler();
/**
* @class IRedisReplyCallBack
* @brief (Redis请求指令后)返回数据的回调类
* @note
*/
class IRedisReplyCallBack
{
public:
virtual ~IRedisReplyCallBack() {}
/**
* @fn OnReplyString
* @brief 响应Redis返回的二进制/字串类型数据
* @param[in] CRedisHandler * pHandler: Redis操作器
* @param[in] char * pBuffer: 二进制数据
* @param[in] int nLen: 数据长度
* @return bool true 表示已被处理,截断;false表示未被处理,继续
*/
virtual bool OnReplyString(CRedisHandler* pHandler, char* pBuffer, int nLen) { return false; };
/**
* @fn OnReplyDouble
* @brief 响应Redis返回的浮点数类型数据
* @param[in] CRedisHandler * pHandler: Redis操作器
* @param[in] double dValue: 数据
* @return bool true 表示已被处理,截断;false表示未被处理,继续
*/
virtual bool OnReplyDouble(CRedisHandler* pHandler, double dValue) { return false; };
/**
* @fn OnReplyInteger
* @brief 响应Redis返回的整性数据
* @param[in] CRedisHandler * pHandler: Redis操作器
* @param[in] int64_t nValue: 数据
* @return bool true 表示已被处理,截断;false表示未被处理,继续
*/
virtual bool OnReplyInteger(CRedisHandler* pHandler, int64_t nValue) { return false; };
};
/**
* @fn Initialize
* @brief 初始化操作器,连接Redis服务器
* @param[in] const std::string & strIP: 服务器IP
* @param[in] int nPort: 服务器端口号
* @param[in] int nDBIndex: 数据库编号(0-16)
* @return bool 是否初始化连接成功
*/
bool Initialize(const std::string& strIP, int nPort = 6379, int nDBIndex = 0);
/**
* @fn Uninitialze
* @brief 卸载操作器,断开与Redis的服务
* @return void
*/
void Uninitialze();
/**
* @fn IsValid
* @brief 操作器当前是否处于可用状态
* @return bool 是否可用
*/
bool IsValid() const;
/**
* @fn Reconnect
* @brief 尝试重连
* @return void
*/
void Reconnect();
/**
* @fn GetIP
* @brief 获取服务器IP地址
* @return std::string IP地址
*/
std::string GetIP() const;
/**
* @fn GetPort
* @brief 获取服务器端口号
* @return int 端口号
*/
int GetPort() const;
/**
* @fn GetDatabaseIndex
* @brief 获取选择的数据库索引号
* @return int 索引号
*/
int GetDatabaseIndex()const;
/**
* @fn QueryRedisVersion
* @brief 请求Redis连接服务器的版本号
* @param[in] int & rMajor: 主版本号
* @param[in] int & rMinor: 副版本号
* @return bool 是否请求成功
*/
bool QueryRedisVersion(int& rMajor, int& rMinor);
/**
* @fn CountKeys
* @brief 计算当DB内的key个数
* @return int 存中key个数
*/
int CountKeys();
/**
* @fn ExecuteSaveCommand
* @brief 执行一次持久化命令(在snapshotting持久化操作下的一次bgsave)
* @return bool 是否执行成功
*/
bool ExecuteSaveCommand();
/**
* @fn ExcuteFlushDBKey
* @brief 执行清空数据库所有Key操作
* @return bool 是否执行成功
*/
bool ExcuteFlushDBKey();
/**
* @fn ExecuteRawCommand
* @brief 执行Redis指令(直接输入)
* @param[in] const std::string & strCommand: 指令
* @param[in] IRedisReplyCallBack * pReply: 响应数据回调
* @return bool 是否执行成功
*/
bool ExecuteRawCommand(const std::string& strCommand, IRedisReplyCallBack* pReply = NULL);
/**
* @fn ExecuteCommand
* @brief 执行Redis指令(输入命令集)
* @param[in] int nCommandCount: 指令个数 eg: 4
* @param[in] const char * * cArgvCommands: 指令数据数组 eg: {"HMSET", "building", "1056", pBuffer}
* @param[in] const size_t * cArgvCommandsLen: 指令长度数组 eg: {5, 8, 4, BufferLen}
* @param[in] IRedisReplyCallBack * pReply: 响应数据回调
* @return bool是否执行成功
*/
bool ExecuteCommand(int nCommandCount, const char** cArgvCommands, const size_t* cArgvCommandsLen, IRedisReplyCallBack* pReply = NULL);
public: // 事务(Redis事务非原子性)
/**
* @fn Transaction
* @brief 标记一个事务块开始
* @return bool 是否启动成功
*/
bool Transaction();
/**
* @fn DiscardTransaction
* @brief 取消事务,放弃执行事务块内的所有命令(与回滚不同,所以区别于Rollback)
* @return bool 是否取消成功
*/
bool DiscardTransaction();
/**
* @fn SetWatchKey
* @brief 监视一个或多个key,如果在执行事务前,key被其它指令改动,事务被打断
* @param[in] const std::vector<std::string> & vecKeys: key
* @return bool 是否设置成功
*/
bool SetWatchKey(const std::vector<std::string>& vecKeys);
/**
* @fn UnWatch
* @brief 取消对所有key的监视
* @return bool 是否取消成功
*/
bool UnWatch();
/**
* @fn IsInTransaction
* @brief 当前是否处于事务块中
* @return bool 是否处于
*/
bool IsInTransaction() const;
/**
* @fn Commit
* @brief 执行事务块内的所有命令
* @return bool 是否执行成功
*/
bool Commit();
private:
// true 表示已被处理,截断;false表示未被处理,继续
bool ReplyCallBack(redisReply* reply, IRedisReplyCallBack* pReply);
private:
redisContext* m_pHandler;
std::string m_strIP;
int m_nPort;
int m_nDBIndex;
bool m_bInTrasaction;
};
#endif // IxRedisHandler_h__
调用示例
封装完成后,可以在RedisClientApp中的main函数内进行测试:
class CReplayCallBack : public CRedisHandler::IRedisReplyCallBack
{
public:
CReplayCallBack() {}
~CReplayCallBack() {}
virtual void OnReplyString(CRedisHandler* pHandler, char* pBuffer, int nLen)
{
std::cout << pBuffer << std::endl;
}
};
int main(int argc, char* argv[])
{
CRedisHandler redisHandler;
bool bInit = redisHandler.Initialize("127.0.0.1");
std::string str = "HMSET baidu-building 6 xxx33";
redisHandler.ExecuteRawCommand(str);
CReplayCallBack redisCallback;
redisHandler.ExecuteRawCommand("keys *", &redisCallback);
redisHandler.Uninitialze();
getchar();
return 0;
}
更复杂的调用方式在后面实际的应用场景中介绍。