转载请注明出处:https://blog.csdn.net/mymottoissh/article/details/83001716
前文书介绍过MongoDb的服务端和客户端的安装、配置以及项目中的集成过程。简单来说,MongoDb存储就是写json,而今天的redis是写键值对。本文主要介绍一下redis、安装过程以及C语言驱动的使用
关键字:数据库 Redis hiredis
下载源码: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服务器启动命令
服务端启动:
直接执行/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是使用键值对进行数据存储的。其中,键统一为字符串,值包含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/
安装好了Redis的服务端,就可以通过C语言连接数据库了。C连接框架是hiredis。
首先下载源码:https://github.com/redis/hiredis
下载完成后,执行 make && make install 对源码进行编译和复制。然后便可以在代码中包含redis库文件。
做一下连接测试:
#include
#include
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的操作分为同步、异步。
同步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语言的接口,所以还是梳理并记录下载,以备后续查用。