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


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

关键字:数据库 Redis hiredis

安装Redis

下载源码:http://www.redis.cn/download.html <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
<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/ <http://doc.redisfans.com/>

hiredis安装和集成

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

首先下载源码:https://github.com/redis/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语言的接口,所以还是梳理并记录下载,以备后续查用。