C++封装Redis操作函数
1、在Linux上安装Redis
1.1、编译源码安装
安装Redis的方法有很多,我这里直接拿源码来编译安装,使用的Redis版本是6.2.1,大家可以自行去Redis官网下载对应的源码,这里给出一份已经下载好的Redis-6.2.1安装包
点击进入下载页面
,下载完成之后,将代码上传到Linux服务器上,可以参考我这篇文章来搭建一个CentOS环境
bifang框架运行环境搭建入门指南
解压出来之后如图所示,大家可以看一下README.md,里面有说明教程,这里就不对其进行分析了,直接开始编译安装
输入 make 编译源代码
输入 make install 将Redis安装到默认目录(/usr/local/bin)里去
输入 cp redis.conf /etc/ 将Redis的配置文件复制到/etc目录下
1.2、配置redis.service
输入 vim /usr/lib/systemd/system/redis.service,将下面的内容复制进去
# example systemd service unit file for redis-server
#
# In order to use this as a template for providing a redis service in your
# environment, _at the very least_ make sure to adapt the redis configuration
# file you intend to use as needed (make sure to set "supervised systemd"), and
# to set sane TimeoutStartSec and TimeoutStopSec property values in the unit's
# "[Service]" section to fit your needs.
#
# Some properties, such as User= and Group=, are highly desirable for virtually
# all deployments of redis, but cannot be provided in a manner that fits all
# expectable environments. Some of these properties have been commented out in
# this example service unit file, but you are highly encouraged to set them to
# fit your needs.
#
# Please refer to systemd.unit(5), systemd.service(5), and systemd.exec(5) for
# more information.
[Unit]
Description=Redis
Documentation=https://redis.io/documentation
#Before=your_application.service another_example_application.service
#AssertPathExists=/var/lib/redis
Wants=network.target
After=network.target
[Service]
#ExecStart=/usr/local/bin/redis-server --supervised systemd --daemonize no
## Alternatively, have redis-server load a configuration file:
ExecStart=/usr/local/bin/redis-server /etc/redis.conf
LimitNOFILE=10032
NoNewPrivileges=yes
#OOMScoreAdjust=-900
#PrivateTmp=yes
#Type=notify
TimeoutStartSec=infinity
TimeoutStopSec=infinity
UMask=0077
#User=redis
#Group=redis
#WorkingDirectory=/var/lib/redis
[Install]
WantedBy=multi-user.target
之后就可以通过service来控制Redis了
输入 service redis start 启动Redis
输入 service redis status 查看Redis状态,如下图所示,就是Redis启动成功了
再输入redis-cli,登录Redis控制台,输入keys *,出现如下图所示的情况,证明Redis可用
![]()
1.3、安装Redis的c库hiredis
进入Redis源码目录,输入cd deps/hiredis/,进入hiredis目录
输入make,编译代码
输入make install,安装hiredis库,这样Redis的整个安装就结束了
2、Redis常用API
要想封装出一个易用的Redis库,就需要先知道官方究竟开放了哪些接口给我们使用。由于前面是用源码安装的,所以我们要看API可以直接进去源码里面的deps/hiredis/目录下就有了,大部分有用的信息都在hiredis.h文件里面,大家可以先把这个文件大致浏览一遍,内容比起MySQL的要少很多,接下来会列举我们用到的几个重要的结构体和API
2.1、结构体
- redisContext相当于一个控制器的作用,调用初始化函数之后就能得到他们,在使用各种API时经常得把他们作为参数传递进去
typedef struct redisContext {
const redisContextFuncs *funcs; /* Function table */
int err; /* Error flags, 0 when there is no error */
char errstr[128]; /* String representation of error when applicable */
redisFD fd;
int flags;
char *obuf; /* Write buffer */
redisReader *reader; /* Protocol reader */
enum redisConnectionType connection_type;
struct timeval *connect_timeout;
struct timeval *command_timeout;
struct {
char *host;
char *source_addr;
int port;
} tcp;
struct {
char *path;
} unix_sock;
/* For non-blocking connect */
struct sockadr *saddr;
size_t addrlen;
/* Optional data and corresponding destructor users can use to provide
* context to a given redisContext. Not used by hiredis. */
void *privdata;
void (*free_privdata)(void *);
/* Internal context pointer presently used by hiredis to manage
* SSL connections. */
void *privctx;
/* An optional RESP3 PUSH handler */
redisPushFn *push_cb;
} redisContext;
- redisReply是使用Redis命令之后的返回值,里面有查询的结果以及错误信息,需要了解里面各个变量的意义,大家自行看官方每个变量的注释即可
/* This is the reply object returned by redisCommand() */
typedef struct redisReply {
int type; /* REDIS_REPLY_* */
long long integer; /* The integer when type is REDIS_REPLY_INTEGER */
double dval; /* The double when type is REDIS_REPLY_DOUBLE */
size_t len; /* Length of string */
char *str; /* Used for REDIS_REPLY_ERROR, REDIS_REPLY_STRING
REDIS_REPLY_VERB, and REDIS_REPLY_DOUBLE (in additional to dval). */
char vtype[4]; /* Used for REDIS_REPLY_VERB, contains the null
terminated 3 character content type, such as "txt". */
size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */
struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */
} redisReply;
2.2、API
// 连接Redis,使用后面那个,可以设置超时时间
redisContext *redisConnect(const char *ip, int port);
redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv);
// 关闭Redis并释放对应的内存
void redisFree(redisContext *c);
// 重新连接Redis(需要注意的是重新连接之后需要重新输入密码才可以使用Redis)
int redisReconnect(redisContext *c);
// 设置Redis命令超时时间
int redisSetTimeout(redisContext *c, const struct timeval tv);
// 向Redis发出命令。在阻塞上下文中,它与调用redisAppendCommand,然后调用redisGetReply相同。
// 如果执行请求时出错,函数将返回NULL,否则返回应答。在非阻塞上下文中,它与仅调用redisAppendCommand相同,并且始终返回NULL。
void *redisvCommand(redisContext *c, const char *format, va_list ap);
void *redisCommand(redisContext *c, const char *format, ...);
void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
// 释放查询结果redisReply的内存
void freeReplyObject(void *reply);
// 将命令写入输出缓冲区。在阻塞模式下使用这些函数可以获得命令管道
int redisvAppendCommand(redisContext *c, const char *format, va_list ap);
int redisAppendCommand(redisContext *c, const char *format, ...);
int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
3、Redis封装细节
3.1、和之前封装MySQL进行对比
我们这里不实现Redis的事务,而且也没有MySQL那些预处理的功能,所以比起之前的MySQL封装会简单很多,只需要两个类Redis和RedisManager即可
3.2、封装两个常用的类
/**
* brief: Redis类
*/
class Redis;
/**
* brief: Redis管理类
*/
class RedisManager;
3.2.1、Redis
该类要要提供的功能有:初始化Redis、连接Redis、执行Redis命令,后者有可以细化为很多部分,不如 ping、del、keys等操作,具体Redis支持的功能可以上百度去查,我这里大部分的方法都是模仿java的jredis提供的接口来实现的,接下来一一举例
- 连接数据库的方法实现如下,需要对返回值进行判断,在使用auth指令登录后,返回值中的str成员必须是“OK”才算登录成功了
bool Redis::reconnect()
{
if (!m_context)
return false;
if (redisReconnect(m_context.get()))
return false;
redisSetTimeout(m_context.get(), m_cmdTimeout);
if (!m_passwd.empty())
{
redisReply* r = (redisReply*)redisCommand(m_context.get(), "auth %s", m_passwd.c_str());
if (!r)
{
std::cout << "auth error(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
if (r->type != REDIS_REPLY_STATUS)
{
std::cout << "auth reply type error:" << r->type << "(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
if (!r->str)
{
std::cout << "auth reply str error:NULL(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
if (strncasecmp(r->str, "OK", 2))
{
std::cout << "auth error:" << r->str << "(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
}
return true;
}
bool Redis::connect()
{
redisContext* conn = redisConnectWithTimeout(m_host.c_str(), m_port, m_connectTimeout);
if (conn)
{
m_context.reset(conn, redisFree);
redisSetTimeout(m_context.get(), m_cmdTimeout);
if (!m_passwd.empty())
{
redisReply* r = (redisReply*)redisCommand(conn, "auth %s", m_passwd.c_str());
if (!r)
{
std::cout << "auth error(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
if (r->type != REDIS_REPLY_STATUS)
{
std::cout << "auth reply type error:" << r->type << "(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
if (!r->str)
{
std::cout << "auth reply str error:NULL(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
if (strncasecmp(r->str, "OK", 2))
{
std::cout << "auth error:" << r->str << "(" << m_host << ":" << m_port << ")" << std::endl;
return false;
}
}
return true;
}
return false;
}
- 连接上之后便可开始使用Redis了,利用官方的redisvCommand这些接口封装成一个好几个更易用的方法,如下所示,使用了智能指针去托管返回信息,这样就不需要每次都自己去释放返回值了
ReplyPtr Redis::cmd(const char* format, ...)
{
va_list ap;
va_start(ap, format);
redisReply* r = (redisReply*)redisvCommand(m_context.get(), format, ap);
if (!r)
{
std::cout << "redisvCommand error:(" << format << ")(" << m_host << ":" << m_port << ")" << std::endl;
va_end(ap);
return nullptr;
}
ReplyPtr ret(r, freeReplyObject);
if (r->type == REDIS_REPLY_ERROR)
{
std::cout << "redisvCommand error:(" << format << ")(" << m_host << ":" << m_port
<< ")" + (r->str ? ", errstr:" + std::string(r->str) : "") << std::endl;
va_end(ap);
return nullptr;
}
va_end(ap);
return ret;
}
ReplyPtr Redis::cmd(const std::vector<std::string>& argv)
{
std::vector<const char*> v;
std::vector<size_t> l;
for (auto& i : argv)
{
v.push_back(i.c_str());
l.push_back(i.size());
}
redisReply* r = (redisReply*)redisCommandArgv(m_context.get(), argv.size(), &v[0], &l[0]);
if (!r)
{
std::cout << "redisCommandArgv error:(" << m_host << ":" << m_port << ")" << std::endl;
return nullptr;
}
ReplyPtr ret(r, freeReplyObject);
if (r->type == REDIS_REPLY_ERROR)
{
std::cout << "redisCommandArgv error:(" << m_host << ":" << m_port
<< ")" + (r->str ? ", errstr:" + std::string(r->str) : "") << std::endl;
return nullptr;
}
return ret;
}
bool Redis::appendCmd(const char* format, ...)
{
va_list ap;
va_start(ap, format);
int ret = redisvAppendCommand(m_context.get(), format, ap);
va_end(ap);
return !ret;
}
bool Redis::appendCmd(const std::vector<std::string>& argv)
{
std::vector<const char*> v;
std::vector<size_t> l;
for (auto& i : argv)
{
v.push_back(i.c_str());
l.push_back(i.size());
}
return !redisAppendCommandArgv(m_context.get(), argv.size(), &v[0], &l[0]);
}
- 基于上面封装的cmd这个方法,我们就可以逐步细化来吧Redis那些操作全部封装成类方法了,因为命令Redis的指令比较多,最快的方法就是去看一下java的jredis代码,仿照它的接口自己来一个一个实现,要注意不同操作所带来的返回值的类型是不一致的,要对其进行判断,这里贴几个比较典型的方法,就不全部贴出来了
int64_t Redis::del(std::unordered_set<std::string> keys)
{
std::string cmd_str = "DEL";
for (auto it = keys.begin(); it != keys.end(); it++)
cmd_str += " " + *it;
ReplyPtr reply = cmd(cmd_str.c_str());
if (!reply || reply->type != REDIS_REPLY_INTEGER)
return -1;
return reply->integer;
}
std::unordered_set<std::string> Redis::keys(const std::string& pattern)
{
std::unordered_set<std::string> s;
ReplyPtr reply = cmd("KEYS %s", pattern.c_str());
if (!reply || reply->type != REDIS_REPLY_ARRAY)
return std::move(s);
for (size_t i = 0; i < reply->elements; i++)
s.insert(reply->element[i]->str);
return std::move(s);
}
int64_t Redis::ttl(const std::string& key)
{
ReplyPtr reply = cmd("TTL %s", key.c_str());
if (!reply || reply->type != REDIS_REPLY_INTEGER)
return -3;
return reply->integer;
}
bool Redis::exists(const std::string& key)
{
ReplyPtr reply = cmd("EXISTS %s", key.c_str());
if (!reply || reply->type != REDIS_REPLY_INTEGER)
return false;
return (bool)reply->integer;
}
bool Redis::expire(const std::string& key, int64_t seconds)
{
ReplyPtr reply = cmd("EXPIRE %s %ld", key.c_str(), seconds);
if (!reply || reply->type != REDIS_REPLY_INTEGER)
return false;
return (bool)reply->integer;
}
bool Redis::persist(const std::string& key)
{
ReplyPtr reply = cmd("PERSIST %s", key.c_str());
if (!reply || reply->type != REDIS_REPLY_INTEGER)
return false;
return (bool)reply->integer;
}
bool Redis::set(const std::string& key, const std::string& value)
{
return !!cmd("SET %s %s", key.c_str(), value.c_str());
}
bool Redis::hset(const std::string& key, const std::string& field, const std::string& value)
{
return !!cmd("HSET %s %s %s", key.c_str(), field.c_str(), value.c_str());
}
3.2.2、RedisManager
该类是一个管理类,用于统一管理所有Redis连接,这样可以很方便地结合配置文件来使用 Redis,而且也可以在每次分配连接时都检查是否需要重新连接数据库,防止服务器因为超时把连接断开,并且提供了回收机制,当Redis池的数据小于我们设置的容量时,每一个被释放的连接都可以重新回到数据池里面循环使用,是借用智能指针来实现这个功能的,大家有兴趣可以看一看具体实现的做法
/**
* brief: Redis管理类
*/
class RedisManager
{
public:
typedef Mutex MutexType;
struct RedisConf
{
std::string host;
int port;
std::string passwd;
uint32_t connect_timeout = 100;
uint32_t cmd_timeout = 100;
uint32_t poolSize = 10;
};
~RedisManager();
void add(const std::string& name, const std::string& host,
int port, const std::string& passwd, uint32_t connectTimeout,
uint32_t cmdTimeout, uint32_t poolSize = 10);
Redis::ptr get(const std::string& name);
void checkConnection(int sec = 30);
private:
void freeRedis(const std::string& name, Redis* r);
private:
MutexType m_mutex;
std::unordered_map<std::string, std::list<Redis*> > m_connections;
std::unordered_map<std::string, RedisConf> m_sqlDefines;
};
4、总结并附上本文源代码
Redis官方给出的API使用起来非常方便,比MySQL简单很多,毕竟作为菲关系型数据库确实功能上少很多用起来也简单很多,有兴趣的同学建议还是去看一下Redis的源码,这样对自己的能力应该会有很大的提升。
最后附上一份源代码,大家可以下载下去调试使用看看,有什么错误的地方也欢迎大家指出
redis c++封装.zip