Redis安装及hiredis初探

转载请注明出处:https://blog.csdn.net/mymottoissh/article/details/83001716

前文书介绍过MongoDb的服务端和客户端的安装、配置以及项目中的集成过程。简单来说,MongoDb存储就是写json,而今天的redis是写键值对。本文主要介绍一下redis、安装过程以及C语言驱动的使用

关键字:数据库 Redis hiredis

安装Redis

下载源码:http://www.redis.cn/download.html 得到压缩文件 redis-4.0.11.tar.gz 并解压、编译及安装。

make
make PREFIX=/usr/local/redis install

其中PREFIX表示安装了路径。

复制源码文件夹中的redis.conf文件到安装路径下作为配置文件。其中主要目录结构为

redis-benchmark --性能测试工具 

redis-check-aof --AOF 文件修复工具

redis-check-dump --RDB文件检查工具(快照持久化文件)

redis-cli --命令行客户端

redis-server --redis服务器启动命令

启动Redis

服务端启动:

直接执行/usr/local/redis/bin路径下的redis-server进行启动

./redis-server

启动时也可以同时指定配置文件

./redis-server ../redis.conf

客户端启动

./redis-cli -h [ip] -p [port]

关于redis配置文件的详细说明可参见另一篇文章:https://blog.csdn.net/mymottoissh/article/details/83002403

Redis数据格式

Redis是使用键值对进行数据存储的。其中,键统一为字符串,值包含5种数据类型。

String

字符串是Redis最常用的类型,且是二进制安全的。字符串类型的value数据最大可容纳512M。

例:set name zhangsan

Hash

Redis中的Hash类型可以看做是value同时具有key和value,所以适合同时存储多个键值到一条数据中。

例:hset student1 name "zhangsan" age 20

List

List是链表结构,每个key对应一个表,通过push和pop压入和弹出数据。

例:lpush student2 Lisi David

Set

每个key对应一set集合,通过add把元素添加到集合中,添加多个元素时,每个元素之间是无序的,通过pop弹出时将随机弹出一个元素。

例:sadd class1 zhangsan lisi wangwu zhaoliu

SortedSet

有序集合,需要将value和对应的score值同时插入,获取内容时会根据score进行排序。典型的排行榜。

例:zadd salary 100 Tom 500 David 300 Lucy

了解了数据库连接方式和基础数据结构,已经能够操作数据库进行常用的存储了。但是Redis的功能还远不止这些,参考文档中还详细讲述了Redis的其他性能如持久化、事务(不支持回滚)、事件订阅和发布、集群配置规范和方法等。有兴趣的同学可以看考一下文档。http://doc.redisfans.com/

hiredis安装和集成

安装好了Redis的服务端,就可以通过C语言连接数据库了。C连接框架是hiredis。

首先下载源码:https://github.com/redis/hiredis

下载完成后,执行 make && make install 对源码进行编译和复制。然后便可以在代码中包含redis库文件。

做一下连接测试:

#include <hiredis/hiredis.h>
#include <iostream>

using namespace std;

int main()
{
    redisContext* c = redisConnect("127.0.0.1", 6379);
    redisReply *reply;
    if ( c->err)
    {
        redisFree(c);
        cout << "Connect to redisServer fail" << endl;
        return 1;
    }
    cout << "Connect to redisServer Success" << endl;
    redisReply* r = (redisReply*)redisCommand(c, "set ccc ccc");
    cout << r->str) << endl;
    r = (redisReply*)redisCommand(c, "get ccc");
    cout << r->str << endl;
    return 0;
}

Makefile

TARGET=redisconn
LIB=-lhiredis
INCLUDE=
SRCFILE=$(wildcard *.cpp)
all:
        @g++ $(SRCFILE) $(INCLUDE) $(LIB) -o $(TARGET)
.PHONY:clean
clean:
        @rm -rf $(TARGET)

执行结果

pdx@ubuntu:~/code/redis$ ./redisconn 
Connect to redisServer Success
OK
ccc

hiredis的基本操作解析

hiredis的操作分为同步、异步。

同步API

redisContext *redisConnect(const char *ip, int port); //tcp连接
void *redisCommand(redisContext *c, const char *format, ...);
void freeReplyObject(void *reply);

第一个和第三个分别是建立连接和释放连接。

第二个为发送指令,第一个入参为连接时创建的指针,后面的参数是将输入组拼成命令行对应的字符串,看入参名就知道和printf的用法一样。使用时将其返回值强转为redisReply *。

typedef struct redisReply {
    int type; /* REDIS_REPLY_* */
    long long integer; /* The integer when type is REDIS_REPLY_INTEGER */
    size_t len; /* Length of string */
    char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */
    size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */
    struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */
} redisReply;

其字段含义在注释中说明的很清晰了。

下面看一下发送指令都干了些什么以及如何实现同步的。

redisvCommand(c,format,ap)
    -- redisvAppendCommand(c,format,ap)
        -- __redisAppendCommand(c,cmd,len)
    -- redisGetReply(c,&reply)

以上罗列出了调用关系,首先调用__redisAppendCommand组拼指令,然后redisGetReply接收回复。

之所以这么说是因为append仅仅实现了指令的存储,而发送和接收指令全部在redisGetReply函数中完成。

int redisGetReply(redisContext *c, void **reply) {
    int wdone = 0;
    void *aux = NULL;

    /* Try to read pending replies */
    if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
        return REDIS_ERR;

    /* For the blocking context, flush output buffer and read reply */
    if (aux == NULL && c->flags & REDIS_BLOCK) {
        /* Write until done */
        do {
            if (redisBufferWrite(c,&wdone) == REDIS_ERR)
                return REDIS_ERR;
        } while (!wdone);

        /* Read until there is a reply */
        do {
            if (redisBufferRead(c) == REDIS_ERR)
                return REDIS_ERR;
            if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
                return REDIS_ERR;
        } while (aux == NULL);
    }

    /* Set reply object */
    if (reply != NULL) *reply = aux;
    return REDIS_OK;
}

同时也看到,读过程会一直循环到读到数据为止,除非socket出错。

与此同时,也注意到,在建立连接时,connect通过fcntl设置了socket为BLOCK状态。

c->flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,NULL);
fcntl(c->fd, F_SETFL, flags) == -1

当然,设置socket为block不仅保证了read时为阻塞的,同时connect时也为阻塞。所以同步API对应的操作流程,从连接到获取应答都保持了一直的同步状态。其实对redis服务器的连接与操作就是建立tcp并读写的过程,从这一点去理解也比较容易。

异步API

edisAsyncContext *redisAsyncConnect(const char *ip, int port)
int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...)
void redisAsyncDisconnect(redisAsyncContext *ac)

 大体用法和同步API一致,重点关注一下两点:1、如何实现异步 2、回调处理

首先在创建连接时,socket被定义为非阻塞状态。

c->flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,&tv);

同时初始化异步连接上下文redisAsyncContext,它这是这么个东西

typedef struct redisAsyncContext {
    /* Hold the regular context, so it can be realloc'ed. */
    redisContext c;

    /* Setup error flags so they can be used directly. */
    int err;
    char *errstr;

    /* Not used by hiredis */
    void *data;

    /* Event library data and hooks */
    struct {
        void *data;

        /* Hooks that are called when the library expects to start
         * reading/writing. These functions should be idempotent. */
        void (*addRead)(void *privdata);
        void (*delRead)(void *privdata);
        void (*addWrite)(void *privdata);
        void (*delWrite)(void *privdata);
        void (*cleanup)(void *privdata);
    } ev;

内部维护了一个同步连接的上下文,和一些未实现的接口函数。

然后是redisAsyncCommand函数,也就是异步指令发送。他有三个主要参数和一套变参

redisAsyncContext *ac:连接时初始化的异步上下文

redisCallbackFn *fn:由于是异步,必须指定对应的回调

void *privdata:传给回调的私有数据

const char *format, ...:一套变参,与同步相同

 接下来看一下他是怎么工作的。进来之后,首先判断指令是否为以下三种

subscribe(订阅)

unsubscribe(取消订阅)

monitor(打印指令)

其他指令则首先执行

__redisPushCallback(&ac->replies,&cb)

将回调加入ac->replies的队尾,然后调用

_EL_ADD_WRITE(ac) 

宏展开是这么个东西 

if ((ac)->ev.addWrite) 
    (ac)->ev.addWrite((ac)->ev.data);

看到这里纠结了很久,因为addWrite是异步上下文内部接口,自创建连接开始一直没有赋值,也就是NULL。这样的话岂不是什么也不做?直到我看了example里的ae源码,才知道使用异步接口时首先要手动调用adapter里的redisAeAttach对接口进行赋值。姑且来看一下ae.h里对于addWrite函数的定义

static void redisAeAddWrite(void *privdata) {
    aeCreateFileEvent(loop,e->fd,AE_WRITABLE,redisAeWriteEvent,e);
}

static void redisAeWriteEvent(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisAsyncHandleWrite(e->context);
}

void redisAsyncHandleWrite(redisAsyncContext *ac) {
    if (redisBufferWrite(c,&done) == REDIS_ERR) {
        __redisAsyncDisconnect(ac);
    } else {
        /* Continue writing when not done, stop writing otherwise */
        if (!done)
            _EL_ADD_WRITE(ac);
        else
            _EL_DEL_WRITE(ac);

        /* Always schedule reads after writes */
        _EL_ADD_READ(ac);
    }
}

以上是去掉校验代码的主要调用关系。可以看到,addWrite最终是调用redisBufferWrite进行写操作,其内部就是给socket写数据。如果没写完,done标志位置0,然后再继续写。如果写完,则删除对应操作。每次写操作不管成功与否,都会执行_EL_ADD_READ。这个宏对应于_EL_ADD_WRITE,直接看内部实现。

static void redisAeAddRead(void *privdata) {
    aeCreateFileEvent(loop,e->fd,AE_READABLE,redisAeReadEvent,e);
}

static void redisAeReadEvent(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisAsyncHandleRead(e->context);
}

void redisAsyncHandleRead(redisAsyncContext *ac) {
    redisContext *c = &(ac->c);
    if (redisBufferRead(c) == REDIS_ERR) {
        __redisAsyncDisconnect(ac);
    } else {
        /* Always re-schedule reads */
        _EL_ADD_READ(ac);
        redisProcessCallbacks(ac);
    }
}

同样,代码只保留了主要的调用关系。最终调用的redisBufferRead从socket读取数据,如果读取成功,则执行callback。反复执行addRead的流程,由于是unblock,流程不会阻塞。所以究其根本,保证异步的方式还是设置socket为非阻塞的状态。

以上,梳理了hiredis对于数据库的C语言主要接口。轻量级的框架,一些实用方式还需要自己封装,如连接池。虽然感觉不如jedis好用,但是接下来的练手项目要用到C语言的接口,所以还是梳理并记录下载,以备后续查用。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值