1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Redis多線程原理詳解

        共 5433字,需瀏覽 11分鐘

         ·

        2020-12-04 15:49

        點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”

        優(yōu)質(zhì)文章,第一時(shí)間送達(dá)

        ? 作者?|??啊漢

        來源 |? urlify.cn/J7BzYn

        66套java從入門到精通實(shí)戰(zhàn)課程分享

        本篇文章為你解答以下問題:

        • 0:redis單線程的實(shí)現(xiàn)流程是怎樣的?

        • 1:redis哪些地方用到了多線程,哪些地方是單線程?

        • 2:redis多線程是怎么實(shí)現(xiàn)的?

        • 3:redis多線程是怎么做到無鎖的?

        ?

        0:redis單線程的實(shí)現(xiàn)流程是怎樣的?

        Redis一開始是單線程模型,在一個(gè)線程中要同時(shí)處理兩種事件:文件事件和時(shí)間事件

        文件事件主要是網(wǎng)絡(luò)I/O的讀寫,請(qǐng)求的接收和回復(fù)

        時(shí)間事件就是單次/多次執(zhí)行的定時(shí)器,如主從復(fù)制、定時(shí)刪除過期數(shù)據(jù)、字典rehash等

        redis所有核心功能都是跑在主線程中的,像aof文件落盤操作是在子線程中執(zhí)行的,那么在高并發(fā)情況下它是怎么做到高性能的呢?

        由于這兩種事件在同一個(gè)線程中執(zhí)行,就會(huì)出現(xiàn)互相影響的問題,如時(shí)間事件到了還在等待/執(zhí)行文件事件,或者文件事件已經(jīng)就緒卻在執(zhí)行時(shí)間事件,這就是單線程的缺點(diǎn),所以在實(shí)現(xiàn)上要將這些影響降到最低。那么redis是怎么實(shí)現(xiàn)的呢?

        ?

        定時(shí)執(zhí)行的時(shí)間事件保存在一個(gè)鏈表中,由于鏈表中任務(wù)沒有按照?qǐng)?zhí)行時(shí)間排序,所以每次需要掃描單鏈表,找到最近需要執(zhí)行的任務(wù),時(shí)間復(fù)雜度是O(N),redis敢這么實(shí)現(xiàn)就是因?yàn)檫@個(gè)鏈表很短,大部分定時(shí)任務(wù)都是在serverCron方法中被調(diào)用。從現(xiàn)在開始到最近需要執(zhí)行的任務(wù)的開始時(shí)間,時(shí)長(zhǎng)定位T,這段時(shí)間就是屬于文件事件的處理時(shí)間,以epoll為例,執(zhí)行epoll_wait最多等待的時(shí)長(zhǎng)為T,如果有就緒任務(wù)epoll會(huì)返回所有就緒的網(wǎng)絡(luò)任務(wù),存在一個(gè)數(shù)組中,這時(shí)我們知道了所有就緒的socket和對(duì)應(yīng)的事件(讀、寫、錯(cuò)誤、掛斷),然后就可以接收數(shù)據(jù),解析,執(zhí)行對(duì)應(yīng)的命令函數(shù)。

        如果最近要執(zhí)行的定時(shí)任務(wù)時(shí)間已經(jīng)過了,那么epoll就不會(huì)阻塞,直接返回已經(jīng)就緒的網(wǎng)絡(luò)事件,即不等待。

        總之單線程,定時(shí)事件和網(wǎng)絡(luò)事件還是會(huì)互相影響的,正在處理定時(shí)事件網(wǎng)絡(luò)任務(wù)來了,正在處理網(wǎng)絡(luò)事件定時(shí)任務(wù)的時(shí)間到了。所以redis必須保證每個(gè)任務(wù)的處理時(shí)間不能太長(zhǎng)。

        ?

        redis處理流程如下:

        1:服務(wù)啟動(dòng),開始網(wǎng)絡(luò)端口監(jiān)聽,等待客戶端請(qǐng)求

        2:客戶端想服務(wù)端發(fā)起連接請(qǐng)求,創(chuàng)建客戶端連接對(duì)象,完成連接

        3:將socket信息注冊(cè)到epoll,設(shè)置超時(shí)時(shí)間為時(shí)間事件的周期時(shí)長(zhǎng),等待客戶端發(fā)起請(qǐng)求

        4:客戶端發(fā)起操作數(shù)據(jù)庫請(qǐng)求(如GET)

        5:epoll收到客戶端的請(qǐng)求,可能多個(gè),按照順序處理請(qǐng)求

        6:接收請(qǐng)求參數(shù),接收完成后解析請(qǐng)求協(xié)議,得到請(qǐng)求命令

        7:執(zhí)行請(qǐng)求命令,即操作redis數(shù)據(jù)庫

        8:將結(jié)果返回給客戶端

        ?

        1:redis哪些地方用到了多線程,哪些地方是單線程?

        Redis多線程和單線程模型對(duì)比如下圖:

        ?

        從上圖中可以看出只有以下3個(gè)地方用的是多線程,其他地方都是單線程:

        1:接收請(qǐng)求參數(shù)

        2:解析請(qǐng)求參數(shù)

        3:請(qǐng)求響應(yīng),即將結(jié)果返回給client

        很明顯以上3點(diǎn)各個(gè)請(qǐng)求都是互相獨(dú)立互不影響的,很適合用多線程,特別是請(qǐng)求體/響應(yīng)體很大的時(shí)候,更能體現(xiàn)多線程的威力。而操作數(shù)據(jù)庫是請(qǐng)求之間共享的,如果使用多線程的話適合讀寫鎖。而操作數(shù)據(jù)庫本身是很快的(就是對(duì)map的增刪改查),單線程不一定就比多線程慢,當(dāng)然也有可能是作者偷懶,懶得實(shí)現(xiàn)罷了,但這次的多線程模型還是值得我們學(xué)習(xí)一下的。

        ?

        2:redis多線程是怎么實(shí)現(xiàn)的?

        先大致說一下多線程的流程:

        1:服務(wù)器啟動(dòng)時(shí)啟動(dòng)一定數(shù)量線程,服務(wù)啟動(dòng)的時(shí)候可以指定線程數(shù),每個(gè)線程對(duì)應(yīng)一個(gè)隊(duì)列(list *io_threads_list[128]),最多128個(gè)線程。

        2:服務(wù)器收到的每個(gè)請(qǐng)求都會(huì)放入全局讀隊(duì)列clients_pending_read,同時(shí)將隊(duì)列中的元素分發(fā)到每個(gè)線程對(duì)應(yīng)的隊(duì)列io_threads_list中,這些工作都是在主線程中執(zhí)行的。

        3:每個(gè)線程(包括主線程和子線程)接收請(qǐng)求參數(shù)并做解析,完事后在client中設(shè)置一個(gè)標(biāo)記CLIENT_PENDING_READ,標(biāo)識(shí)參數(shù)解析完成,可以操作數(shù)據(jù)庫了。(主線程和子線程都會(huì)執(zhí)行這個(gè)步驟)

        4:主線程遍歷隊(duì)列clients_pending_read,發(fā)現(xiàn)設(shè)有CLIENT_PENDING_READ標(biāo)記的,就操作數(shù)據(jù)庫

        5:操作完數(shù)據(jù)庫就是響應(yīng)client了,響應(yīng)是一組函數(shù)addReplyXXX,在client中設(shè)置標(biāo)記CLIENT_PENDING_WRITE,同時(shí)將client加入全局寫隊(duì)列clients_pending_write

        6:主線程將全局隊(duì)列clients_pending_write以輪訓(xùn)的方式將任務(wù)分發(fā)到每個(gè)線程對(duì)應(yīng)的隊(duì)列io_threads_list

        7:所有線程將遍歷自己的隊(duì)列io_threads_list,將結(jié)果發(fā)送給client

        ?

        3:redis多線程是怎么做到無鎖的?

        上面說了多線程的地方都是互相獨(dú)立互不影響的。但是每個(gè)線程的隊(duì)列就存在兩個(gè)兩個(gè)線程訪問的情況:主線程向隊(duì)列中寫數(shù)據(jù),子線程消費(fèi),redis的實(shí)現(xiàn)有點(diǎn)反直覺。按正常思路來說,主線程在往隊(duì)列中寫數(shù)據(jù)的時(shí)候加鎖;子線程復(fù)制隊(duì)列&并將隊(duì)列清空,這個(gè)兩個(gè)動(dòng)作是加鎖的,子線程消費(fèi)復(fù)制后的隊(duì)列,這個(gè)過程是不需要加鎖的,按理來說主線程和子線程的加鎖動(dòng)作都是非??斓?。但是redis并沒有這么實(shí)現(xiàn),那么他是怎么實(shí)現(xiàn)的呢?

        ?

        redis多線程的模型是主線程負(fù)責(zé)搜集任務(wù),放入全局讀隊(duì)列clients_pending_read和全局寫隊(duì)列clients_pending_write,主線程在將隊(duì)列中的任務(wù)以輪訓(xùn)的方式分發(fā)到每個(gè)線程對(duì)應(yīng)的隊(duì)列(list *io_threads_list[128])

        1:一開始子線程的隊(duì)列都是空,主線程將全對(duì)隊(duì)列中的任務(wù)分發(fā)到每個(gè)線程的隊(duì)列,并設(shè)置一個(gè)隊(duì)列有數(shù)據(jù)的標(biāo)記(_Atomic unsigned long io_threads_pending[128]),io_threads_pending[1]=5表示第一個(gè)線程的隊(duì)列中有5個(gè)元素

        2:子線程死循環(huán)輪訓(xùn)檢查io_threads_pending[index] > 0,有數(shù)據(jù)就開始處理,處理完成之后將io_threads_pending[index] = 0,沒數(shù)據(jù)繼續(xù)檢查

        3:主線程將任務(wù)分發(fā)到子線程的隊(duì)列中,自己處理自己隊(duì)列中的任務(wù),處理完成后,等待所有子線程處理完所有任務(wù),繼續(xù)收集任務(wù)到全局隊(duì)列,在將任務(wù)分發(fā)給子線程,這樣就避免了主線程和子線程同時(shí)訪問隊(duì)列的情況,主線程向隊(duì)列寫的時(shí)候子線程還沒開始消費(fèi),子線程在消費(fèi)的時(shí)候主線程在等待子線程消費(fèi)完,子線程消費(fèi)完后主線程才會(huì)往隊(duì)列中繼續(xù)寫,就必須加鎖了。因?yàn)槿蝿?wù)是平均分配到每個(gè)隊(duì)列的,所以每個(gè)隊(duì)列的處理時(shí)間是接近的,等待的時(shí)間會(huì)很短。?

        ?

        4:源碼執(zhí)行流程

        為了方便你看源碼,這里加上一些代碼的執(zhí)行流程

        啟動(dòng)socket監(jiān)聽,注冊(cè)連接處理函數(shù),連接成功后創(chuàng)建連接對(duì)象connection,創(chuàng)建client對(duì)象,通過aeCreateFileEvent注冊(cè)client的讀事件

        main?->?initServer?->?acceptTcpHandler?->?anetTcpAccept?->?anetGenericAccept?->?accept(獲取到socket連接句柄)
        connCreateAcceptedSocket?->?connCreateSocket?->?創(chuàng)建一個(gè)connection對(duì)象
        acceptCommonHandler?->?createClient創(chuàng)建client連接對(duì)象?->?connSetReadHandler?->?aeCreateFileEvent?->?readQueryFromClient

        main?->?aeMain?->?aeProcessEvents?->?aeApiPoll(獲取可讀寫的socket)?->?readQueryFromClient(如果可讀)?->?processInputBuffer?->?processCommandAndResetClient(多線程下這個(gè)方法在當(dāng)前流程下不會(huì)執(zhí)行,而由主線程執(zhí)行)

        在多線程模式下,readQueryFromClient會(huì)將client信息加入server.clients_pending_read隊(duì)列,listAddNodeHead(server.clients_pending_read,c);

        ?

        主線程會(huì)將server.clients_pending_read中的數(shù)據(jù)分發(fā)到子線程的隊(duì)列(io_threads_list)中,子線程會(huì)調(diào)用readQueryFromClient就行參數(shù)解析,主線程分發(fā)完任務(wù)后,會(huì)執(zhí)行具體的操作數(shù)據(jù)庫的命令,這塊是單線程

        如果參數(shù)解析完成會(huì)在client->flags中加一個(gè)標(biāo)記CLIENT_PENDING_COMMAND,在主線程中先判斷client->flags & CLIENT_PENDING_COMMAND > 0,說明參數(shù)解析完成,才會(huì)調(diào)用processCommandAndResetClient,之前還擔(dān)心如果子線程還在做參數(shù)解析,主線程就開始執(zhí)行命令難道不會(huì)有問題嗎?現(xiàn)在一切都清楚了

        main?->?aeMain?->?aeProcessEvents?->?beforeSleep?->?handleClientsWithPendingReadsUsingThreads?->?processCommandAndResetClient?->?processCommand?->?call

        讀是多次讀:socket讀緩沖區(qū)有數(shù)據(jù),epoll就會(huì)一直觸發(fā)讀事件,所以讀可能是多次的

        寫是一次寫:往socket寫數(shù)據(jù)是在子線程中執(zhí)行的,直接循環(huán)直到數(shù)據(jù)寫完位置,就算某個(gè)線程阻塞了,也不會(huì)像單線程那樣導(dǎo)致所有任務(wù)都阻塞

        執(zhí)行完相關(guān)命令后,就是將結(jié)果返回給client,回復(fù)client是一組函數(shù),我們以addReply為例,說一下執(zhí)行流程,執(zhí)行addReply還是單線程的,將client信息插入全局隊(duì)列server.clients_pending_write。
        addReply?->?prepareClientToWrite?->?clientInstallWriteHandler?->?listAddNodeHead(server.clients_pending_write,c)

        在主線程中將server.clients_pending_write中的數(shù)據(jù)以輪訓(xùn)的方式分發(fā)到多個(gè)子線程中
        beforeSleep?->?handleClientsWithPendingWritesUsingThreads?->?將server.clients_pending_write中的數(shù)據(jù)以輪訓(xùn)的方式分發(fā)到多個(gè)線程的隊(duì)列中io_threads_list
        list?*io_threads_list[IO_THREADS_MAX_NUM];是數(shù)組雙向鏈表,一個(gè)線程對(duì)應(yīng)其中一個(gè)隊(duì)列

        子線程將client中的數(shù)據(jù)發(fā)給客戶端,所以是多線程
        server.c?->?main?->?initThreadedIO(啟動(dòng)一定數(shù)量的線程)?->?IOThreadMain(線程執(zhí)行的方法)?->?writeToClient?->?connWrite?->?connSocketWrite

        網(wǎng)絡(luò)操作對(duì)應(yīng)的一些方法,所有connection對(duì)象的type字段都是指向CT_Socket

        ConnectionType?CT_Socket?=?{
        ????.ae_handler?=?connSocketEventHandler,
        ????.close?=?connSocketClose,
        ????.write?=?connSocketWrite,
        ????.read?=?connSocketRead,
        ????.accept?=?connSocketAccept,
        ????.connect?=?connSocketConnect,
        ????.set_write_handler?=?connSocketSetWriteHandler,
        ????.set_read_handler?=?connSocketSetReadHandler,
        ????.get_last_error?=?connSocketGetLastError,
        ????.blocking_connect?=?connSocketBlockingConnect,
        ????.sync_write?=?connSocketSyncWrite,
        ????.sync_read?=?connSocketSyncRead,
        ????.sync_readline?=?connSocketSyncReadLine
        };








        粉絲福利:實(shí)戰(zhàn)springboot+CAS單點(diǎn)登錄系統(tǒng)視頻教程免費(fèi)領(lǐng)取

        ???

        ?長(zhǎng)按上方微信二維碼?2 秒
        即可獲取資料



        感謝點(diǎn)贊支持下哈?

        瀏覽 77
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产露脸精品国产探花 | 男人把女人捅到爽 | 四虎影库久免费视频 | 趴在老师腿被脱裙打屁股 | 神马午夜限制 | 人妻洗澡被强公日日澡 | 精品国产91久久久久久一区黄 | 少妇啪啪高潮肉谢 | chinesespanking调教luna | 爱福利视频|