后端工程師基礎(chǔ)知識大全

1~5. Java基礎(chǔ)

6. Kafka

7. 操作系統(tǒng)

7.1 進(jìn)程,線程和協(xié)程

進(jìn)程是操作系統(tǒng)分配資源(CPU,內(nèi)存等)的基本單位。它是程序執(zhí)行時的一個實例。而線程是程序執(zhí)行時的最小單位,一個進(jìn)程可以包含多個線程,這些線程共享進(jìn)程的所有資源,但又獨自擁有自己的堆棧和局部變量。

  • 進(jìn)程是資源分配的最小單位,而線程是程序執(zhí)行時的最小單元
  • 每個進(jìn)程擁有自己獨立的地址空間,每啟動一個進(jìn)程,系統(tǒng)就會為它分配地址空間(建立數(shù)據(jù)表來維護(hù)代碼段、堆棧段和數(shù)據(jù)段),這種操作非常昂貴。而線程則共享進(jìn)程的地址空間,因此CPU切換一個線程的花費(fèi)遠(yuǎn)比進(jìn)程要小很多,同時創(chuàng)建一個線程的開銷也比進(jìn)程要小很多。
  • 線程間的通信更加容易,同一個進(jìn)程中的線程可以通過共享全局變量,靜態(tài)變量等方式通信。而進(jìn)程間通信則需要使用IPC。

而協(xié)程是屬于線程的。協(xié)程程序是在線程里面跑的,因此沒有線程的上下文切換消耗。協(xié)程的調(diào)度切換是用戶(程序員)手動切換的,因此更加靈活,因此又叫用戶空間線程。由于協(xié)程是用戶調(diào)度的,所以不會出現(xiàn)執(zhí)行一半的代碼片段被強(qiáng)制中斷了,因此無需原子操作鎖。

7.2 進(jìn)程間通信

進(jìn)程通信就是兩個進(jìn)程之間進(jìn)行數(shù)據(jù)交換。下面是常用的進(jìn)程間通信的方式:

  • 管道(pipe)
    管道是一種最古老也是最基本的系統(tǒng)IPC形式,所有的Linux系統(tǒng)都提供此種通信機(jī)制。管道有兩個口,一個入水口一個出水口。pipe系統(tǒng)調(diào)用會返回兩個文件描述符,一個文件描述符用來寫一個用來讀。如上圖所示,兩個file結(jié)構(gòu)指向同一個inode,對應(yīng)管道在內(nèi)存種所獲得的一片區(qū)域。這里稍微要注意的一點是,盡管我們平時的應(yīng)用都是一個管道對應(yīng)一個寫進(jìn)程一個讀進(jìn)程,但是管道本身是支持多個進(jìn)程進(jìn)行讀寫的,他們只要對相同的描述符進(jìn)行操作,加之系統(tǒng)的互斥進(jìn)程就可以實現(xiàn)多進(jìn)程的通信。這里也可以看出管道是半雙工的,沒有一個文件描述符可以用來進(jìn)行讀and寫,如果想在兩進(jìn)程間進(jìn)行全雙工操作就開兩條管道吧。
  • 信號量(semophore )
    信號量是一個計數(shù)器,可以用來控制多個進(jìn)程對共享資源的訪問。它常作為一種鎖機(jī)制,防止某進(jìn)程正在訪問共享資源時,其他進(jìn)程也訪問該資源。因此,主要作為進(jìn)程間以及同一進(jìn)程內(nèi)不同線程之間的同步手段。
  • 消息隊列(message queue)
    通過int msgget(key_t, key, int msgflg)int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg)函數(shù)來進(jìn)行消息的讀和寫。消息隊列是由消息的鏈表,存放在內(nèi)核中并由消息隊列標(biāo)識符標(biāo)識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節(jié)流以及緩沖區(qū)大小受限等缺點。
  • 信號(signal)
    信號是在軟件層次上對中斷機(jī)制的一種模擬,在原理上,一個進(jìn)程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進(jìn)程不必通過任何操作來等待信號的到達(dá),事實上,進(jìn)程也不知道信號到底什么時候到達(dá)。信號是進(jìn)程間通信機(jī)制中唯一的異步通信機(jī)制,可以看作是異步通知,通知接收信號的進(jìn)程有哪些事情發(fā)生了。
  • 共享內(nèi)存(shared memory)
    共享內(nèi)存就是映射一段能被其他進(jìn)程所訪問的內(nèi)存,這段共享內(nèi)存由一個進(jìn)程創(chuàng)建,但多個進(jìn)程都可以訪問。共享內(nèi)存是最快的 IPC 方式,它是針對其他進(jìn)程間通信方式運(yùn)行效率低而專門設(shè)計的。它往往與其他通信機(jī)制,如信號量,配合使用,來實現(xiàn)進(jìn)程間的同步和通信。
  • 套接字(socket)
    套解口也是一種進(jìn)程間通信機(jī)制,與其他通信機(jī)制不同的是,它可用于不同機(jī)器間的進(jìn)程通信。

7.3 select,poll和epoll

select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過一種機(jī)制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實現(xiàn)會負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。

select流程如下:
(1)從用戶空間拷貝fd_set到內(nèi)核空間
(2)遍歷所有fd,調(diào)用其對應(yīng)的poll方法
(3)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼
(4)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則調(diào)用select的進(jìn)程(也就是current)進(jìn)入睡眠(睡眠時間為schedule_timeout)。當(dāng)設(shè)備驅(qū)動發(fā)生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進(jìn)程。
(5)如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調(diào)用select的進(jìn)程會重新被喚醒獲得CPU,進(jìn)而重新遍歷fd,判斷有沒有就緒的fd。
(6)把fd_set從內(nèi)核空間拷貝到用戶空間。

select的幾大缺點:
(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大
(2)同時每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個開銷在fd很多時也很大
(3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024

poll的實現(xiàn)和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),其他的都差不多。

select和poll都只提供了一個函數(shù)——select或者poll函數(shù)。而epoll提供了三個函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。

對于第一個缺點,epoll的解決方案在epoll_ctl函數(shù)中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時候重復(fù)拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應(yīng)的設(shè)備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)并為每個fd指定一個回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊列上的等待者時,就會調(diào)用這個回調(diào)函數(shù),而這個回調(diào)函數(shù)會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd。

總結(jié):
(1)select,poll實現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替。但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間。這就是回調(diào)機(jī)制帶來的性能提升。

(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷。

7.4 同步IO和異步IO

同步IO分為同步阻塞IO和同步非阻塞IO。Java中BIO是同步阻塞IO,NIO是同步非阻塞IO。同步阻塞IO在內(nèi)核數(shù)據(jù)未準(zhǔn)備好時,IO線程會直接阻塞直到內(nèi)核數(shù)據(jù)準(zhǔn)備完成。而同步非阻塞IO在內(nèi)核數(shù)據(jù)未準(zhǔn)備好時,read請求也能立即返回(此時IO線程可以做其他事或者不斷輪詢判斷內(nèi)核數(shù)據(jù)是否準(zhǔn)備好)。

為了解決IO線程在用戶空間不斷輪詢的問題,因此提出了IO多路復(fù)用的機(jī)制。通過向selector注冊多個channel,能夠?qū)崿F(xiàn)沒有數(shù)據(jù)準(zhǔn)備好時阻塞,數(shù)據(jù)準(zhǔn)備好時返回對應(yīng)channel。從而實現(xiàn)了單線程處理多個socket連接。Java NIO即采用了IO多路復(fù)用機(jī)制,它的底層依賴于內(nèi)核的epoll函數(shù)來實現(xiàn)。

而異步IO于同步IO的區(qū)別在于,異步IO完全沒有阻塞的過程。即使是同步非阻塞IO,也存在著從內(nèi)核拷貝準(zhǔn)備好的數(shù)據(jù)到用戶空間的過程,這一個拷貝過程仍然是阻塞的。而異步IO在一開始注冊好了一個回調(diào)函數(shù),內(nèi)核準(zhǔn)備好數(shù)據(jù)后直接將數(shù)據(jù)拷貝進(jìn)了用戶空間并調(diào)用回調(diào)函數(shù)。因此異步IO不存在阻塞過程。Java中AIO是一種異步IO。

IO分類.PNG

7.5 進(jìn)程,僵尸進(jìn)程和孤兒進(jìn)程

一個進(jìn)程都由另一個稱之為父進(jìn)程的進(jìn)程啟動,被父進(jìn)程啟動的進(jìn)程叫做子進(jìn)程。Linux系統(tǒng)啟動時候,它將運(yùn)行一個名為init的進(jìn)程,該進(jìn)程是系統(tǒng)運(yùn)行的第一個進(jìn)程,它的進(jìn)程號為1,它負(fù)責(zé)管理其它進(jìn)程,可以把它看做是操作系統(tǒng)進(jìn)程管理器,它是其它所有進(jìn)程的祖先進(jìn)程。系統(tǒng)中的進(jìn)程要么是由init進(jìn)程啟動,要么是由init進(jìn)程啟動的其他進(jìn)程啟動。

Linux創(chuàng)建進(jìn)程的函數(shù)有:system,exec和fork。

system函數(shù)調(diào)用“/bin/sh -c command”執(zhí)行特定的命令,阻塞當(dāng)前進(jìn)程直到command命令執(zhí)行完畢。system()會調(diào)用fork()產(chǎn)生子進(jìn)程,由子進(jìn)程來調(diào)用“/bin/sh -c command”來執(zhí)行參數(shù)string字符串所代表的命令,此命令執(zhí)行完后隨即返回原調(diào)用的進(jìn)程。使用System函數(shù)并非啟動其他進(jìn)程的理想手段,因為它必須用一個shell來啟動需要的程序。

exec是一個函數(shù)組,包括execl,execlp等6個函數(shù)。exec函數(shù)可以把當(dāng)前進(jìn)程替換為一個新進(jìn)程(僅僅是替換,PID不變),新進(jìn)程由path或file參數(shù)指定。新進(jìn)程的PID、PPID和nice值與原來的完全一樣,exec只是用另一個新程序替換了當(dāng)前進(jìn)程的正文、數(shù)據(jù)、堆和棧段。 事實上,這里發(fā)生的一切其實就是, 當(dāng)進(jìn)程調(diào)用一種exec函數(shù)時,該進(jìn)程完全由新進(jìn)程替換。

fork函數(shù)操作系統(tǒng)會復(fù)制一個與父進(jìn)程完全相同的子進(jìn)程,雖說是父子關(guān)系,但是在操作系統(tǒng)看來,他們更像兄弟關(guān)系。這2個進(jìn)程共享代碼空間,但是數(shù)據(jù)空間是互相獨立的,子進(jìn)程數(shù)據(jù)空間中的內(nèi)容是父進(jìn)程的完整拷貝,指令指針也完全相同,子進(jìn)程擁有父進(jìn)程當(dāng)前運(yùn)行到的位置,并將從fork調(diào)用處執(zhí)行,就像原進(jìn)程一樣。

僵尸進(jìn)程

當(dāng)一個子進(jìn)程結(jié)束運(yùn)行時,并非馬上就消失掉,而是留下一個稱為僵尸進(jìn)程(Zombie)的數(shù)據(jù)結(jié)構(gòu),等待父進(jìn)程處理。

在Linux進(jìn)程的狀態(tài)中,僵尸進(jìn)程是非常特殊的一種,它已經(jīng)放棄了幾乎所有內(nèi)存空間,沒有任何可執(zhí)行代碼,也不能被調(diào)度,僅僅在進(jìn)程列表中保留一個位置,直到父進(jìn)程通過wait / waitpid 函數(shù)來取得子進(jìn)程的終止?fàn)顟B(tài)時才釋放。如果它的父進(jìn)程沒安裝SIGCHLD信號處理函數(shù)調(diào)用wait或waitpid()等待子進(jìn)程結(jié)束,又沒有顯式忽略該信號,那么它就一直保持僵尸狀態(tài)。如果這時父進(jìn)程結(jié)束了,那么init進(jìn)程自動會接手這個子進(jìn)程,為它"收尸",它還是能被清除的。

孤兒進(jìn)程

一個父進(jìn)程退出,而它的一個或多個子進(jìn)程還在運(yùn)行,那些子進(jìn)程將成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號為1)所收養(yǎng),并由init進(jìn)程對它們完成狀態(tài)收集工作。

僵尸進(jìn)程和孤兒進(jìn)程的區(qū)別:

  • 僵尸進(jìn)程:當(dāng)前進(jìn)程運(yùn)行結(jié)束父進(jìn)程未結(jié)束且父進(jìn)程沒有調(diào)用wait來清理子進(jìn)程。如果大量的產(chǎn)生僵死進(jìn)程,將因為沒有可用的進(jìn)程號而導(dǎo)致系統(tǒng)不能產(chǎn)生新的進(jìn)程。
  • 孤兒進(jìn)程:當(dāng)前進(jìn)程結(jié)束且父進(jìn)程在它之前結(jié)束,這時子進(jìn)程被“托孤”于init進(jìn)程。孤兒進(jìn)程無危害。

8. Docker

Docker是一個軟件,它通過容器技術(shù)能夠?qū)崿F(xiàn)快速部署和運(yùn)行應(yīng)用。一個容器是Docker中的一個管理單元,它其中包含了應(yīng)用所必須的運(yùn)行環(huán)境,因此應(yīng)用能夠快速的在容器中啟動和運(yùn)行,并且能夠毫無成本的遷移到其他安裝了Docker的物理機(jī)器上。

9. Kubernetes

10. 限流

10.1 令牌桶算法

令牌桶算法的原理是,系統(tǒng)以固定的速率往令牌桶中放入令牌;當(dāng)請求進(jìn)來時,則從桶中取走令牌;當(dāng)桶中令牌為空時,觸發(fā)限流。下面是一個簡易的令牌桶實現(xiàn):

public class RateLimiter {
    private int capacity;
    private int rate;
    private long lastTimestamp;
    private int token;

    public RateLimiter(int permitsPerSecond) {
        this.capacity = permitsPerSecond; //令牌桶容量
        this.rate = permitsPerSecond / 1000;  //每毫秒產(chǎn)生rate個令牌
    }

    public synchronized boolean tryAcquire(int permits) {
        long now = System.currentTimeMillis();
        int newToken = (int) ((now - lastTimestamp) * rate);
        if (newToken > 0) {
            token = Math.min(capacity, token + newToken);
            lastTimestamp = now;
        }
        if (token >= permits) {
            token -= permits;
            return true;
        }
        return false;
    }
}

11. Cassandra

12. Redis

12.1 緩存的作用和可能的問題

緩存的優(yōu)勢主要有兩個:高性能和高并發(fā)。由于緩存存儲在內(nèi)存中,因此查詢速度遠(yuǎn)遠(yuǎn)快于數(shù)據(jù)庫查詢。另外,單機(jī)Mysql的并發(fā)量在2000QPS左右,而Redis單機(jī)并發(fā)量通常在幾萬的級別,因此天然支持高并發(fā)。

使用緩存可能帶來的問題包括:

  • 緩存雪崩,緩存穿透和緩存擊穿
  • 緩存與數(shù)據(jù)庫的一致性問題
  • 緩存并發(fā)競爭

緩存雪崩

緩存雪崩,指的是大量緩存在一段時間內(nèi)集體失效。這樣的話,短時間內(nèi)大量請求會直接打到數(shù)據(jù)庫。

解決方案:針對這種情況,可以在緩存的生存時間后面再加上一個隨機(jī)數(shù),這樣的話就不至于同一時刻集體過期。實際上,因為大量緩存失效意味著這些緩存在同一時刻被設(shè)置的,而這種情況不多見。

緩存穿透

緩存穿透是指大量請求同時訪問數(shù)據(jù)中不存在的數(shù)據(jù)。由于數(shù)據(jù)庫中不存在,因此這些請求每次訪問都會跳過緩存直接訪問數(shù)據(jù)庫。

解決方案:服務(wù)只要從數(shù)據(jù)庫中沒查到,就寫一個默認(rèn)值到緩存里,并設(shè)置一個過期時間。這樣的話,下次有相同的key 來訪問的時候,在緩存失效之前,都可以直接從緩存中取數(shù)據(jù)。

緩存擊穿

緩存擊穿是指在熱點數(shù)據(jù)過期的瞬間,大量請求直接打到數(shù)據(jù)庫,從而造成數(shù)據(jù)庫無法支撐的現(xiàn)象。

解決方案:可以將熱點數(shù)據(jù)設(shè)置為永遠(yuǎn)不過期;或者基于 redis or zookeeper 實現(xiàn)互斥鎖,等待第一個請求構(gòu)建完緩存之后,再釋放鎖,進(jìn)而其它請求才能通過該 key 訪問數(shù)據(jù)。

緩存雙寫一致性

Cache Aside Pattern

最經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式,就是 Cache Aside Pattern。

  • 讀的時候,先讀緩存,緩存沒有的話,就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)。
  • 更新的時候,先更新數(shù)據(jù)庫,然后再刪除緩存。

下面是一些常見的保證緩存一致性的方案:

1. 先寫數(shù)據(jù)庫,再更新緩存(棄用)

這個方案主要缺點有兩個:
(1)有可能更新緩存的代價很高。比如可能更新了某個表的一個字段,然后其對應(yīng)的緩存,是需要查詢另外兩個表的數(shù)據(jù)并進(jìn)行運(yùn)算,才能計算出緩存最新的值的。
(2)可能緩存并不會被頻繁訪問。如果你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。那么對于一個讀請求很小,寫請求很多的緩存來說,那么代價就很高。

2. 先刪除緩存,再更新數(shù)據(jù)庫

可能的不一致性:A刪除了緩存后,再更新數(shù)據(jù)庫之前,B訪問了該數(shù)據(jù)。由于沒有緩存,因此B直接讀取數(shù)據(jù)庫,查詢到了舊值,并寫入了緩存。此后A再更新了數(shù)據(jù)庫數(shù)據(jù)。這就造成了數(shù)據(jù)的不一致性。

解決方案:采用延時雙刪策略

public void write(String key,Object data){
        redis.delKey(key);
        db.updateData(data);
        Thread.sleep(1000);
        redis.delKey(key);
}

在更新數(shù)據(jù)庫后,再進(jìn)行一次Redis key的延遲刪除。這個具體的延時時長取決于業(yè)務(wù)的讀耗時,可以設(shè)置成讀耗時+幾十毫秒。這樣能夠確保刪除讀操作帶來的臟數(shù)據(jù)。

采用這種延時雙刪策略,吞吐量降低怎么辦?
可以將第二次刪除作為異步的。例如重起一個線程,進(jìn)行異步刪除。

3. 先更新數(shù)據(jù)庫,再刪除緩存

可能的不一致性:緩存剛好失效,A讀取數(shù)據(jù)庫得到舊值。B更新數(shù)據(jù)庫后,B刪除緩存。A將舊值寫入緩存。

上面的這種情況概率非常小,因為數(shù)據(jù)庫的讀操作往往比寫操作快很多,因此基本不可能出現(xiàn)A讀完數(shù)據(jù)后,要等待B更新完數(shù)據(jù)后再寫舊值到緩存。

另外,針對方案2和方案3,都可能會存在刪除緩存失敗的情況,這樣緩存中的數(shù)據(jù)就是臟數(shù)據(jù)了。在這種情況下,可以通過將要刪除的key發(fā)送給消息隊列,consumer通過消費(fèi)刪除消息執(zhí)行Redis key的刪除,刪除失敗則不斷重試。另外,還有一種和業(yè)務(wù)邏輯解耦的方式:通過訂閱Mysql的binglog,然后訂閱程序提取出所需要的數(shù)據(jù)以及key,發(fā)送給消息隊列,Consumer負(fù)責(zé)消費(fèi)消息然后進(jìn)行Redis數(shù)據(jù)的刪除,刪除失敗則不斷重試。

另外,通過給緩存設(shè)置過期時間,能夠確保最基本的最終一致性。

如果必須保證強(qiáng)一致性的話,那么可以通過內(nèi)部隊列的方式,將讀寫請求串行化。下面是基本思想:

更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標(biāo)識,將操作路由之后,發(fā)送到一個 jvm 內(nèi)部隊列中。讀取數(shù)據(jù)的時候,如果發(fā)現(xiàn)數(shù)據(jù)不在緩存中,那么將重新執(zhí)行“讀取數(shù)據(jù)+更新緩存”的操作,根據(jù)唯一標(biāo)識路由之后,也發(fā)送到同一個 jvm 內(nèi)部隊列中。

一個隊列對應(yīng)一個工作線程,每個工作線程串行拿到對應(yīng)的操作,然后一條一條的執(zhí)行。這樣的話,一個數(shù)據(jù)變更的操作,先刪除緩存,然后再去更新數(shù)據(jù)庫,但是還沒完成更新。此時如果一個讀請求過來,沒有讀到緩存,那么可以先將緩存更新的請求發(fā)送到隊列中,此時會在隊列中積壓,然后同步等待緩存更新完成。

這里有一個優(yōu)化點,一個隊列中,其實多個更新緩存請求串在一起是沒意義的,因此可以做過濾,如果發(fā)現(xiàn)隊列中已經(jīng)有一個更新緩存的請求了,那么就不用再放個更新請求操作進(jìn)去了,直接等待前面的更新操作請求完成即可。

另外,考慮到讀請求可能積壓時間過長,因此可以設(shè)置一個讀超時時間,一旦到達(dá)這個超時時間仍然沒有出隊列,則直接訪問數(shù)據(jù)庫返回讀取到的值。

緩存并發(fā)競爭

緩存并發(fā)競爭是指多客戶端同時并發(fā)寫一個 key,可能本來應(yīng)該先到的數(shù)據(jù)后到了,導(dǎo)致數(shù)據(jù)錯誤。解決方案如下:

  1. 使用Redis或Zookeeper構(gòu)建分布式鎖,所有針對同一個key的讀寫操作都必須獲得鎖
  2. 對value增加一個時間戳,set時先判斷當(dāng)前value的時間戳是否早于自己的時間戳,如果早于才執(zhí)行set

高可用緩存

Redis緩存宕機(jī)了,有可能造成大量請求直接打到數(shù)據(jù)庫,從而造成服務(wù)不可用。解決方案:

1. 事前:構(gòu)建Redis高可用集群,避免Redis全盤宕機(jī)
2. 事中:使用服務(wù)的本地ehcache緩存,Hystrix限流/降級,避免系統(tǒng)崩潰
3. 事后:Redis 持久化,一旦重啟,自動從磁盤上加載數(shù)據(jù),從而快速恢復(fù)緩存數(shù)據(jù)

12.2 Redis構(gòu)建分布式鎖

對于單機(jī)Redis,構(gòu)建分布式鎖依賴于setNX命令。下面是構(gòu)建一個鎖的命令:

SET resource_name random_value NX PX 30000
  • NX參數(shù)表示key不存在的時候才會設(shè)置成功
  • PX參數(shù)表示過期時間。

釋放鎖的時候,使用lua腳本進(jìn)行CAS操作。

-- 刪除鎖的時候,找到 key 對應(yīng)的 value,跟自己傳過去的 value 做比較,如果是一樣的才刪除。
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

為什么要用 random_value 隨機(jī)值呢?因為如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執(zhí)行完。此時可能已經(jīng)自動釋放鎖了,因此別的客戶端已經(jīng)獲取到了這個鎖,要是你這個時候直接刪除 key 的話那么其他客戶端獲得的鎖就被刪除了,所以得用隨機(jī)值加上面的 lua 腳本來保證原子性的釋放鎖。

在分布式場景下,可以使用RedLock算法:

這個場景是假設(shè)有一個 redis cluster,有 5 個 redis master 實例。然后執(zhí)行如下步驟獲取一把鎖:

  1. 獲取當(dāng)前時間戳,單位是毫秒;
  2. 跟上面類似,輪流嘗試在每個 master 節(jié)點上創(chuàng)建鎖,過期時間較短,一般就幾十毫秒;
  3. 嘗試在大多數(shù)節(jié)點上建立一個鎖,比如 5 個節(jié)點就要求是 3 個節(jié)點 n / 2 + 1;
  4. 客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了;
  5. 要是鎖建立失敗了,那么就依次之前建立過的鎖刪除;
  6. 只要別人建立了一把分布式鎖,你就得不斷輪詢?nèi)L試獲取鎖。

12.3 Redis的線程模型

redis 內(nèi)部使用文件事件處理器 file event handler,這個文件事件處理器是單線程的,所以 redis 才叫做單線程的模型。它采用 IO 多路復(fù)用機(jī)制同時監(jiān)聽多個 socket,將產(chǎn)生事件的 socket 壓入內(nèi)存隊列中,事件分派器根據(jù) socket 上的事件類型來選擇對應(yīng)的事件處理器進(jìn)行處理。

12.4 Redis內(nèi)部數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)

Hash

存儲Hash數(shù)據(jù)時,Redis內(nèi)部使用兩種數(shù)據(jù)結(jié)構(gòu):ziplist和hashtable。

ziplist是一個經(jīng)過特殊編碼的雙向鏈表,它的設(shè)計目標(biāo)就是為了提高存儲效率。ziplist可以用于存儲字符串或整數(shù),其中整數(shù)是按真正的二進(jìn)制表示進(jìn)行編碼的,而不是編碼成字符串序列。它能以O(shè)(1)的時間復(fù)雜度在表的兩端提供push和pop操作。

實際上,ziplist充分體現(xiàn)了Redis對于存儲效率的追求。一個普通的雙向鏈表,鏈表中每一項都占用獨立的一塊內(nèi)存,各項之間用地址指針(或引用)連接起來。這種方式會帶來大量的內(nèi)存碎片,而且地址指針也會占用額外的內(nèi)存。而ziplist卻是將表中每一項存放在前后連續(xù)的地址空間內(nèi),一個ziplist整體占用一大塊內(nèi)存。它是一個表(list),但其實不是一個鏈表(linked list)。

ziplist是一個雙向鏈表,當(dāng)Hash對象滿足以下條件時,Redis會使用ziplist存儲:

  1. 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于 64 字節(jié)
  2. 哈希對象保存的鍵值對數(shù)量小于 512 個

當(dāng)不滿足以上兩種情況時,Hash對象會被存儲在hashtable中。

Sorted Set

存儲Sorted Set時,Redis內(nèi)部使用兩種數(shù)據(jù)結(jié)構(gòu):zset ziplist和skiplist(跳表)。

ziplist是一個雙向鏈表,當(dāng)Sorted Set對象滿足以下條件時,Redis會使用zset ziplist存儲:

  1. Set中元素的 member 長度小于服務(wù)器屬性 server.zset_max_ziplist_value 的值(默認(rèn)為 64 )。
  2. Set的大小小于服務(wù)器屬性 server.zset_max_ziplist_entries 的值(默認(rèn)為 128 )。

zset ziplist插入新元素時,會根據(jù)score進(jìn)行的插入排序,因此zset ziplist總是升序的。

當(dāng)不滿足以上條件時,Sorted Set對象會被存儲在dict和skiplist中。dict是一個hash桶,用于在O(1)時間內(nèi)查找集合元素。skiplist(跳表)效率和平衡樹媲美(但實現(xiàn)比平衡樹簡單很多),用于O(logN)時間內(nèi)根據(jù)score進(jìn)行定位,通常用于ZRANGE,ZRANK等命令。

關(guān)于跳表的數(shù)據(jù)結(jié)構(gòu),請參考:https://lotabout.me/2018/skip-list/

12.5 RDB和AOF

12.6 Redis過期策略

12.7 Redis高可用

Redis高可用可以分為兩個部分,主從架構(gòu)和哨兵。

主從架構(gòu)

主從架構(gòu).PNG

Redis主從架構(gòu),Master負(fù)責(zé)寫請求,Slave負(fù)責(zé)讀請求,然后通過Redis的主從復(fù)制實現(xiàn)主從一致性。

當(dāng)啟動一個 slave node 的時候,它會發(fā)送一個 PSYNC 命令給 master node。如果這是 slave node 初次連接到 master node,那么會觸發(fā)一次 full resynchronization 全量復(fù)制。此時 master 會啟動一個后臺線程,開始生成一份 RDB 快照文件,同時還會將從客戶端 client 新收到的所有寫命令緩存在內(nèi)存中。RDB 文件生成完畢后, master 會將這個 RDB 發(fā)送給 slave,slave 會先寫入本地磁盤,然后再從本地磁盤加載到內(nèi)存中,接著 master 會將內(nèi)存中緩存的寫命令發(fā)送到 slave,slave 也會同步這些數(shù)據(jù)。slave node 如果跟 master node 有網(wǎng)絡(luò)故障,斷開了連接,會自動重連,連接之后 master node 僅會復(fù)制給 slave 部分缺少的數(shù)據(jù)。

之后,master 每次接收到寫命令之后,先在內(nèi)部寫入數(shù)據(jù),然后異步發(fā)送給 slave node。

另外,slave 不會過期 key,只會等待 master 過期 key。如果 master 過期了一個 key,或者通過 LRU 淘汰了一個 key,那么會模擬一條 del 命令發(fā)送給 slave。

哨兵

哨兵(sentinel)是Redis集群中的一個重要組件,它的主要作用如下:

  • 集群監(jiān)控:負(fù)責(zé)監(jiān)控 redis master 和 slave 進(jìn)程是否正常工作。
  • 消息通知:如果某個 redis 實例有故障,那么哨兵負(fù)責(zé)發(fā)送消息作為報警通知給管理員。
  • 故障轉(zhuǎn)移:如果 master node 掛掉了,會自動轉(zhuǎn)移到 slave node 上。
  • 配置中心:如果故障轉(zhuǎn)移發(fā)生了,通知 client 客戶端新的 master 地址。

故障轉(zhuǎn)移時,判斷一個 master node 是否宕機(jī)了,需要大部分的哨兵都同意才行,涉及到了分布式選舉的問題。哨兵至少需要 3 個實例,來保證自己的健壯性。哨兵 + redis 主從的部署架構(gòu),是不保證數(shù)據(jù)零丟失的,只能保證 redis 集群的高可用性。

每次一個哨兵要做主備切換,首先需要quorum個哨兵認(rèn)為master宕機(jī)了(哨兵會定時向master和slave發(fā)送心跳),然后選舉出一個哨兵來做切換,此時這個選舉出來的哨兵還需要得到majority個哨兵的認(rèn)同,才能正式執(zhí)行切換。

哨兵互相之間的發(fā)現(xiàn),是通過 redis 的 pub/sub 系統(tǒng)實現(xiàn)的,每個哨兵都會往 sentinel:hello 這個 channel 里發(fā)送一個消息,這時候所有其他哨兵都可以消費(fèi)到這個消息,并感知到其他的哨兵的存在。

更多關(guān)于哨兵的原理,請參考:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-sentinel.md

12.8 Redis集群和gossip協(xié)議

12.9 Redis熱點key問題

所謂熱key問題就是,突然有幾十萬的請求去訪問redis上的某個特定key。那么,這樣會造成流量過于集中,達(dá)到物理網(wǎng)卡上限,從而導(dǎo)致這臺redis的服務(wù)器宕機(jī)。

12.9.1 怎么發(fā)現(xiàn)熱點key

1. 憑借業(yè)務(wù)經(jīng)驗,進(jìn)行預(yù)估哪些是熱key
其實這個方法還是挺有可行性的。比如某商品在做秒殺,那這個商品的key就可以判斷出是熱key。缺點很明顯,并非所有業(yè)務(wù)都能預(yù)估出哪些key是熱key。

2. 在客戶端進(jìn)行收集
這個方式就是在操作redis之前,加入一行代碼進(jìn)行數(shù)據(jù)統(tǒng)計。那么這個數(shù)據(jù)統(tǒng)計的方式有很多種,也可以是給外部的通訊系統(tǒng)發(fā)送一個通知信息。缺點就是對客戶端代碼造成入侵。

3. 在Proxy層做收集
有些集群架構(gòu)是下面這樣的,Proxy可以是Twemproxy,是統(tǒng)一的入口??梢栽赑roxy層做收集上報,這種方案對業(yè)務(wù)侵入最小,但是缺點很明顯,并非所有的redis集群架構(gòu)都有proxy。

redis proxy.PNG

4. 使用redis monitor命令
下面是使用用redis monitor命令的例子:

//獲取10萬條命令
List<String> keyList = redis.monitor(100000);
//存入到字典中,分別是key和對應(yīng)的次數(shù)
AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create(); 
//統(tǒng)計
for (String command : commandList) {
    ATOMIC_LONG_MAP.incrementAndGet(key);
}
//后續(xù)統(tǒng)計和分析熱點key
statHotKey(ATOMIC_LONG_MAP);

這種方案的缺點是,高并發(fā)條件下會有造成redis 內(nèi)存爆掉的隱患。

5. 使用redis hot-keys參數(shù)
redis 4.0.3提供了redis-cli的熱點key發(fā)現(xiàn)功能,執(zhí)行redis-cli時加上–hotkeys選項即可。但是該參數(shù)在執(zhí)行的時候,如果key比較多,執(zhí)行起來比較慢。下面使用hot-keys命令的例子:

$./redis-cli --hotkeys

# Scanning the entire keyspace to find hot keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Hot key 'counter:000000000002' found so far with counter 87
[00.00%] Hot key 'key:000000000001' found so far with counter 254
[00.00%] Hot key 'mylist' found so far with counter 107
[00.00%] Hot key 'key:000000000000' found so far with counter 254
[45.45%] Hot key 'counter:000000000001' found so far with counter 87
[45.45%] Hot key 'key:000000000002' found so far with counter 254
[45.45%] Hot key 'myset' found so far with counter 64
[45.45%] Hot key 'counter:000000000000' found so far with counter 93

-------- summary -------

Sampled 22 keys in the keyspace!
hot key found with counter: 254    keyname: key:000000000001
hot key found with counter: 254    keyname: key:000000000000
hot key found with counter: 254    keyname: key:000000000002
hot key found with counter: 107    keyname: mylist
hot key found with counter: 93    keyname: counter:000000000000
hot key found with counter: 87    keyname: counter:000000000002
hot key found with counter: 87    keyname: counter:000000000001
hot key found with counter: 64    keyname: myset

要使用--hot-keys參數(shù),必須將maxmemory-policy參數(shù)設(shè)置為volatile-lfuallkeys-lfu。

Redis 4.0新增了allkey-lfu和volatile-lfu兩種數(shù)據(jù)逐出策略。lru策略是淘汰最近一段時間沒有用到的key,而lfu則是淘汰最近一段時間使用次數(shù)最少的數(shù)據(jù)。

6. 抓包評估
Redis客戶端使用TCP協(xié)議與服務(wù)端進(jìn)行交互,通信協(xié)議采用的是RESP。自己寫程序監(jiān)聽端口,按照RESP協(xié)議規(guī)則解析數(shù)據(jù),進(jìn)行分析。缺點就是開發(fā)成本高,維護(hù)困難,有丟包可能性。

12.9.2 發(fā)現(xiàn)熱點key后如何解決

發(fā)現(xiàn)熱點key后,解決方案主要有兩種:利用二級緩存和分散熱點key。

1. 利用二級緩存
比如利用ehcache,甚至一個HashMap都可以。在你發(fā)現(xiàn)熱key以后,把熱key加載到系統(tǒng)的JVM中。針對這種熱key請求,會直接從jvm中取,而不會走到redis層。

缺點:1. 需要額外保證Redis和服務(wù)器端熱點Key的數(shù)據(jù)一致性。2. 如果對可能成為 hot key 的 key 都進(jìn)行本地緩存,那么本地緩存是否會過大,從而影響應(yīng)用程序本身所需的內(nèi)存空間。

2. 備份熱點Key
這種解決方案很簡單,就是將熱點key進(jìn)行分散。每一次讀熱點key時,都加上一個隨機(jī)數(shù)(隨機(jī)數(shù)可以在0-集群機(jī)器數(shù)量間隨機(jī))進(jìn)行讀取,不存在則追加這個熱點key和隨機(jī)數(shù)組成的key。這樣其實相當(dāng)于將熱點key備份了多份。下面是一個簡單實現(xiàn):

const M = N * 2
//生成隨機(jī)數(shù)
random = GenRandom(0, M)
//構(gòu)造備份新key
bakHotKey = hotKey + “_” + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = GetFromDB()
    redis.SET(bakHotKey, expireTime + GenRandom(0,5))
}

缺點:由于熱點key備份了多份,因此增加了保證數(shù)據(jù)一致性的復(fù)雜度。

12.10 BitMap和HyperLogLog

BitMap API如下:

  • SETBIT key offset value給一個指定key的值的第offset位賦值為value。value只能為0或1。
  • GETBIT key offset獲取指定key的值的第offset位的值。
  • bitcount key [start, end] 統(tǒng)計 key 上位為1的個數(shù)。

通過bitmap,我們可以用最少的內(nèi)存處理以下問題:

  • 統(tǒng)計網(wǎng)站活躍用戶數(shù)量(將uid位置為1,然后調(diào)用bitcount返回)
  • 統(tǒng)計用戶在線狀態(tài)

通過BITOP operation destkey key [key ...]命令能夠?qū)崿F(xiàn)對多個bitmap的運(yùn)算操作。其中operation支持 AND 、 OR 、 NOT 、 XOR 4種操作。

而HyperLogLog目的是做基數(shù)統(tǒng)計,故不是集合,不會保存元數(shù)據(jù),只記錄數(shù)量而不是數(shù)值,存在一定誤差。HyperLogLog API如下:

  • PFADD key element [element …]給指定key添加元素
  • PFCOUNT key [key …] 返回指定key的基數(shù)
  • PFMERGE destkey sourcekey [sourcekey …] 合并多個HyperLogLog
redis> PFADD  databases  "Redis"  "MongoDB"  "MySQL"
(integer) 1

redis> PFCOUNT  databases
(integer) 3

redis> PFADD  databases  "Redis"    # Redis 已經(jīng)存在,不必對估計數(shù)量進(jìn)行更新
(integer) 0

redis> PFCOUNT  databases    # 元素估計數(shù)量沒有變化
(integer) 3

redis> PFADD  databases  "PostgreSQL"    # 添加一個不存在的元素
(integer) 1

redis> PFCOUNT  databases    # 估計數(shù)量增一
4

應(yīng)用場景:

  • 基數(shù)不大,數(shù)據(jù)量不大就用不上,會有點大材小用浪費(fèi)空間
  • 有局限性,就是只能統(tǒng)計基數(shù)數(shù)量,而沒辦法去知道具體的內(nèi)容是什么
  • 和bitmap相比,屬于兩種特定統(tǒng)計情況,簡單來說,HyperLogLog 去重比 bitmap 方便很多
  • 一般可以bitmap和hyperloglog配合使用,bitmap標(biāo)識哪些用戶活躍,hyperloglog計數(shù)

12.11 Redis構(gòu)建樂觀鎖

https://my.oschina.net/itommy/blog/1790641

13. 分布式相關(guān)

13.1 CAP理論

13.2 全局唯一ID算法

TDDL算法

  1. 在數(shù)據(jù)庫中創(chuàng)建 sequence 表,用于記錄當(dāng)前已被占用的id最大值。
  2. 每臺客戶端主機(jī)取一個id區(qū)間(比如 1000~2000, 即max_id + step)緩存在本地,并更新 sequence 表中的id最大值記錄。
  3. 客戶端主機(jī)之間取不同的id區(qū)間,用完再取,使用樂觀鎖機(jī)制控制并發(fā)。

Snowflake算法

將64位ID分割為4部分:

  1. 最高位不使用
  2. 41位存儲時間戳。需要注意的是此處的 41 位時間戳并非存儲當(dāng)前時間的時間戳,而是存儲時間戳的差值(當(dāng)前時間戳 - 起始時間戳),這里的起始時間戳一般是ID生成器開始使用的時間戳,由程序來指定。
  3. 10位存儲機(jī)器標(biāo)志。包括5位數(shù)據(jù)標(biāo)識位和5位機(jī)器標(biāo)識位,這10位決定了分布式系統(tǒng)中最多可以部署 1 << 10 = 1024 s個節(jié)點。超過這個數(shù)量,生成的ID就有可能會沖突。
  4. 12位毫秒內(nèi)的序列。這 12 位計數(shù)支持每個節(jié)點每毫秒(同一臺機(jī)器,同一時刻)最多生成 1 << 12 = 4096個ID。

13.3 分布式事務(wù)

14. 數(shù)據(jù)庫

14.1 主從復(fù)制

隨著系統(tǒng)中業(yè)務(wù)訪問量的增大,如果是單機(jī)部署數(shù)據(jù)庫,就會導(dǎo)致I/O訪問頻率過高。有了主從復(fù)制,增加多個數(shù)據(jù)存儲節(jié)點,將負(fù)載分布在多個從節(jié)點上,降低單機(jī)磁盤I/O訪問的頻率,提高單個機(jī)器的I/O性能。

讀寫分離,簡單來說就是一個主庫,掛多個從庫,然后我們就單單只是寫主庫,然后主庫會自動把數(shù)據(jù)給同步到從庫上去。

主從復(fù)制過程中,主庫將變更寫入 binlog 日志,然后主庫的IO線程負(fù)責(zé)將新的變更發(fā)送給從庫,從庫有一個 IO 線程將主庫的binglog拷貝到自己本地,寫入一個 relay 中繼日志中。接著從庫中有一個 SQL 線程會從中繼日志讀取 binlog。

Mysql主從復(fù)制模式分為三種:異步模式,半同步模式和全同步模式。

  • 異步模式(默認(rèn)):主庫將事務(wù) Binlog 事件寫入到 Binlog 文件中,此時主庫只會通知一下IO線程發(fā)送這些新的 Binlog,然后主庫就會繼續(xù)處理提交操作,而此時不會保證這些 Binlog 傳到任何一個從庫節(jié)點上。
  • 半同步模式:是介于全同步復(fù)制與全異步復(fù)制之間的一種,主庫只需要等待至少一個從庫節(jié)點收到并且 Flush Binlog 到 Relay Log 文件即可,主庫不需要等待所有從庫給主庫反饋。同時,這里只是一個收到的反饋,而不是已經(jīng)完全完成并且提交的反饋,如此,節(jié)省了很多時間。
  • 全同步模式:指當(dāng)主庫執(zhí)行完一個事務(wù),所有的從庫都執(zhí)行了該事務(wù)才返回給客戶端。因為需要等待所有從庫執(zhí)行完該事務(wù)才能返回,所以全同步復(fù)制的性能必然會收到嚴(yán)重的影響。

binlog記錄格式也分為三種:基于SQL語句的復(fù)制(statement-based replication,SBR),基于行的復(fù)制(row-based replication,RBR),混合模式復(fù)制(mixed-based replication,MBR)。對應(yīng)的binlog文件的格式也有三種:STATEMENT,ROW,MIXED。

14.2 分庫分表

水平拆分是把一個表的數(shù)據(jù)給弄到多個庫的多個表里去,但是每個庫的表結(jié)構(gòu)都一樣,只不過每個庫表放的數(shù)據(jù)是不同的,所有庫表的數(shù)據(jù)加起來就是全部數(shù)據(jù)。水平拆分的意義,就是將數(shù)據(jù)均勻放更多的庫里,然后用多個庫來扛更高的并發(fā),還有就是用多個庫的存儲容量來進(jìn)行擴(kuò)容。

垂直拆分的意思,就是把一個有很多字段的表給拆分成多個表,或者是多個庫上去。每個庫表的結(jié)構(gòu)都不一樣,每個庫表都包含部分字段。一般來說,會將較少的訪問頻率很高的字段放到一個表里去,然后將較多的訪問頻率很低的字段放到另外一個表里去。因為數(shù)據(jù)庫是有緩存的,你訪問頻率高的行字段越少,就可以在緩存里緩存更多的行,性能就越好。這個一般在表層面做的較多一些。

分庫分表方案

平滑遷移

采用雙倍擴(kuò)容策略。擴(kuò)容前每個節(jié)點的數(shù)據(jù),有一半要遷移至一個新增節(jié)點中,對應(yīng)關(guān)系比較簡單,并且無需停止應(yīng)用服務(wù)器。具體操作如下(假設(shè)已有 2 個節(jié)點 A/B,要雙倍擴(kuò)容至 A/A2/B/B2 這 4 個節(jié)點):

  • 新增兩個數(shù)據(jù)庫 A2/B2 作為從庫,設(shè)置主從同步關(guān)系為:A=>A2、B=>B2,直至主從數(shù)據(jù)同步完畢(早期數(shù)據(jù)可手工同步)
  • 調(diào)整分片規(guī)則并使之生效:
    原 ID%2=0 => A 改為 ID%4=0 => A, ID%4=2 => A2
    原 ID%2=1 => B 改為 ID%4=1 => B, ID%4=3 => B2
  • 解除數(shù)據(jù)庫實例的主從同步關(guān)系,并使之生效
  • 此時,四個節(jié)點的數(shù)據(jù)都已完整,只是有冗余(多存了和自己配對的節(jié)點的那部分?jǐn)?shù)據(jù)),擇機(jī)清除即可(過后隨時進(jìn)行,不影響業(yè)務(wù))

更多平滑擴(kuò)容的內(nèi)容,請參考:

14.3 最左前綴匹配原則

mysql建立多列索引(聯(lián)合索引)有最左前綴的原則,即最左優(yōu)先,即:

如果有一個2列的索引(col1,col2),則已經(jīng)對(col1)、(col1,col2)上建立了索引;
如果有一個3列索引(col1,col2,col3),則已經(jīng)對(col1)、(col1,col2)、(col1,col2,col3)上建立了索引;

下面是2個例子:

1、b+樹的數(shù)據(jù)項是復(fù)合的數(shù)據(jù)結(jié)構(gòu),比如(name,age,sex)的時候,b+樹是按照從左到右的順序來建立搜索樹的,比如當(dāng)(張三,20,F)這樣的數(shù)據(jù)來檢索的時候,b+樹會優(yōu)先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最后得到檢索的數(shù)據(jù);但當(dāng)(20,F)這樣的沒有name的數(shù)據(jù)來的時候,b+樹就不知道第一步該查哪個節(jié)點,因為建立搜索樹的時候name就是第一個比較因子,必須要先根據(jù)name來搜索才能知道下一步去哪里查詢。

2、比如當(dāng)(張三,F)這樣的數(shù)據(jù)來檢索時,b+樹可以用name來指定搜索方向,但下一個字段age的缺失,所以只能把名字等于張三的數(shù)據(jù)都找到,然后再匹配性別是F的數(shù)據(jù)了, 這個是非常重要的性質(zhì),即索引的最左匹配特性。(這種情況無法用到聯(lián)合索引)

問題1:假如要查 A in () AND B in (), 怎么建索引?

  • 只給選擇性高的一列建索引,這里因為兩個都是范圍查詢所以另一個是走不到索引的。(這里答的不好,其實也可以建聯(lián)合索引然后用 (A,B) in ((1,2),(3,4)) 的方式去查)

問題2: 查 A in () AND B in () 時, MySQL 是怎么利用索引的?

  • 先走一個非聚簇索引,查詢出行數(shù)據(jù)后再用另一列回表做篩選

14.4 分頁

limit 的用法是 limit [offset], [rows],其中 offset 表示偏移值, rows 表示需要返回的數(shù)據(jù)行。mysql 的 limit 給分頁帶來了極大的方便,但數(shù)據(jù)偏移量一大,limit 的性能就急劇下降。

如果能夠確定上一次分頁的最大id,那么可以這樣優(yōu)化:

select * from table_name where (id >= 10000) limit 10

否則,可以通過先select主鍵再子查詢的方式來進(jìn)行:

SQL代碼1:平均用時6.6秒 SELECT * FROM `cdb_posts` ORDER BY pid LIMIT 1000000 , 30

SQL代碼2:平均用時0.6秒 SELECT * FROM `cdb_posts` WHERE pid >= (SELECT pid FROM  
`cdb_posts` ORDER BY pid LIMIT 1000000 , 1) LIMIT 30

因為要取出所有字段內(nèi)容,第一種需要跨越大量數(shù)據(jù)塊并取出,而第二種基本通過直接根據(jù)索引字段定位后,才取出相應(yīng)內(nèi)容,效率自然大大提升。對limit的優(yōu)化,不是直接使用limit,而是首先獲取到offset的id,然后直接使用limit size來獲取數(shù)據(jù)。(這種優(yōu)化在聚簇索引下可能無效)

14.5 索引

Mysql支持的索引包括B+樹索引,哈希索引,全文索引等等。B+樹是一個平衡的多叉樹。B+樹從根節(jié)點到葉子節(jié)點的搜索效率基本相當(dāng),不會出現(xiàn)大幅波動。哈希索引采用一定的哈希算法,把鍵值換成新的哈希值,檢索時不需要類似B+樹那樣從根節(jié)點逐級查找,只需一次哈希算法即可立刻定位到相應(yīng)的位置。

哈希索引的缺點:

  1. 只支持等值查詢,不支持范圍查詢
  2. 不支持按照索引值排序
  3. 不支持部分索引查詢(哈希索引始終是使用索引列的全部內(nèi)容來計算哈希值)

更多關(guān)于索引的內(nèi)容,請參考:https://www.cnblogs.com/duanxz/p/3799045.html

為什么數(shù)據(jù)庫使用B+樹作為索引實現(xiàn)?

1. B+樹的磁盤讀寫代價更低。由于B樹的每一個節(jié)點都保存了數(shù)據(jù),而B+樹所有的數(shù)據(jù)都保存在葉子節(jié)點,因此每一次IO B+樹讀到的關(guān)鍵字信息就更多,因此IO次數(shù)相對更少。

2. B+樹的范圍查詢效率更高。由于B樹的所有信息分散在整顆樹中,因此范圍查詢需要一遍中序遍歷。而B+樹的葉子節(jié)點保存了所有的數(shù)據(jù),并使用鏈表連接,因此范圍查詢更加迅速。

更多關(guān)于數(shù)控索引數(shù)據(jù)結(jié)構(gòu)的內(nèi)容,請參考:https://blog.csdn.net/xlgen157387/article/details/79450295

14.6 Mysql事務(wù),鎖和MVCC原理

15. RPC框架的設(shè)計

16. 大數(shù)據(jù)查詢問題

17. ZooKeeper

18. Hystrix

19. 計算機(jī)網(wǎng)絡(luò)

20. 應(yīng)用類問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容