Redis 英文拼写:REmote DIctionary Server(Redis)
Redis 是一个由Salvatore Sanfilippo 写的高性能key-value 存储系统。
远程,指的是有网络api接口,也就是提供6379端口tcp server给客户端使用。
字典服务,指的是数据类型为key:value类型,与mysql的关系型数据库有所区别。
特点:比较耗内存,速度比较快。
去除冗余信息,选择一个比较早期的源码进行分析,可以更清楚的看出一个源码的设计模型。
从main()取分析,redis主要做了以下几件事情:
redis原理:在内存层面用类型、key_len、ken、value_len、value存储数据,在文件中一行一条数据(kv关系)。
设置默认参数
创建对象、持久化定时器回调
加载文件参数
加载文件数据写入字典
创建client connect事件handler(accept后,写入读写事件select(),最新的版本用epoll)
调用事件多路复用循环
退出
int main(int argc, char * *argv)
{
initServerConfig(); // 设置默认参数
initServer(); // 设置系统信号、字典、对象链表、公共对象、事件(文件,定时器)、6379服务器、持久化回调定时器
if (argc == 2) { // 使用配置文件替代部分默认参数
ResetServerSaveParams();
loadServerConfig(argv[1]);
redisLog(REDIS_NOTICE, "Configuration loaded");
}
else if (argc > 2) {
fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
exit(1);
}
redisLog(REDIS_NOTICE, "Server started");
if (loadDb("dump.rdb") == REDIS_OK) // 加载数据到字典
redisLog(REDIS_NOTICE, "DB loaded from disk");
if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
acceptHandler, NULL, NULL) == AE_ERR)
oom("creating file event"); // 注册6379客户端连接事件处理
redisLog(REDIS_NOTICE, "The server is now ready to accept connections");
aeMain(server.el); // 定时/网络读写事件多路复用监听
aeDeleteEventLoop(server.el); // 销毁事件循环,退出redis进程
return 0;
}
主要组件:
事件模型:
// ae.h
/* Types and data structures */
typedef void aeFileProc(struct aeEventLoop * eventLoop, int fd, void * clientData, int mask);
typedef int aeTimeProc(struct aeEventLoop * eventLoop, long long id, void * clientData);
typedef void aeEventFinalizerProc(struct aeEventLoop * eventLoop, void * clientData);
/* File event structure */
typedef struct aeFileEvent {
int fd; // 目标文件文件描述符,也就是redis client的fd
int mask; /* one of AE_(READABLE|WRITABLE|EXCEPTION) */
aeFileProc * fileProc; /* 客户端网络事件发生后,回调函数 */
aeEventFinalizerProc * finalizerProc; /* 清理函数 */
void * clientData; /* 回调参数 */
struct aeFileEvent * next; // 单链环
} aeFileEvent;
/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* 本定时器对象的id */
long when_sec; /* 定时秒 */
long when_ms; /* 定时毫秒 */
aeTimeProc * timeProc; // 定时回掉函数
aeEventFinalizerProc * finalizerProc; // 定时器销毁时,调用的清理函数
void * clientData; // 定时器回调函数参数
struct aeTimeEvent * next; // 单链表环
} aeTimeEvent;
/* State of an event based program */
typedef struct aeEventLoop {
long long timeEventNextId; // 定时器事件id,自增,用于给新创建定时器编号
aeFileEvent * fileEventHead; // 客户都fd读写异常事件的链表头
aeTimeEvent * timeEventHead; // 定时器事件的链表头
int stop; // 事件循环while退出条件
} aeEventLoop;
// 事件类型:读、写、异常。
#define AE_READABLE 1
#define AE_WRITABLE 2
#define AE_EXCEPTION 4
// 监控的事件源的类型:网络、定时器、全部、非阻塞标识(在select()时,设置timeout为0)
#define AE_FILE_EVENTS 1
#define AE_TIME_EVENTS 2
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4
事件dispatch()循环
/* 调用后,遍历eventLoop下面挂载的网络事件链表、定时器链表。使用定时器链表中最近的超市时间,作为select()的最后一个参数,去监听全部的客户端fd,
处理客户端事件后,查询定时器事件并处理。
继续while (!eventLoop->stop)循环,直到stop==true退出循环
*/
void aeMain(aeEventLoop * eventLoop)
{
eventLoop->stop = 0;
while (!eventLoop->stop)
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
int aeProcessEvents(aeEventLoop * eventLoop, int flags)
{
int maxfd = 0, numfd = 0, processed = 0;
fd_set rfds, wfds, efds;
aeFileEvent * fe = eventLoop->fileEventHead; // fe获得客户端事件链表头
aeTimeEvent * te; // 指向定时器事件
long long maxId;
AE_NOTUSED(flags);
/* Nothing to do? return ASAP */
if (! (flags & AE_TIME_EVENTS) && ! (flags & AE_FILE_EVENTS))
return 0;
// 清零载体。准备注册事件
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_ZERO(&efds);
/* Check file events */
/* 注册客户端fd */
if (flags & AE_FILE_EVENTS) {
while (fe != NULL) {
if (fe->mask & AE_READABLE)
FD_SET(fe->fd, &rfds);
if (fe->mask & AE_WRITABLE)
FD_SET(fe->fd, &wfds);
if (fe->mask & AE_EXCEPTION)
FD_SET(fe->fd, &efds);
if (maxfd < fe->fd)
maxfd = fe->fd;
numfd++;
fe = fe->next;
}
}
/* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
if (numfd || ((flags & AE_TIME_EVENTS) && ! (flags & AE_DONT_WAIT))) {
int retval;
aeTimeEvent * shortest = NULL;
struct timeval tv, *tvp;
// 获取最近一次的定时超时时间点,在早期redis版本中,用的还是比较笨的遍历整条链表的方法,在新的redis中,是用哈希表,或最小根来排序,不需要遍历
if (flags & AE_TIME_EVENTS && ! (flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
// 计算select()的超时时间参数
if (shortest) {
long now_sec, now_ms;
/* Calculate the time missing for the nearest
* timer to fire. */
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms + 1000) -now_ms) * 1000;
tvp->tv_sec--;
}
else {
tvp->tv_usec = (shortest->when_ms - now_ms) * 1000;
}
}
else {
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to se the timeout
* to zero */
// timeout为0,如果无时间则直接返回
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
}
// timeout为NULL,一直等待直到有事件发生
else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
// 监控客户端fd事件,超时事件使用上面计算出来的参数
retval = select(maxfd + 1, &rfds, &wfds, &efds, tvp);
// 如果检测到事件发生,则调用客户端fd处理回调函数
if (retval > 0) {
fe = eventLoop->fileEventHead;
while (fe != NULL) {
int fd = (int)
fe->fd;
if ((fe->mask & AE_READABLE && FD_ISSET(fd, &rfds)) || (fe->mask & AE_WRITABLE &&
FD_ISSET(fd, &wfds)) || (fe->mask & AE_EXCEPTION && FD_ISSET(fd, &efds))) {
int mask = 0;
if (fe->mask & AE_READABLE && FD_ISSET(fd, &rfds))
mask |= AE_READABLE;
if (fe->mask & AE_WRITABLE && FD_ISSET(fd, &wfds))
mask |= AE_WRITABLE;
if (fe->mask & AE_EXCEPTION && FD_ISSET(fd, &efds))
mask |= AE_EXCEPTION;
fe->fileProc(eventLoop, fe->fd, fe->clientData, mask);
processed++;
/* After an event is processed our file event list
* may no longer be the same, so what we do
* is to clear the bit for this file descriptor and
* restart again from the head. */
fe = eventLoop->fileEventHead;
FD_CLR(fd, &rfds);
FD_CLR(fd, &wfds);
FD_CLR(fd, &efds);
}
else {
fe = fe->next;
}
}
}
}
/* Check time events */
/* 遍历定时器链表,发现超时时间比current实际小的,则调用定时器回调函数 */
if (flags & AE_TIME_EVENTS) {
te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId - 1;
while (te) {
long now_sec, now_ms;
long long id;
if (te->id > maxId) {
te = te->next;
continue;
}
aeGetTime(&now_sec, &now_ms);
if (now_sec > te->when_sec || (now_sec == te->when_sec && now_ms >= te->when_ms)) {
int retval;
id = te->id;
retval = te->timeProc(eventLoop, id, te->clientData); // 定时器回调
/* After an event is processed our time event list may
* no longer be the same, so we restart from head.
* Still we make sure to don't process events registered
* by event handlers itself in order to don't loop forever.
* To do so we saved the max ID we want to handle. */
if (retval != AE_NOMORE) {
aeAddMillisecondsToNow(retval, &te->when_sec, &te->when_ms);
}
else {
aeDeleteTimeEvent(eventLoop, id);
}
te = eventLoop->timeEventHead;
}
else {
te = te->next;
}
}
}
return processed; /* return the number of processed file/time events */
}
链表:
//链表节点
typedef struct listNode {
struct listNode * prev; // 链表双环
struct listNode * next;
void * value; // 注意,但凡是void *的数据类型,必然是指向结构体地址
} listNode;
typedef struct list {
listNode * head; // 指向链表头
listNode * tail; // 指向链表尾
void * (*dup) (void * ptr); // 注意,dup复制字符串。返回值是void *。那么自然的就要联想到返回的是void * value,因为类型是一致的
void (*free) (void * ptr); // 释放链表节点的void * value
int (*match) (void * ptr, void * key); // 比较value,用于逻辑排序。程序中,比较字符串有两个目的:一个是排序(数学关系的大小),另外一个是逻辑排序(强行虚拟一个顺序出来,使对象在创建和销毁期间,具备一定的顺序性,方便调用)
int len; // 链表的节点数量
} list;
// 链表迭代器(统一定义封装类型)。
typedef struct listIter {
listNode * next;
listNode * prev;
int direction;
} listIter;
字典:
// 词汇
typedef struct dictEntry {
void * key;
void * val;
struct dictEntry * next;
} dictEntry;
// 字典目录
typedef struct dictType {
unsigned int(*hashFunction) (const void * key);
void * (*keyDup) (void * privdata, const void * key);
void * (*valDup) (void * privdata, const void * obj);
int(*keyCompare) (void * privdata, const void * key1, const void * key2);
void(*keyDestructor) (void * privdata, void * key);
void(*valDestructor) (void * privdata, void * obj);
} dictType;
// 字典
typedef struct dict {
dictEntry * * table; // 字典指针数组,每个数组是一条链表(一页词汇)。
dictType * type; // 字典目录,用于维护字典数据
unsigned int size; // 字典中的词汇量
unsigned int sizemask;
unsigned int used;
void * privdata; // void *类型的私有数据,一般指向的是结构体或函数指针,此处应该是结构体
} dict;
// 字典迭代器
typedef struct dictIterator {
dict * ht;
int index;
dictEntry * entry, *nextEntry;
} dictIterator;
static void initServerConfig()
{
server.dbnum = REDIS_DEFAULT_DBNUM; // 默认16本字典,argv[x]参数可以指定其它值
server.port = REDIS_SERVERPORT; // 6379服务端口,用于对外(client)提供api接口
server.verbosity = REDIS_DEBUG; // 是否打印调试信息
server.maxidletime = REDIS_MAXIDLETIME; // 默认客户端timeout,此处是300s
server.saveparams = NULL;
server.logfile = NULL; /* NULL = log on standard output */
ResetServerSaveParams();
// 持久化策略
appendServerSaveParams(60 * 60, 1); /* 1条数据变化,超过1小时必须写入磁盘 */
appendServerSaveParams(300, 100); /* 100条数据变化,超过5分钟必须写入磁盘*/
appendServerSaveParams(60, 10000); /* 1万条数据变化,超过1分钟必须写入磁盘 */
}
数据持久化有两个途径:
第一serverCron()->saveDbBackground(“dump.rdb”)->saveDb(filename。
第二,bgsaveCommand()->saveDb(filename),也就是命令要求立即刷新。
// redis server在6379端口监听客户端连接,此函数就是socket的accept(),成功后加入事件链表,事件链表被多路复用监听,收到cli命令后,解析并处理(kv操作),返回状态到终端。
static void acceptHandler(aeEventLoop * el, int fd, void * privdata, int mask)
{
int cport, cfd;
char cip[128];
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
REDIS_NOTUSED(privdata);
cfd = anetAccept(server.neterr, fd, cip, &cport);
if (cfd == AE_ERR) {
redisLog(REDIS_DEBUG, "Accepting client connection: %s", server.neterr);
return;
}
redisLog(REDIS_DEBUG, "Accepted %s:%d", cip, cport);
// 客户端fd加入事件监听链表
if (createClient(cfd) == REDIS_ERR) {
redisLog(REDIS_WARNING, "Error allocating resoures for the client");
close(cfd); /* May be already closed, just ingore errors */
return;
}
}
经过上述源码分析,我们发现,所谓redis主要干了以下几件事情:
设置默认参数,argv[x]覆盖部分默认参数
从磁盘加载k-v
在6379端口创建server
收到client对6379的connect()请求后accept(),并交给select()多路复用,监听事件发生后,解析,执行,打印结果。默认300秒超时断开。
将内存中的数据,刷入磁盘,格式为:type key_len key value_len value,一行一条。
支持的数据类型有:
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
redis基本的模型如上分析完毕。
对于比较新的redis,当然不会这么草草了事(新版本的redis支持更多的数据类型,不同数据类型存放在不同的文件中(分类存放))。一个可用的高性能组件,在容错机制上面,性能方面都会比最初模型要强打许多。
最新的redis,应该在排序方面使用了哈希表、最小根;在事件方面,使用epoll、kqueue;在功能方面支持更多的数据类型,如数组、哈希表等;还有其它一些新功能,如支持lLua脚本(这里解释以下LUA。Lua是应用层的shell , shell是内核层的。Lua底层是c api,中间层是一个高级语言(如python、golang这种语法的语言)的解释器,上层是golang风格的c代码)。
有兴趣的可下载最新源码,进行深入研究。