C++封装Redis操作函数

  • Post author:
  • Post category:其他




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



版权声明:本文为weixin_43798887原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。