Redis源碼簡要分析


轉載自:http://blog.chinaunix.net/uid-790245-id-3766842.html

在文章的開頭我們把所有服務端文件列出來,並且標示出其作用:

  • adlist.c //雙向鏈表
  • ae.c //事件驅動
    ae_epoll.c //epoll
    接口, linux
    ae_kqueue.c //kqueue
    接口, freebsd
    ae_select.c //select
    接口, windows
  • anet.c //網絡處理
  • aof.c //處理AOF文件
  • bio.c //后台線程的業務邏輯
  • config.c //配置文件解析
  • db.c //DB處理
  • dict.c //hash
  • intset.c //轉換為數字類型數據
  • multi.c //事務,多條命令一起打包處理
  • networking.c //讀取、解析和處理客戶端命令
  • object.c //各種對像的創建與銷毀,stringlistsetzsethash
  • rdb.c //redis數據文件處理
  • redis.c //程序主要文件
  • replication.c //數據同步master-slave
  • sds.c //字符串處理
  • sort.c //用於listsetzset排序
  • t_hash.c //hash類型處理
    t_list.c //list
    類型處理
    t_set.c //set
    類型處理
    t_string.c //string
    類型處理
    t_zset.c //zset
    類型處理
  • ziplist.c //節省內存方式的list處理
  • zipmap.c //節省內存方式的hash處理
  • zmalloc.c //內存管理

 

上面基本是redis最主要的處理文件,部分沒有列出來,如VM之類的,就不在這里講了。

首先我們來回顧一下redis的一些基本知識:

  1. redisNDB(默認為16DB),並且每個db有一個hash表負責存放key,同一個DB不能有相同的KEY,但是不同的DB可以相同的KEY;
  2. 支持的幾種數據類型:stringhashlistsetzset;
  3. redis可以使用aof來保存寫操作日志(也可以使用快照方式保存數據文件)

 

 

對於數據類型在這里簡單的介紹一下(網上有圖,下面我貼上圖片可能更容易理解)
1
、對於一個string對像,直接存儲內容;
2
、對於一個hash對像,當成員數量少於512的時候使用zipmap(一種很省內存的方式實現hash table),反之使用hash(key存儲成員名,value存儲成員數據);
3
、對於一個list對像,當成員數量少於512的時候使用ziplist(一種很省內存的方式實現list),反之使用雙向鏈表(list);
4
、對於一個set對像,使用hash(key存儲數據,內容為空)
5
、對於一個zset對像,使用跳表(skip list),關於跳表的相關內容可以查看本blog的跳表學習筆記;

下面正式進入源代碼的分析
1
、首先是初始化配置,initServerConfig(redis.c:759)

void initServerConfig() {
server.port = REDIS_SERVERPORT;
server.bindaddr = NULL;
server.unixsocket = NULL;
server.ipfd = -1;
server.sofd = -1;

2.在初始化配置中調用了populateCommandTable(redis.c:925)函數,該函數的目地是將命令集分布到一個hash table,大家可以看到每一個命令都對應一個處理函數,因為redis支持的命令集還是蠻多,所以如果要靠if分支來做命令處理的話即繁瑣效率還底, 因此放到hash table中,在理想的情況下只需一次就能定位命令的處理函數。

void populateCommandTable(void) {
    int j;
    int numcommands = sizeof(readonlyCommandTable)/sizeof(struct redisCommand);

    for (j = 0; j < numcommands; j++) {
        struct redisCommand *c = readonlyCommandTable+j;
        int retval;

        retval = dictAdd(server.commands, sdsnew(c->name), c);
        assert(retval == DICT_OK);
    }
}

3、對參數的解析,redis-server有一個參數(可以不需要),這個參數是指定配置文件路徑,然后由函數loadServerConfig(config.c:28)加載所有配置

if (argc == 2) {
    if (strcmp(argv[1], “-v”) == 0 || strcmp(argv[1], “–version”) == 0)
version();
if (strcmp(argv[1], “–help”) == 0) usage(); resetServerSaveParams(); loadServerConfig(argv[1]);

4、初始化服務器initServer(redis.c:836), 該函數初始化一些服務器信息,包括創建事件處理對像、dbsocket、客戶端鏈表、公共字符串等。

void initServer() {
    int j;

    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    setupSignalHandlers();//設置信號處理

    if (server.syslog_enabled) {
        openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,server.syslog_facility);
    }

5、在上面初始化服務器中有一段代碼是創建事件驅動,aeCreateTimeEvent是創建一個定時器,下面創建的定時器將會每毫秒調用 serverCron函數,而aeCreateFileEvent是創建網絡事件驅動,當server.ipfdserver.sofd有數據可讀的情況將會分別調用函數acceptTcpHandleracceptUnixHandler

aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,acceptTcpHandler,NULL) == AE_ERR)
oom(“creating file event”); if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,acceptUnixHandler,NULL) == AE_ERR)
oom(“creating file event”);

6、接下來就是初始化數據,如果開啟了AOF,那么會調用loadAppendOnlyFile(aof.c:216)去加載AOF文件,在AOF 文件中存放了客戶端的命令,函數將數據讀取出來然后依次去調用命令集去處理,當AOF文件很大的時候勢必為影響客戶端的請求,所以每處理1000條命令就會去嘗試接受和處理客戶端的請求,其代碼在aof.c250但是如果沒有開啟AOF並且有rdb的情況,會調用rdbLoad(redis.c:873)嘗試去加載rdb文件,理所當然的在加載rdb文件的內部也會考慮文件太大而影響客戶端請求,所以跟AOF一樣,每處理1000條也會嘗試去接受和處理客戶端請求。

7、當所有初始化工作做完之后,服務端就開始正式工作了

aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);

8、大家都知道redis單線程模式,所有的請求、處理都是在同一個線程里面進行,也就是一個無限循環,在這個無限循環的內部有兩件事要做,第一件就是調用通過aeSetBeforeSleepProc函數設置的回調函數,第二件就是開始接受客戶端的請求和處理,所以我們可以在第7節看到設置了回調函數為beforeSleep,但是這個beforeSleep到底有什么作用呢?我們在第9節再詳細講述。對於aeMain(ae.c:375)就是 整個程序的主要循環。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

9、在beforeSleep內部一共有三部分,第一部分對vm進行處理(即第一個if),這里我們略過;第二部分是釋放客戶端的阻塞操作,在 redis里有兩個命令BLPOPBRPOP需要使用這些操作(彈出列表頭或者尾,實現方式見t_list.c:862行的 blockingPopGenericCommand函數),當指定的key不存在或者列表為空的情況下,那么客戶端會一直阻塞,直到列表有數據時,服務 端就會去執行lpop或者rpop並返回給客戶端,那么什么時候需要到BLPOPBRPOP呢?大家平時肯定用redis做過隊列,最常見的處理方式 就是使用llen去判斷隊列有沒有數據,如果有數據就去取N條,然后處理,如果沒有就sleep(3),然后繼續循環,其實這里就可以使用BLPOP或者 BRPOP來輕松實現,而且可以減少請求,具體怎么實現留給大家思考;第三部分就是flushAppendOnlyFile(aof.c:60),這個函 數主要目的是將aofbuf的數據寫到文件,那aofbuf是什么呢?他是AOF的一個緩沖區,所以客戶端的命令都會在處理完后把這些命令追加到這個緩沖 區中,然后待一輪數據處理完之后統一寫到文件(所以aof也是不能100%保證數據不丟失的,因為如果當redis正在處理這些命令的情況下服務就掛掉, 那么這部分的數據是沒有保存到硬盤的),大家都知道寫數據到文件並不是立即寫到硬盤,只是保存到一個文件緩沖區中,什么情況下會把緩沖區的數據轉到硬盤 呢?只要滿足如下三種條件的一種就能將數據真正存到硬盤:

  1. 手動調用刷新緩沖區;
  2. 緩沖區已滿;
  3. 程序正常退出。

因此redis將數據寫到文件緩沖區之后會判斷是否需要刷到硬盤,server.appendfsync有兩種方式,第一種(APPENDFSYNC_ALWAYS):無條件刷新,即每次寫文件都會保存到硬盤,第二種(APPENDFSYNC_EVERYSEC):每隔一秒保存到硬盤。

10、接下來我們開始講解aeProcessEvents(ae.c:275)的處理流程,首先我們來回顧一下第5節設置的定時器和監聽socket事件處理,其中socket事件處理會回調acceptTcpHandler(networking.c:410)和定時器回調函數 serverCron(redis.c:519),在aeProcessEvents的內部有兩部分需要處理,第一部分是調用aeApiPoll判斷 socket是否有數據可讀,整個服務端的socket里面要分監聽socket和客戶端socket,當有客戶端鏈接服務器時,會觸發監聽socket 的事件處理函數,也就是acceptTcpHandler,而acceptTcpHandler會去調用 createClient(networking.c:13)創建客戶端對像,然后為這個客戶端設置事件處理函數 readQueryFromClient(networking.c:827),所以當客戶端有消息時就會觸發客戶端socket 事件處理函數,處理數據部分講在后面詳細講解,接下來的第二部分就是定時器,每次在socket部分處理完后就用調用processTimeEvents(ae.c:212)來處理定時器,那么內部實現也很簡單,當設置定時器的時候就會計算好應該觸發的時間,所以這里就 只需要判斷當前時間是否大於或者等於應該觸發的時間即可。那么這個定時器到底做了什么呢?請繼續第11節。

11、我們繼續跟蹤源代碼serverCron(redis.c:519),整個函數分為七個部分,

  • 第一部分:在服務端打印一些關於DB的信息(包括key數量,內存使用量等)
  • 第二部分:判斷DBhash table是否需要擴展大小tryResizeHashTables(redis.c:432)
  • 第三部分:關閉太長時間沒有通信的鏈接 closeTimedoutClients(networking.c:629)
  • 第四部分:保存rdb文件 rdbSaveBackground(rdb.c:507),當然也是在需要保存的情況才會保存,即設置save參數;
  • 第五部分:清除過期的key,當然 這里不是清除全部,他只是隨機取出一些activeExpireCycle(redic.c:477)
  • 第六部分:虛擬內存交換部分,將一部分key轉到 虛擬內存中,這里的key也是隨機抽取的, vmSwapOneObjectBlocking(vm.c:521)
  • 第七部分:主從同 步,replicationCron(replication.c:500)

12、在第10節中我們講到客戶端socket處理函數readQueryFromClient,這里我們一層層分析,首先是從客戶端讀取數據,然后調用processInputBuffer,在內部先是判斷類型,然后調用processInlineBuffer或者 processMultibulkBuffer解析參數,解析后的參數由argv存儲參數,其類型是一個指向指針的指針,其中argv[0]是命令名稱, 后面就是命令參數,argc存儲參數數量;然后調用processCommand(redis.c:979)處理命令,在內部調用 lookupCommand(redis.c:940)獲取命令對應的函數,然后調用freeMemoryIfNeeded(redis.c:1385) 判斷是否需要釋放一些內存,接下來就是調用call(redis.c:954)去執行命令,執行命令后會調用 feedAppendOnlyFile(aof.c:137)把命令行保存到aofbuf中,然后判斷是否需要同步數據到slave,如果需要則調用 replicationFeedSlaves(replication.c:10),接下來就是判斷是否需要將數據發送到監控端,如果需要則調用 replicationFeedMonitors(replication.c:82),到這里整個服務流程就結束了。至於每條命令如何執行,大家可以去 查看以t_開頭的幾個文件。下面是一張整個服務的流程圖。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM