redis学习总结

Redis学习总结

redis是一个单线程基于事件机制的一个模型,使用事件处理框架 aeEvent

1.       启动过程

Reids的启动过程大致如下:

1.初始化全局struct server数据结构,给每个成员赋予默认值,并且创建命令表,用于查找相应命令对应的处理函数:initServerConfig()--->populateCommandTable
2.如果指定了配置文件,会读取redis.conf重新赋值给structserver实例:loadServerConfig()
3.初始化其它数据结构,创建db 对应dict数据结构等,bind,listen启动网络服务:initServer()
该操作同时创建aeEventLoop结构,aeCreateTimeEvent添加定时器事件(serverCron),第一个定时器事件在1ms后被调用,aeCreateFileEvent添加IO事件
这个事件监听的是服务器的listen套接字可读事件,相应的事件处理函数为acceptTcpHandler
4.加载数据库:appendfile 加载 OR rdbfile加载。这里是如果有aof的话先加载,rdb就不进行,否则再判断是否有rdb。即优先使用aof方式。
5.最后调用aeMain来真正的监听网络套接字,并进行事件响应

1.1   timeEven事件

在事件启动后的下一毫秒调用 serverCron 函数,实际这个serverCron在执行完后会被重新设置加入调度事件中,以后每隔100 毫秒执行一次。
响应处理函数为:serverCron,定时器事件处理流程(该内容参考自http://www.w3ccollege.org/redis/redis-internal/understanding-redis-internal-the-main-structure-and-start-the-process.html)
1. 打印非空db的一些信息,log功能
2. 如果当前没有后台运行dump数据保存到文件的进程,则可以根据内存使用情况重新调整hashTables大小
3. 日志输出一些client的连接信息
4. 检测idle的client 连接,关闭idle连接
5. 判断是否有正在进行dump数据到文件的后台进程
如果有判断是否已经结束,进行结束后的一些工作(这个跟replication有关,后续详细说明)没有则判断是否需要启动新的进程dump数据到文件
6. 处理expired的key
7. vmSwapOut功能,如果启动了vm选项,则判断是否超过vmMaxMemory,如果超过进行value的swap
8. 判断如果server是slave,则连接master,发送sync命令,和master同步数据 

1.2   fileEvent事件

FileEvent在这里有两种类型的fd,一个是listen时的fd,另一个是accept返回的fd。显然第一个fd是一直存在的,它的响应函数为acceptTcpHandler,这个处理函数会调用acceptCommonHandler,该函数为此客户createClient,并为accept返回的fd createFileEvent(fd),该类型event的响应函数为readQueryFromClient(觉得这函数名取得不怎么好,起码后面加个Handler);这样两种类型的fd事件都注册好,并且它们的响应函数分别为:acceptCommonHandler,readQueryFromClient。
注意到这里还没有真正的启动好服务,其底层调用的是epoll_ctl,即添加(注册)事件。而真正的wait则是在aeMain里,进入aeMain后,server才进入真正的服务阶段。首先遍历aeTimeEvent链表,接到最近的事件,如果该事件的响应时间小于当前(即已经过了这个时间但还没被处理),所以显然在调用aeApiPoll进行epoll_wait时timeout为0,即当没有fileEvent时直接返回,否则epoll_wait的timeout可设计为这个最近时间减去当前时间,即可以等待这么长的时间,这么长的时间如果还没有fileEvent的话就应该返回,因为此时timeEvent已经到点了。如果在这个时间内收到了fileEvent时候,则把这些可用的事件的fd及属性mask保存到aeFiredEvent数组中(server的epoll一开始是使用aeEventLoop->apidata(aeApiState)->events[AE_SETSIZE]来保存这些可用的event结构),从aeApiPoll返回后台是一个for循环对所有可用的event进行处理,即调用相应的事件处理函数aeFileEvent->rfileProc,如acceptCommonHandler,readQueryFromClient;aeFileEvent->wFileEvent,sendReplyToClient。最后再判断是否有timeEvent可以处理。(可以看图1,图2来理解整个过程)

以上就是redis的主处理流程。下面一张图是来自http://www.w3ccollege.org/redis/redis-internal/redis-redis-event-library-source-code-analysis.html
非常清楚的描述了redis的主框架及事件处理流程


图1 redis主框架及事件流程

2.       redis主要类型关系及查询过程

2.1   类型关系图


图2 redis主要类型关系

通过该图我们可以看到redis的主要三个构成部分:
第一部分查询存储数据时使用的内存数据结构(redisDb,dict,dictht,dictEntry,redisObject)及相应数据的存储方式(string,list,set,zset,hash;它们内部根据element个数,size(这两个值由配置文件指定)再决定是否使用相应的紧凑型存储(set,list,hash紧凑存储类型:ziplist,intset,zipmap));
第二部分就是事件的类型结构(aeEventLoop,aeFileEvent[...],aeTimeEvent[单向链表],aeFiredEvent[...](用来存放可用的fileEvent))。
第三部分就是redisClient用来保存client[双向链表]信息的:用户发来的命令,以及对应于这些命令对应的创建redisObject对象类型,命令处理函数等信息。

 

2.2   查找过程

通过这张图我们可以大概的知道redis查询更新的一个流程:通过FileEvent监听客户端的请求,当某个fd可用时,调用readQueryFromClient进行命令解析processInputBuffer(该函数内部根据客户端(telnet或redis-cli)发送的命令协议格式分别调用processInlineBuffer或processMultibulkBuffer这两个函数都是完成把保存在redisClient->querybuf解析为一个个的参数,并且构造为redisObject对象保存到redisClient->argv[c->argc]中),然后调用processCommand开始执行客户端发送过来的命令,第一个参数显示是客户端想要执行的操作,所以先执行lookupCommand进行命令查询(使用命令查找ht表readonlyCommandTable)获得相应的命令处理函数redisCommand.redisCommandProc,然后调用再call(c)进行真正的命令(增删查找等)操作,这里以get命令为例进行下面的讲解,通过查表获得getCommand函数,然后执行lookupKeyReadOrReply进行查询与返回操作,首先执行lookupKeyWrite(c->db,key),其最底层还是调用dictFind(db,key):

    if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */

    if (dictIsRehashing(d)) _dictRehashStep(d);

    h = dictHashKey(d, key); //进行真正的hash计算,hashFunction由dictht的dictType指定

    for (table = 0; table <= 1; table++) {

        idx = h & d->ht[table].sizemask;

        he = d->ht[table].table[idx];  //获得dictEntry*,所有hash

        while(he) {

            if (dictCompareHashKeys(d, key, he->key)) //对key值进行校验

                return he;

            he = he->next;

        }

        if (!dictIsRehashing(d)) return NULL;

    }

得到结果后调用addReply(c,robj)来进入写返回操作,它首先调用_installWriteEvent向epoll注册可写事件,相应的响应函数为sendReplyToClient,然后判断robj的encoding类型,进行getDecodedObject解码,把解码后的内容拷贝到redisClient->buf(_addReplyToBuffer),最后当刚才的事件发生时,由sendReplyToClient进行真正的write(cfd)操作。(真正的wait事件是在最外层的aeMain函数,所以这些内容会被正确拷贝到c->buf里)。
[注dict里有个dictht[2],表示两个hash表,这个是为了实现rehash时使用,当ht[0],链接太长,就应该进行rehash,ht[1].size=ht[0].size*2]这里只介绍简单的SDS查询的过程,该类型其实就是一个动态数组的类型,下面的连接介绍了redis各种内存存储的结构,非常的详细:
http://www.w3ccollege.org/redis/redis-internal/redis-memory-storage-structure-analysis-2.html

 

3.       持久化:

该章节内容转自

【http://www.w3ccollege.org/redis/redis-internal/redis-2-2-4-studies-4-persistence.html】

Redis的持久化目前有两种方式:snapshot与aof方式。

3.1   snapshot(快照)

进行快照的时机是由配置文件的save设置的,如save 900 1;900s有一个更新;或者由client发送save或者bgsave命令来进行快照。其中save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有client的请求,这种方式会阻塞所有client请求,所以不推荐使用。bgsave会执行后台dump(新建子进程执行dump)。整个过程如下:
当dump条件满足(或者接收到bgsave命令)redis调用系统fork,创建子进程
父进程继续处理client请求,子进程负责将内存内容写入到临时文件
由于Liunx的copy on write机制,子进程会在内存映射表中建立一个指针,指向父进程相同的内存地址,两个进程会共享相同的内存和物理文件,当父进程处理写请求时,os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数据是fork时刻整个内存的快照,当子进程将快照写入临时文件完毕后,用刚才的临时文件替换原来的快照文件,然后子进程退出。
可以看出这里每次都是把整个内存快照全写到文件中,而不是只写修改的内容。
主要的函数是:static int rdbSave(char *filename)
注:在shutdown server,及执行flushall命令时也要执行dump操作。 

3.2   AOF(Append-Only File)

通过write函数,将每次修改db数据的命令,追加到文件中,默认是appendonly.aof ,因此粒度是最小的了,跟日志方式有点类似。所以,可以简单理解为跟MySQL binlog一样的东西,作用就是记录每次的写操作,在遇到断电等问题时可以用它来恢复数据库状态。但,他不是bin的,而是text的,一行一行写得很规范,就是说我们也能人工通过它恢复数据当Redis重启时,会执行.aof 文件中保存的命令在内存中重建整个数据库的内容(注:因为启动时是先加载该文件的,并且它与dump.rdb两者只会使用其一,所以如果在启动之前创建一个空的.aof文件,那么启动后整个redis instance是空的,即使现在dump.rdb有数据)。但不管如何,总有丢失数据的可能,毕竟整个过程是异步的。为了降低风险,我们可以修改配置文件,选择调用fsync()的频率,来保证日志写入到磁盘的时机。
这种方式也有弊端,就是任何写入操作都会进行持久化,那么,AOF文件会越来越大,重启的时候数据初始化要很久,这时候我们可以通过执行bgrewriteaof[background rewrite append only file] 命令在后台重建该文件。
  收到此命令Redis将使用与快照类似的方式,将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。举例来说就是:之前可能用了10w次操作共改变了100条数据(比如在一条数据上进行了多次操作),那这时AOF中的10w条写操作记录就变成了100条记录。相当于将前面的执行全部合并了,原本很大AOF变小了。[相当于最后只记录所有key的一次写操作,数值就是它们当前最新的值]
  需要注意到是重写AOF文件的操作,并没有读取旧的AOF文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的AOF文件,这点和快照有点类似,(fork子进程,copy-on-write)。过程如下:
  redis调用fork ,现在有父子两个进程
  子进程根据内存中的数据库快照,将所有数据生成一条写入日志到临时文件
  父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题
  当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件
现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

 

4.       事务

本章主要参考
【http://www.w3ccollege.org/redis/redis-notes/redis-study-notes-of-the-matters.html】
redis的事务较简单,并不具备传统事务的acid的全部特征。主要原因之一是redis事务中的命令并不是立即执行的,会一直排队到发布exec命令才执行所有的命令。即redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令。另外面它不支持回滚,事务中的命令可以部分成功,部分失败,命令失败时跟不在事务上下文执行时返回的信息类似。其表现方式是使用multi,exec命令对实现事务:当一个client在一个连接中发出multi命令后,这个连接会进入一个事务上下文,该连接后续的命令并不是立即执行,而是先放到一个队列中,直到此连接受到exec命令后,redis才会顺序的执行队列中的所有命令,并将所有命令的运行结果打包一起返回给client,然后此连接就结束事务上下文。[在redis2.1中加了watch命令来实现乐观锁,实现对共享资源的同步,如果watch一个key之后,该key如果在执行连接之外进行了任何更改,都会导致该连接的事务失败]

 

5.       主从复制

本章节转自
【http://www.w3ccollege.org/redis/redis-notes/redis-study-notes-of-the-master-slave-replication.html】
当设置好slave服务器后,slave会建立和master的连接,然后发送sync命令。无论是第一次同步建立的连接还是连接断开后的重新连接,master都会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存起来。后台进程完成写文件后,master就发送文件给slave,slave将文件保存到磁盘上,然后加载到内存恢复数据库快照到slave上。接着master就会把缓存的命令转发给slave。而且后续master收到的写命令都会通过开始建立的连接发送给slave。从master到slave的同步数据的命令和从client发送的命令使用相同的协议格式。当master和slave的连接断开时slave可以自动重新建立连接。如果master同时收到多个 slave发来的同步连接命令,只会使用启动一个进程来写数据库镜像,然后发送给所有slave。
配置slave服务器很简单,只需要在配置文件中加入如下配置:slaveof 192.168.1.1 6379 #指定master的ip和端口

 

后话

上面的内容,很多都是总结自网上的资料,有的忘了是来自哪里的,所以没有标注。自己也是在看了他们的文章后,再看了server 启动的代码(第一章节)及event库及查询执行过程的代码(第二章节),其它的章节由于时间关系就没去看code,直接转载别人的。写这篇文章更多的是让自己以后需要的时候可以快速查阅,也是对这几天来学习的总结,总算有个实质性的输出。感谢互联网以及那些把自己的知识分享出来的同学们。
[注:redis的代码比较少,并且主体架构也比较清析,所以代码读起来比较容易,自己是先从redis-benchmark.c读起的,因为它的代码更少,但是它包含了redis的事件驱动机制的实现,所以对于后面读取redis-server还是有帮助的。下面的三个参考连接基本上有了redis的所有主题,讲的也非常的详细和清晰,强烈推荐关注redis的同学关注]

 

参考资料

http://www.w3ccollege.org/category/redis

http://www.hoterran.info/

http://www.petermao.com/

 

你可能感兴趣的:(NoSQL学习)