【备战秋招系列-4】Java高频知识——并发、Spring、MySQL、redis
并發(fā) 20
P1:Java 內(nèi)存模型
Java 線程的通信由 JMM 控制,JMM 的主要目的是定義程序中各種變量的訪問(wèn)規(guī)則,關(guān)注在虛擬機(jī)中把變量值存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細(xì)節(jié)。此處的變量包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組元素的對(duì)象,但不包括局部變量與方法參數(shù),因?yàn)樗鼈兪蔷€程私有的,不存在多線程競(jìng)爭(zhēng)問(wèn)題。為了獲得更好的執(zhí)行效率,JMM 沒(méi)有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來(lái)和主內(nèi)存進(jìn)行交互,也沒(méi)有限制即時(shí)編譯器是否要進(jìn)行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施,JMM 遵循一個(gè)基本原則:只要不改變程序執(zhí)行結(jié)果,編譯器和處理器怎么優(yōu)化都行。例如編譯器分析某個(gè)鎖只會(huì)單線程訪問(wèn)就消除該鎖,某個(gè) volatile 變量只會(huì)單線程訪問(wèn)就把它當(dāng)作普通變量。
JMM 規(guī)定了所有變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,工作內(nèi)存中保存了被該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作都必須在工作空間中進(jìn)行,而不能直接讀寫主內(nèi)存中的數(shù)據(jù)。不同線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,兩個(gè)線程之間的通信必須經(jīng)過(guò)主內(nèi)存,JMM 通過(guò)控制主內(nèi)存與每個(gè)線程的工作內(nèi)存之間的交互來(lái)提供內(nèi)存可見(jiàn)性保證。
關(guān)于主內(nèi)存與工作內(nèi)存之間的交互,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存等實(shí)現(xiàn)細(xì)節(jié),JMM 定義了 8 種原子操作:
- lock:作用于主內(nèi)存變量,把變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
- unlock:作用于主內(nèi)存變量,把處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才能被其他線程鎖定。
- read:作用于主內(nèi)存變量,把變量值從主內(nèi)存?zhèn)鞯焦ぷ鲀?nèi)存。
- load:作用于工作內(nèi)存變量,把 read 從主存中得到的值放入工作內(nèi)存的變量副本。
- use:作用于工作內(nèi)存變量,把工作內(nèi)存中的變量值傳給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到需要使用變量值的字節(jié)碼指令時(shí)執(zhí)行該操作。
- assign:作用于工作內(nèi)存變量,把從執(zhí)行引擎接收的值賦給工作內(nèi)存變量,每當(dāng)虛擬機(jī)遇到給變量賦值的字節(jié)碼指令時(shí)執(zhí)行該操作。
- store:作用于工作內(nèi)存變量,把工作內(nèi)存中的變量值傳送到主內(nèi)存。
- write:作用于主內(nèi)存變量,把 store 從工作內(nèi)存取到的變量值放入主內(nèi)存變量中。
如果要把一個(gè)變量從主內(nèi)存拷貝到工作內(nèi)存,就要按順序執(zhí)行 read 和 load ,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要按順序執(zhí)行 store 和 write 。JMM 只要求這兩種操作必須按順序執(zhí)行,但不要求連續(xù),也就是說(shuō) read 和 load、store 和 write 之間可插入其他指令。這種定義十分嚴(yán)謹(jǐn)?shù)^(guò)于復(fù)雜,之后 Java 將內(nèi)存操作簡(jiǎn)化為 lock、unlock、read 和 write 四種,但這只是語(yǔ)言描述上的等價(jià)化簡(jiǎn)。
P2:as-if-serial 和 happens-before 規(guī)則
as-if-serial
as-if-serial 的語(yǔ)義是:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變,編譯器和處理器必須遵循 as-if-serial 語(yǔ)義。
為了遵循 as-if-serial 語(yǔ)義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
as-if-serial 語(yǔ)義把單線程程序保護(hù)了起來(lái),給了程序員一種幻覺(jué):單線程程序是按程序的順序執(zhí)行的,使程序員無(wú)需擔(dān)心重排序會(huì)干擾他們,也無(wú)需擔(dān)心內(nèi)存可見(jiàn)性問(wèn)題。
happens-before
先行發(fā)生原則,是 JMM 中定義的兩項(xiàng)操作之間的偏序關(guān)系,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng),線程是否安全的重要手段。
JMM 將 happens-before 要求禁止的重排序按是否會(huì)改變程序執(zhí)行結(jié)果分為兩類。對(duì)于會(huì)改變結(jié)果的重排序 JMM 要求編譯器和處理器必須禁止這種重排序,對(duì)于不會(huì)改變結(jié)果的重排序,JMM 對(duì)編譯器和處理器不做要求。
JMM 存在一些天然的 happens-before 關(guān)系,無(wú)需任何同步器協(xié)助就已經(jīng)存在。如果兩個(gè)操作的關(guān)系不在此列,并且無(wú)法從這些規(guī)則推導(dǎo)出來(lái),它們就沒(méi)有順序性保障,虛擬機(jī)可以對(duì)它們隨意進(jìn)行重排序。
- 程序次序規(guī)則:在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 管程鎖定規(guī)則:一個(gè) unlock 操作先行發(fā)生于后面對(duì)同一個(gè)鎖的 lock 操作。
- volatile 規(guī)則:對(duì)一個(gè) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。
- 線程啟動(dòng)規(guī)則:線程對(duì)象的 start 方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
- 線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè)。
- 線程中斷規(guī)則:對(duì)線程 interrupt 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生。
- 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于它的 finalize 方法的開(kāi)始。
- 傳遞性:如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C 。
區(qū)別
as-if-serial 保證單線程程序的執(zhí)行結(jié)果不被改變,happens-before 保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。這兩種語(yǔ)義的目的都是為了在不改變程序執(zhí)行結(jié)果的前提下盡可能提高程序執(zhí)行的并行度。
P3:指令重排序
重排序指從源代碼到指令序列的重排序,在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器通常會(huì)對(duì)指令進(jìn)行重排序,分為三種:
- 編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語(yǔ)義的前提下可以重排語(yǔ)句的執(zhí)行順序。
- 指令級(jí)并行的重排序:如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序:由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作操作看上去可能是亂序執(zhí)行。
從 Java 源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷編譯器優(yōu)化重排序、指令級(jí)并行重排序和內(nèi)存系統(tǒng)重排序,這些重排序可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。
對(duì)于編譯器,JMM 的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序。對(duì)于處理器重排序,JMM 的處理器重排序規(guī)則會(huì)要求 Java 編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令,即一組用于實(shí)現(xiàn)對(duì)內(nèi)存操作順序限制的處理器指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。JMM 屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和處理器平臺(tái)上,通過(guò)禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。
P4:原子性、可見(jiàn)性和有序性
原子性
由 JMM 直接保證的原子性變量操作包括 read、load、assign、use、store 和 write,基本數(shù)據(jù)類型的訪問(wèn)都是具備原子性的,例外就是 long 和 double 的非原子性協(xié)定,允許虛擬機(jī)將沒(méi)有被 volatile 修飾的 64 位數(shù)據(jù)的操作劃分為兩次 32 位的操作。
如果應(yīng)用場(chǎng)景需要更大范圍的原子性保證,JMM 還提供了 lock 和 unlock 操作滿足這種需求,盡管 JVM 沒(méi)有把這兩種操作直接開(kāi)放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令 monitorenter 和 monitorexit 來(lái)隱式地使用這兩個(gè)操作,這兩個(gè)字節(jié)碼指令反映到 Java 代碼中就是 synchronized 關(guān)鍵字。
可見(jiàn)性
可見(jiàn)性就是指當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知修改。JMM 通過(guò)在變量修改后將值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式實(shí)現(xiàn)可見(jiàn)性,無(wú)論是普通變量還是volatile變量都是如此,區(qū)別是 volatile 保證新值能立即同步到主內(nèi)存以及每次使用前立即從主內(nèi)存刷新,因此說(shuō) volatile 保證了多線程操作變量的可見(jiàn)性,而普通變量則不能保證。
除了 volatile 之外,還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見(jiàn)性,分別是 synchronized 和 final,同步塊的可見(jiàn)性是由"對(duì)一個(gè)變量執(zhí)行unlock 前必須先把此變量同步回主內(nèi)存中,即先執(zhí)行 store 和 write"這條規(guī)則獲得的。final 的可見(jiàn)性是指:被 final 修飾的字段在構(gòu)造方法中一旦被初始化完成,并且構(gòu)造方法沒(méi)有把"this"引用傳遞出去,那么其他線程就能看到 final 字段的值。
有序性
有序性可以總結(jié)為:在本線程內(nèi)觀察所有操作是有序的,在一個(gè)線程內(nèi)觀察另一個(gè)線程,所有操作都是無(wú)序的。前半句是指"as-if-serial 語(yǔ)義",后半句是指"指令重排序"和"工作內(nèi)存與主內(nèi)存同步延遲"現(xiàn)象。
Java 提供了 volatile 和 synchronized 保證線程間操作的有序性,volatile 本身就包含了禁止指令重排序的語(yǔ)義,而 synchronized 則是由"一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作"這條規(guī)則獲得的,該規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行進(jìn)入。
P5:volatile 關(guān)鍵字
輕量級(jí)的線程操作可見(jiàn)方式,JMM 為 volatile 定義了一些特殊的訪問(wèn)規(guī)則,當(dāng)一個(gè)變量被定義為 volatile 后具備兩種特性:
- 保證此變量對(duì)所有線程的可見(jiàn)性
可見(jiàn)性是指當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來(lái)說(shuō)是立即可以得知的。而普通變量并不能做到這一點(diǎn),普通變量的值在線程間傳遞時(shí)均需要通過(guò)主內(nèi)存來(lái)完成。
volatile 變量在各個(gè)線程的工作內(nèi)存中不存在一致性問(wèn)題,但 Java 的運(yùn)算操作符并非原子操作,這導(dǎo)致 volatile 變量運(yùn)算在并發(fā)下仍是不安全的。 - 禁止指令重排序優(yōu)化
使用 volatile 變量進(jìn)行寫操作,匯編指令操作是帶有 lock 前綴的,相當(dāng)于一個(gè)內(nèi)存屏障,后面的指令不能重排到內(nèi)存屏障之前的位置。只有一個(gè)處理器時(shí)不需要使用內(nèi)存屏障,但如果有兩個(gè)或更多的處理器訪問(wèn)同一塊內(nèi)存,且其中有一個(gè)在觀測(cè)另一個(gè),就需要使用內(nèi)存屏障來(lái)保證一致性了。
使用 lock 前綴的指令在多核處理器中會(huì)引發(fā)兩件事:① 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。② 這個(gè)寫回內(nèi)存的操作會(huì)使其他在CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效。
這種操作相當(dāng)于對(duì)緩存中的變量做了一次 store 和 write 操作,可以讓 volatile 變量的修改對(duì)其他處理器立即可見(jiàn)。
靜態(tài)變量 i 執(zhí)行多線程 i++ 的不安全問(wèn)題
通過(guò)反編譯會(huì)發(fā)現(xiàn)一個(gè)自增語(yǔ)句是由 4 條字節(jié)碼指令構(gòu)成的,依次為getstatic、iconst_1、iadd、putstatic,當(dāng)getstatic把 i 的值取到操作棧頂時(shí),volatile保證了 i 的值在此刻是正確的,但是在執(zhí)行iconst_1、iadd這些指令時(shí),其他線程可能已經(jīng)改變了i的值,而操作棧頂?shù)闹稻妥兂闪诉^(guò)期的數(shù)據(jù),所以 putstatic 指令執(zhí)行后就可能把較小的 i 值同步回了主內(nèi)存。
即使編譯出來(lái)只有一條字節(jié)碼指令也不能意味著這條指令就是一個(gè)原子操作,一條字節(jié)碼指令在解釋執(zhí)行時(shí),解釋器要運(yùn)行很多行代碼才能實(shí)現(xiàn)它的語(yǔ)義。如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉(zhuǎn)化成若干條本地機(jī)器碼指令。
適用場(chǎng)景
運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
變量不需要與其他狀態(tài)變量共同參與不變約束。
volatile的內(nèi)存語(yǔ)義
從內(nèi)存語(yǔ)義角度來(lái)說(shuō),volatile的寫-讀與鎖的釋放-獲取具有相同的內(nèi)存效果。
- 寫內(nèi)存語(yǔ)義:當(dāng)寫一個(gè)volatile變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
- 讀內(nèi)存語(yǔ)義:當(dāng)讀一個(gè)volatile變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,線程接下來(lái)將從主內(nèi)存中讀取共享變量。
volatile指令重排序的特點(diǎn)
當(dāng)?shù)诙€(gè)操作是volatile 寫時(shí),不管第一個(gè)操作是什么都不能重排序,確保寫之前的操作不會(huì)被編譯器重排序到寫之后。
當(dāng)?shù)谝粋€(gè)操作是volatile 讀時(shí),不管第二個(gè)操作是什么都不能重排序,確保讀之后的操作不會(huì)被編譯器重排序到讀之前。
當(dāng)?shù)谝粋€(gè)操作是volatile 寫,第二個(gè)操作是 volatile 讀時(shí)不能重排序。
JSR-133 增強(qiáng) volatile 語(yǔ)義的原因
在舊的內(nèi)存模型中,雖然不允許 volatile 變量之間重排序,但允許 volatile 變量與普通變量重排序,可能導(dǎo)致內(nèi)存不可見(jiàn)問(wèn)題。為了提供一種比鎖更輕量級(jí)的線程通信機(jī)制,嚴(yán)格限制了編譯器和處理器對(duì) volatile 變量與普通變量的重排序,確保 volatile 的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語(yǔ)義。
P6:final 關(guān)鍵字
final 可以保證可見(jiàn)性,被 final 修飾的字段在構(gòu)造方法中一旦被初始化完成,并且構(gòu)造方法沒(méi)有把 this 的引用傳遞出去,那么在其他線程中就能看見(jiàn) final 字段的值。
JSR-133 增強(qiáng) final語(yǔ)義的原因
在舊的 JMM 中,一個(gè)嚴(yán)重的缺陷就是線程可能看到 final 域的值會(huì)改變。比如一個(gè)線程看到一個(gè) int 類型 final 域的值為0,此時(shí)該值是還未初始化之前的零值,過(guò)一段時(shí)間之后該值被某線程初始化后這個(gè)線程再去讀這個(gè) final 域的值會(huì)發(fā)現(xiàn)值變?yōu)?。
為了修復(fù)該漏洞,JSR-133 通過(guò)為 final 域增加寫和讀重排序規(guī)則,提供初始化安全保證:只要對(duì)象是正確構(gòu)造的(被構(gòu)造對(duì)象的引用在構(gòu)造方法中沒(méi)有逸出),那么不需要使用同步就可以保證任意線程都能看到這個(gè)final域在構(gòu)造方法中被初始化之后的值。
寫 final 域重排序規(guī)則
禁止把 final 域的寫重排序到構(gòu)造方法之外,編譯器會(huì)在final域的寫之后,構(gòu)造方法的 return之前,插入一個(gè)Store Store屏障。該規(guī)則可以確保在對(duì)象引用為任意線程可見(jiàn)之前,對(duì)象的 final 域已經(jīng)被正確初始化過(guò)了,而普通域不具有這個(gè)保障。
對(duì)于引用類型,增加了約束:在構(gòu)造方法內(nèi)對(duì)一個(gè) final 引用的對(duì)象的成員域的寫入,與隨后在構(gòu)造方法外把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
讀 final 域重排序規(guī)則
在一個(gè)線程中,初次讀對(duì)象引用和初次讀該對(duì)象包含的final域,JMM 禁止處理器重排序這兩個(gè)操作。編譯器會(huì)在讀final域操作的前面插入一個(gè)Load Load 屏障,該規(guī)則可以確保在讀一個(gè)對(duì)象的 final 域之前,一定會(huì)先讀包含這個(gè) final 域的對(duì)象的引用。
P7:synchronized 關(guān)鍵字
每個(gè) Java 對(duì)象都有一個(gè)關(guān)聯(lián)的 monitor 監(jiān)視器,使用 synchronized 時(shí),JVM 會(huì)根據(jù) synchronized 的使用環(huán)境找到對(duì)應(yīng)對(duì)象的 monitor,再根據(jù) monitor 的狀態(tài)進(jìn)行加、解鎖的判斷。如果成功加鎖就成為該 monitor 的唯一持有者,monitor 在被釋放前不能再被其他線程獲取。
方法元信息中會(huì)使用 ACC_SYNCHRONIZED 標(biāo)識(shí)該方法是一個(gè)同步方法,同步代碼塊中會(huì)使用 monitorenter 和 monitorexit 這兩個(gè)字節(jié)碼指令獲取和釋放 monitor。這兩個(gè)字節(jié)碼指令都需要一個(gè)引用類型的參數(shù)來(lái)指明要鎖定和解鎖的對(duì)象,對(duì)于同步普通方法,鎖是當(dāng)前實(shí)例對(duì)象;對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的 Class 對(duì)象;對(duì)于同步方法塊,鎖是 synchronized 括號(hào)里的對(duì)象。
在執(zhí)行 monitorenter 指令時(shí),首先要去嘗試獲取對(duì)象的鎖。如果這個(gè)對(duì)象沒(méi)有被鎖定,或者當(dāng)前線程已經(jīng)持有了那個(gè)對(duì)象的鎖,那么就把鎖的計(jì)數(shù)器的值增加 1,而在執(zhí)行 monitorexit 指令時(shí)會(huì)將鎖計(jì)數(shù)器的值減 1。一旦計(jì)數(shù)器的值為 0,鎖隨即就被釋放了。如果獲取鎖對(duì)象失敗,那當(dāng)前線程就應(yīng)該被阻塞等待,直到請(qǐng)求鎖定的對(duì)象被持有它的線程釋放為止。
例如有兩個(gè)線程 A 和 B 競(jìng)爭(zhēng) monitor,當(dāng)線程 A 競(jìng)爭(zhēng)到鎖時(shí),會(huì)將 monitor 中的 owner 設(shè)置為 A,把線程 B 阻塞并放到等待競(jìng)爭(zhēng)資源的 ContentionList 隊(duì)列。ContentionList 中的部分線程會(huì)進(jìn)入 EntryList,EntryList 中的線程會(huì)被指定為 OnDeck 競(jìng)爭(zhēng)候選者線程,如果獲得了鎖資源將進(jìn)入 Owner 狀態(tài),釋放鎖資源后進(jìn)入 !Owner 狀態(tài)。被阻塞的線程會(huì)進(jìn)入 WaitSet。
被 synchronized 修飾的同步塊對(duì)一條線程來(lái)說(shuō)是可重入的,并且同步塊在持有鎖的線程執(zhí)行完畢并釋放鎖之前,會(huì)無(wú)條件地阻塞后面其他線程的進(jìn)入。從執(zhí)行成本的角度看,持有鎖是一個(gè)重量級(jí)的操作。在主流 JVM 實(shí)現(xiàn)中,Java 的線程是映射到操作系統(tǒng)的原生內(nèi)核線程之上的,如果要阻塞或喚醒一條線程,則需要操作系統(tǒng)幫忙完成,這就不可避免陷入用戶態(tài)到核心態(tài)的轉(zhuǎn)換中,進(jìn)行這些狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多的處理器時(shí)間。
不公平的原因
所有收到鎖請(qǐng)求的線程首先自旋,如果通過(guò)自旋也沒(méi)有獲取鎖資源將被放入 ContentionList 隊(duì)列,該做法對(duì)于已經(jīng)進(jìn)入隊(duì)列的線程是不公平的。
為了防止 ContentionList 尾部的元素被大量線程進(jìn)行 CAS 訪問(wèn)影響性能,Owner 線程會(huì)在釋放鎖時(shí)將 ContentionList 的部分線程移動(dòng)到 EntryList 并指定某個(gè)線程為 OnDeck 線程,Owner 并沒(méi)有將鎖直接傳遞給 OnDeck 線程而是把鎖競(jìng)爭(zhēng)的權(quán)利交給它,該行為叫做競(jìng)爭(zhēng)切換,犧牲了公平性但提高了性能。
P8:鎖優(yōu)化
JDK 6 對(duì) synchronized 做了很多優(yōu)化,引入了適應(yīng)自旋、鎖消除、鎖粗化、偏向鎖和輕量級(jí)鎖等提高鎖的效率,鎖一共有 4 個(gè)狀態(tài),級(jí)別從低到高依次是:無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但不能降級(jí),這種只能升級(jí)不能降級(jí)的鎖策略是為了提高獲得鎖和釋放鎖的效率。
自旋鎖與自適應(yīng)自旋
互斥同步對(duì)性能最大的影響是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給 JVM 的并發(fā)性能帶來(lái)了很大壓力。同時(shí)虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)也注意到了在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛機(jī)和恢復(fù)線程并不值得。現(xiàn)在絕大多數(shù)的個(gè)人電腦和服務(wù)器都是多核心處理器系統(tǒng),如果物理機(jī)器有一個(gè)以上的處理器或者處理器核心,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程稍等一會(huì),但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán),這項(xiàng)技術(shù)就是所謂的自旋鎖。
自旋鎖在 JDK 1.4中就已經(jīng)引入,只不過(guò)默認(rèn)是關(guān)閉的,在 JDK 6中就已經(jīng)改為默認(rèn)開(kāi)啟了。自旋等待不能代替阻塞,自旋等待本身雖然避免了線程切換的開(kāi)銷,但它要占用處理器時(shí)間,所以如果鎖被占用的時(shí)間很短,自旋的效果就會(huì)非常好,反之只會(huì)白白消耗處理器資源。因此自旋的時(shí)間必須有一定的限度,如果自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程。自旋次數(shù)的默認(rèn)次數(shù)是 10 次。
在 JDK 6 中對(duì)自旋鎖的優(yōu)化,引入了自適應(yīng)自旋。自旋的時(shí)間不再是固定的了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間。如果對(duì)于某個(gè)鎖,自旋很少成功獲得過(guò)鎖,那在以后要獲取這個(gè)鎖時(shí)將有可能之間省略掉自旋過(guò)程,以避免浪費(fèi)處理器資源。有了自適應(yīng)自旋,隨著程序運(yùn)行時(shí)間的增長(zhǎng)以及性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越精準(zhǔn)。
鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼要求同步,但是對(duì)被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。鎖消除的主要判定依據(jù)來(lái)源于逃逸分析的數(shù)據(jù)支持,如果判斷到一段代碼中,在堆上的所有數(shù)據(jù)都不會(huì)逃逸出去被其他線程訪問(wèn)到,那就可以把它們當(dāng)作棧上的數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然就無(wú)須再進(jìn)行。
鎖粗化
原則上我們?cè)诰帉懘a時(shí),總是推薦將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)得實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變少,即使存在鎖競(jìng)爭(zhēng),等待鎖得線程也能盡可能快拿到鎖。
大多數(shù)情況下這種原則是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體之外的,那么即使沒(méi)有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能消耗。
如果虛擬機(jī)探測(cè)到有一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。
偏向鎖
偏向鎖的目的是為了在資源沒(méi)有被多線程競(jìng)爭(zhēng)的情況下盡量減少鎖帶來(lái)的性能開(kāi)銷。輕量級(jí)鎖是在無(wú)競(jìng)爭(zhēng)的情況下使用 CAS 操作消除同步互斥量,偏向鎖是在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都去掉,連 CAS 操作都不做了。
偏向鎖的意思就是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖一直沒(méi)有被其他線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并獲取鎖時(shí),會(huì)在對(duì)象頭和幀棧中的鎖記錄里存儲(chǔ)鎖偏向的線程 ID,以后該線程再進(jìn)入和退出同步代碼塊不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,只需要簡(jiǎn)單地測(cè)試一下對(duì)象頭的"Mark Word"里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。如果測(cè)試成功表示線程已經(jīng)獲得了鎖,如果失敗則需要再測(cè)試一下"Mark Word"中偏向鎖的標(biāo)識(shí)是否設(shè)置成了 1 即表示當(dāng)前使用偏向鎖,如果設(shè)置了就嘗試使用 CAS 將對(duì)象頭的偏向鎖指向當(dāng)前線程,否則使用 CAS 方式競(jìng)爭(zhēng)鎖。
偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。偏向鎖的撤銷需要等待全局安全點(diǎn)即此時(shí)沒(méi)有正在執(zhí)行的字節(jié)碼,它會(huì)首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動(dòng)狀態(tài)則將對(duì)象頭設(shè)為無(wú)鎖狀態(tài)。如果線程還活著,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對(duì)象頭的"Mark Word"要么重新偏向于其他線程,要么恢復(fù)到無(wú)鎖或者標(biāo)記對(duì)象不適合作為偏向鎖,最后喚醒暫停的線程。
輕量級(jí)鎖
輕量級(jí)是相對(duì)于操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機(jī)制就被稱為重量級(jí)鎖。輕量級(jí)鎖并不是用來(lái)代替重量級(jí)鎖的,它設(shè)計(jì)的初衷是在沒(méi)有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
在代碼即將進(jìn)入同步塊的時(shí)候,如果此同步對(duì)象沒(méi)有被鎖定,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝。然后虛擬機(jī)將使用 CAS 操作嘗試把對(duì)象的 Mark Word 更新為指向鎖記錄的指針,如果這個(gè)更新操作成功了,即代表該線程擁有了這個(gè)對(duì)象的鎖,并且鎖標(biāo)志位將轉(zhuǎn)變?yōu)?#34;00",表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
如果這個(gè)更新操作失敗了,那就意味著至少存在一條線程與當(dāng)前線程競(jìng)爭(zhēng)獲取該對(duì)象的鎖。虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是則說(shuō)明當(dāng)前線程以及擁有了這個(gè)對(duì)象的鎖,直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了。如果出現(xiàn)兩條以上的線程爭(zhēng)用同一個(gè)鎖的情況,那輕量級(jí)鎖就不再有效,必須要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)變?yōu)?#34;10",此時(shí)Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖的指針,后面等待鎖的線程也必須進(jìn)入阻塞狀態(tài)。
解鎖操作也同樣是通過(guò) CAS 操作來(lái)進(jìn)行,如果對(duì)象的 Mark Word 仍然指向線程的鎖記錄,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和線程復(fù)制的 Mark Word 替換回來(lái)。假如能夠替換成功,那整個(gè)同步過(guò)程就順利完成了,如果替換失敗,則說(shuō)明有其他線程嘗試過(guò)獲取該鎖,就要在釋放鎖的同時(shí)喚醒被掛起的線程。
偏向鎖、輕量級(jí)鎖和重量級(jí)鎖的區(qū)別
偏向鎖的優(yōu)點(diǎn)是加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距,缺點(diǎn)是如果線程間存在鎖競(jìng)爭(zhēng)會(huì)帶來(lái)額外鎖撤銷的消耗,適用于只有一個(gè)線程訪問(wèn)同步代碼塊的場(chǎng)景。
輕量級(jí)鎖的優(yōu)點(diǎn)是競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度,缺點(diǎn)是如果線程始終得不到鎖會(huì)自旋消耗CPU,適用于追求響應(yīng)時(shí)間和同步代碼塊執(zhí)行非常快的場(chǎng)景。
重量級(jí)鎖的優(yōu)點(diǎn)是線程競(jìng)爭(zhēng)不使用自旋不會(huì)消耗CPU,缺點(diǎn)是線程會(huì)被阻塞,響應(yīng)時(shí)間很慢,適應(yīng)于追求吞吐量、同步代碼塊執(zhí)行較慢的場(chǎng)景。
P9:Lock 接口
自 JDK 5 起 Java 類庫(kù)提供了 juc 并發(fā)包,Lock 接口是 juc 包的頂層接口。基于Lock 接口,用戶能夠以非塊結(jié)構(gòu)來(lái)實(shí)現(xiàn)互斥同步,從而擺脫了語(yǔ)言特性的束縛,改為在類庫(kù)層面去實(shí)現(xiàn)同步。Lock 并未用到 synchronized,而是利用了 volatile 的可見(jiàn)性。
重入鎖 ReentrantLock 是 Lock 接口最常見(jiàn)的一種實(shí)現(xiàn),它與 synchronized 一樣是可重入的,在基本用法上也很相似,不過(guò)它增加了一些高級(jí)功能,主要包括以下三項(xiàng):
- 等待可中斷:是指持有鎖的線程長(zhǎng)期不釋放鎖時(shí),正在等待的線程可以選擇放棄等待而處理其他事情。可中斷特性對(duì)處理執(zhí)行時(shí)間非常長(zhǎng)的同步塊很有幫助。
- 公平鎖:是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)依次獲得鎖,而非公平鎖則不保證這一點(diǎn),在鎖被釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖。synchronized中的鎖是非公平的,ReentrantLock在默認(rèn)情況下也是非公平的,但可以通過(guò)帶有布爾值的構(gòu)造方法要求使用公平鎖。不過(guò)一旦使用了公平鎖,將會(huì)導(dǎo)致性能急劇下降,明顯影響吞吐量。
- 鎖綁定多個(gè)條件:是指一個(gè) ReentrantLock 對(duì)象可以同時(shí)綁定多個(gè) Condition 對(duì)象。在 synchronized中,鎖對(duì)象的 wait 跟它的notify/notifyAll 方法配合可以實(shí)現(xiàn)一個(gè)隱含的條件,如果要和多于一個(gè)的條件關(guān)聯(lián)時(shí)就不得不額外添加一個(gè)鎖,而 ReentrantLock 可以多次調(diào)用 newCondition 方法。
一般優(yōu)先考慮使用synchronized:① synchronized 是 Java 語(yǔ)法層面的同步,足夠清晰和簡(jiǎn)單。② Lock 應(yīng)該確保在 finally 中釋放鎖,否則一旦受同步保護(hù)的代碼塊中拋出異常,則有可能永遠(yuǎn)不會(huì)釋放持有的鎖。這一點(diǎn)必須由程序員自己來(lái)保證,而使用 synchronized 可以由 JVM 來(lái)確保即使出現(xiàn)異常鎖也能被正常釋放。③ 盡管在 JDK 5 時(shí)ReentrantLock 的性能領(lǐng)先于 synchronized,但在 JDK 6 進(jìn)行鎖優(yōu)化之后,二者的性能基本能夠持平。從長(zhǎng)遠(yuǎn)來(lái)看 JVM 更容易針對(duì)synchronized進(jìn)行優(yōu)化,因?yàn)?JVM 可以在線程和對(duì)象的元數(shù)據(jù)中記錄 synchronized 中鎖的相關(guān)信息,而使用Lock的話 JVM 很難得知具體哪些鎖對(duì)象是由特定線程持有的。
ReentrantLock 的可重入實(shí)現(xiàn)
以非公平鎖為例,通過(guò) nonfairTryAcquire 方法獲取鎖,該方法增加了再次獲取同步狀態(tài)的處理邏輯:通過(guò)判斷當(dāng)前線程是否為獲取鎖的線程來(lái)決定獲取操作是否成功,如果是獲取鎖的線程再次請(qǐng)求則將同步狀態(tài)值進(jìn)行增加并返回 true,表示獲取同步狀態(tài)成功。
成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態(tài)值,這就要求 ReentrantLock 在釋放同步狀態(tài)時(shí)減少同步狀態(tài)值。如果該鎖被獲取了 n 次,那么前 n-1 次 tryRelease 方法必須都返回fasle,只有同步狀態(tài)完全釋放了才能返回 true,該方法將同步狀態(tài)是否為 0 作為最終釋放的條件,當(dāng)同步狀態(tài)為 0 時(shí),將占有線程設(shè)置為null,并返回 true 表示釋放成功。
對(duì)于非公平鎖只要 CAS 設(shè)置同步狀態(tài)成功則表示當(dāng)前線程獲取了鎖,而公平鎖則不同。公平鎖使用 tryAcquire 方法,該方法與nonfairTryAcquire 的唯一區(qū)別就是判斷條件中多了對(duì)同步隊(duì)列中當(dāng)前節(jié)點(diǎn)是否有前驅(qū)節(jié)點(diǎn)的判斷,如果該方法返回 true 表示有線程比當(dāng)前線程更早地請(qǐng)求獲取鎖,因此需要等待前驅(qū)線程獲取并釋放鎖之后才能繼續(xù)獲取鎖。、
P10:讀寫鎖
ReentrantLock 是排他鎖,在同一時(shí)刻只允許一個(gè)線程進(jìn)行訪問(wèn),而讀寫鎖在同一時(shí)刻可以允許多個(gè)讀線程訪問(wèn),但是在寫線程訪問(wèn)時(shí),所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護(hù)了一個(gè)讀鎖和一個(gè)寫鎖,通過(guò)分離讀寫鎖使并發(fā)性相比一般的排他鎖有了很大提升。
除了保證寫操作對(duì)讀操作的可見(jiàn)性以及并發(fā)性的提升之外,讀寫鎖能夠簡(jiǎn)化讀寫交互場(chǎng)景的編程方式。只需要在讀操作時(shí)獲取讀鎖,寫操作時(shí)獲取寫鎖即可,當(dāng)寫鎖被獲取時(shí)后續(xù)的讀寫操作都會(huì)被阻塞,寫鎖釋放之后所有操作繼續(xù)執(zhí)行,編程方式相對(duì)于使用等待/通知機(jī)制的實(shí)現(xiàn)方式變得簡(jiǎn)單。
讀寫鎖同樣依賴自定義同步器來(lái)實(shí)現(xiàn)同步功能,而讀寫狀態(tài)就是其同步器的同步狀態(tài)。讀寫鎖的自定義同步器需要在同步狀態(tài)即一個(gè)整形變量上維護(hù)多個(gè)讀線程和一個(gè)寫線程的狀態(tài)。如果在一個(gè) int 型變量上維護(hù)多種狀態(tài),就一定要按位切割使用這個(gè)變量,讀寫鎖將變量切分成了兩個(gè)部分,高 16 位表示讀,低 16 位表示寫。
寫鎖是一個(gè)支持重入的排他鎖,如果當(dāng)前線程已經(jīng)獲得了寫鎖則增加寫狀態(tài),如果當(dāng)前線程在獲取寫鎖時(shí),讀鎖已經(jīng)被獲取或者該線程不是已經(jīng)獲得寫鎖的線程則當(dāng)前線程進(jìn)入等待狀態(tài)。寫鎖的釋放與 ReentrantLock 的釋放過(guò)程類似,每次釋放均減少寫狀態(tài),當(dāng)寫狀態(tài)為 0時(shí)表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續(xù)訪問(wèn)讀寫鎖,同時(shí)前次寫線程的修改對(duì)后續(xù)讀寫線程可見(jiàn)。
讀鎖是一個(gè)支持重入的共享鎖,它能夠被多個(gè)線程同時(shí)獲取,在沒(méi)有其他寫線程訪問(wèn)時(shí),讀鎖總會(huì)被成功地獲取,而所做的只是線程安全地增加讀狀態(tài)。如果當(dāng)前線程已經(jīng)獲取了讀鎖,則增加讀狀態(tài)。如果當(dāng)前線程在獲取讀鎖時(shí),寫鎖已被其他線程獲取則進(jìn)入等待狀態(tài)。讀鎖的每次釋放均會(huì)減少讀狀態(tài),減少的值是(1<<16),讀鎖的每次釋放是線程安全的。
鎖降級(jí)指的是寫鎖降級(jí)成為讀鎖,如果當(dāng)前線程擁有寫鎖,然后將其釋放,最后再獲取讀鎖,這種分段完成的過(guò)程不能稱之為鎖降級(jí)。鎖降級(jí)指的是把持住當(dāng)前擁有的寫鎖,再獲取到讀鎖,隨后釋放先前擁有的寫鎖的過(guò)程。
鎖降級(jí)中讀鎖的獲取是必要的,主要是為了保證數(shù)據(jù)的可見(jiàn)性,如果當(dāng)前線程不獲取讀鎖而是直接釋放寫鎖,假設(shè)此刻另一個(gè)線程 A 獲取了寫鎖修改了數(shù)據(jù),那么當(dāng)前線程是無(wú)法感知線程 A 的數(shù)據(jù)更新的。如果當(dāng)前線程獲取讀鎖,即遵循鎖降級(jí)的步驟,線程 A 將會(huì)被阻塞,直到當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,線程 A 才能獲取寫鎖進(jìn)行數(shù)據(jù)更新。
P11:AQS 隊(duì)列同步器
隊(duì)列同步器是用來(lái)構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架,它使用了一個(gè) volatile int state 變量作為共享資源,如果線程獲取資源失敗,則進(jìn)入同步 FIFO 隊(duì)列中等待;如果獲取成功就執(zhí)行臨界區(qū)代碼,執(zhí)行完釋放資源時(shí),會(huì)通知同步隊(duì)列中的等待線程來(lái)獲取資源后出隊(duì)并執(zhí)行。
使用方式
同步器的主要使用方式是繼承,子類通過(guò)繼承同步器并實(shí)現(xiàn)它的抽象方法來(lái)管理同步狀態(tài),在抽象方法的實(shí)現(xiàn)過(guò)程中免不了要對(duì)同步狀態(tài)進(jìn)行更改,這時(shí)就需要使用同步器提供的3個(gè)方法 getState、setState 和 compareAndSetState 來(lái)進(jìn)行操作,因?yàn)樗鼈兡軌虮WC狀態(tài)的改變是安全的。子類推薦被定義為自定義同步組件的靜態(tài)內(nèi)部類,同步器自身沒(méi)有實(shí)現(xiàn)任何同步接口,它僅僅是定義了若干同步狀態(tài)獲取和釋放的方法來(lái)供自定義同步組件使用,同步器既可以支持獨(dú)占式地獲取同步狀態(tài),也可以支持共享式地獲取同步狀態(tài),這樣就可以方便實(shí)現(xiàn)不同類型地同步組件。
和鎖的關(guān)系
同步器是實(shí)現(xiàn)鎖的關(guān)鍵,在鎖的實(shí)現(xiàn)中聚合同步器,利用同步器實(shí)現(xiàn)鎖的語(yǔ)義。鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實(shí)現(xiàn)細(xì)節(jié);同步器面向的是鎖的實(shí)現(xiàn)者,它簡(jiǎn)化了鎖的實(shí)現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程的排隊(duì)、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實(shí)現(xiàn)者所關(guān)注的領(lǐng)域。
同步隊(duì)列
AQS 中每當(dāng)有新的線程請(qǐng)求資源時(shí),該線程都會(huì)進(jìn)入一個(gè)等待隊(duì)列,只有當(dāng)持有鎖的線程釋放鎖資源后該線程才能持有資源。等待隊(duì)列通過(guò)雙向鏈表實(shí)現(xiàn),線程會(huì)被封裝在鏈表的 Node 節(jié)點(diǎn)中,Node 的等待狀態(tài)包括:CANCELLED 表示線程已取消、SIGNAL 表示線程需要喚醒、CONDITION 表示線程正在等待、PROPAGATE 表示后繼節(jié)點(diǎn)會(huì)傳播喚醒操作,只會(huì)在共享模式下起作用。
兩種模式
獨(dú)占模式表示鎖會(huì)被一個(gè)線程占用,其他線程必須等到持有鎖的線程釋放鎖后才能獲取到鎖繼續(xù)執(zhí)行,在同一時(shí)間內(nèi)只能有一個(gè)線程獲取到這個(gè)鎖,ReentrantLock 就采用的是獨(dú)占模式。
共享模式表示多個(gè)線程獲取同一個(gè)鎖的時(shí)候有可能會(huì)成功,ReadLock 就采用的是共享模式。
獨(dú)占模式通過(guò) acquire 和 release 方法獲取和釋放鎖,共享模式通過(guò) acquireShared 和 releaseShared 方法獲取和釋放鎖。
獨(dú)占式的獲取和釋放流程
在獲取同步狀態(tài)時(shí),同步器調(diào)用 acquire 方法,維護(hù)一個(gè)同步隊(duì)列,使用 tryAcquire 方法安全地獲取線程同步狀態(tài),獲取狀態(tài)失敗的線程會(huì)構(gòu)造同步節(jié)點(diǎn)并通過(guò) addWaiter 方法被加入到同步隊(duì)列的尾部,并在隊(duì)列中進(jìn)行自旋。之后會(huì)調(diào)用 acquireQueued 方法使得該節(jié)點(diǎn)以死循環(huán)的方式獲取同步狀態(tài),如果獲取不到則阻塞節(jié)點(diǎn)中的線程,而被阻塞線程的喚醒主要依靠前驅(qū)節(jié)點(diǎn)的出隊(duì)或阻塞節(jié)點(diǎn)被中斷實(shí)現(xiàn),移出隊(duì)列或停止自旋的條件是前驅(qū)節(jié)點(diǎn)是頭結(jié)點(diǎn)并且成功獲取了同步狀態(tài)。
在釋放同步狀態(tài)時(shí),同步器調(diào)用 tryRelease 方法釋放同步狀態(tài),然后調(diào)用 unparkSuccessor 方法(該方法使用 LockSupport 喚醒處于等待狀態(tài)的線程)喚醒頭節(jié)點(diǎn)的后繼節(jié)點(diǎn),進(jìn)而使后繼節(jié)點(diǎn)重新嘗試獲取同步狀態(tài)。
只有當(dāng)前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn)時(shí)才能夠嘗試獲取同步狀態(tài)原因
頭節(jié)點(diǎn)是成功獲取到同步狀態(tài)的節(jié)點(diǎn),而頭節(jié)點(diǎn)的線程釋放同步狀態(tài)之后,將會(huì)喚醒其后繼節(jié)點(diǎn),后繼節(jié)點(diǎn)的線程被喚醒后需要檢查自己的前驅(qū)節(jié)點(diǎn)是否是頭節(jié)點(diǎn)。
維護(hù)同步隊(duì)列的FIFO原則,節(jié)點(diǎn)和節(jié)點(diǎn)在循環(huán)檢查的過(guò)程中基本不相互通信,而是簡(jiǎn)單地判斷自己的前驅(qū)是否為頭節(jié)點(diǎn),這樣就使得節(jié)點(diǎn)的釋放規(guī)則符合FIFO,并且也便于對(duì)過(guò)早通知的處理(過(guò)早通知是指前驅(qū)節(jié)點(diǎn)不是頭結(jié)點(diǎn)的線程由于中斷而被喚醒)。
共享式的獲取和釋放流程
在獲取同步狀態(tài)時(shí),同步器調(diào)用 acquireShared 方法,該方法調(diào)用 tryAcquireShared 方法嘗試獲取同步狀態(tài),返回值為 int 類型,當(dāng)返回值大于等于 0 時(shí)表示能夠獲取到同步狀態(tài)。因此在共享式獲取鎖的自旋過(guò)程中,成功獲取到同步狀態(tài)并退出自旋的條件就是該方法的返回值大于等于0。
釋放同步狀態(tài)時(shí),調(diào)用 releaseShared 方法,釋放同步狀態(tài)后會(huì)喚醒后續(xù)處于等待狀態(tài)的節(jié)點(diǎn)。對(duì)于能夠支持多線程同時(shí)訪問(wèn)的并發(fā)組件,它和獨(dú)占式的主要區(qū)別在于 tryReleaseShared 方法必須確保同步狀態(tài)安全釋放,一般通過(guò)循環(huán)和 CAS 來(lái)保證,因?yàn)獒尫磐綘顟B(tài)的操作會(huì)同時(shí)來(lái)自多個(gè)線程。
P12:線程
現(xiàn)代操作系統(tǒng)在運(yùn)行一個(gè)程序時(shí)會(huì)為其創(chuàng)建一個(gè)進(jìn)程,而操作系統(tǒng)調(diào)度的最小單位是線程,線程也叫輕量級(jí)進(jìn)程。在一個(gè)進(jìn)程中可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的計(jì)數(shù)器、堆棧和局部變量等屬性,并且能夠訪問(wèn)共享的內(nèi)存變量。處理器在這些線程上告訴切換,讓使用者感覺(jué)到這些線程在同時(shí)執(zhí)行。
生命周期
① NEW:新建狀態(tài),是線程被創(chuàng)建且未啟動(dòng)的狀態(tài),此時(shí)還未調(diào)用 start 方法。
② RUNNABLE:Java 線程將操作系統(tǒng)中的就緒和運(yùn)行兩種狀態(tài)統(tǒng)稱為 RUNNABLE,此時(shí)線程有可能正在等待操作系統(tǒng)分配CPU時(shí)間片,也有可能正在執(zhí)行。
③ BLOCKED:阻塞狀態(tài),可能由于鎖被其他線程占用、調(diào)用了 sleep 或 join 方法、執(zhí)行了 wait 方法等。
④ WAITING:等待狀態(tài),處于該狀態(tài)的線程不會(huì)被分配CPU時(shí)間片,當(dāng)前線程需要等待其他線程通知或中斷。導(dǎo)致線程進(jìn)入該狀態(tài)的方法:無(wú)參數(shù)的 wait 和 join 方法、LockSupport 的 park 方法。
⑤ TIME_WAITING:限期等待狀態(tài),可以在指定時(shí)間內(nèi)自行返回。導(dǎo)致線程進(jìn)入該狀態(tài)的方法:有參數(shù)的 wait 和 join 方法、LockSupport 的 parkNanos 和 parkUntil 方法。
⑥ TERMINATED:終止?fàn)顟B(tài),表示當(dāng)前線程已經(jīng)執(zhí)行完畢或異常退出。
實(shí)現(xiàn)方式
① 繼承 Thread 類并重寫 run 方法。優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,缺點(diǎn)是不符合里氏替換原則,不可以繼承其他類。② 實(shí)現(xiàn) Runnable 接口并重寫 run 方法。優(yōu)點(diǎn)是避免了單繼承的局限性,使編程更加靈活,實(shí)現(xiàn)解耦操作,對(duì)外暴露細(xì)節(jié)少。③實(shí)現(xiàn) Callable 接口并重寫 call 方法。優(yōu)點(diǎn)是可以獲取線程執(zhí)行結(jié)果的返回值,并且可以拋出異常。
方法
① wait 是Object類的方法,調(diào)用 wait 方法的線程會(huì)進(jìn)入 WAITING 狀態(tài),只有等待其他線程的通知或被中斷后才會(huì)解除阻塞,調(diào)用wait方法會(huì)釋放鎖資源。② sleep 是 Thread 類的方法,調(diào)用 sleep 方法會(huì)導(dǎo)致當(dāng)前線程進(jìn)入休眠狀態(tài),與 wait 不同的是該方法不會(huì)釋放鎖資源,進(jìn)入的是 TIMED-WAITING 狀態(tài)。③ yiled 方法會(huì)使當(dāng)前線程讓出 CPU 時(shí)間片給優(yōu)先級(jí)相同或更高的線程,回到 RUNNABLE 狀態(tài),與其他線程一起重新競(jìng)爭(zhēng)CPU時(shí)間片。④ join 方法用于等待其他線程運(yùn)行終止,如果當(dāng)前線程調(diào)用了另一個(gè)線程的 join 方法,則當(dāng)前線程進(jìn)入阻塞狀態(tài),當(dāng)另一個(gè)線程結(jié)束時(shí)當(dāng)前線程才能從阻塞狀態(tài)轉(zhuǎn)為就緒態(tài),等待獲取CPU時(shí)間片。底層使用的是wait,也會(huì)釋放鎖。
守護(hù)線程
守護(hù)線程是一種支持型線程,因?yàn)樗饕挥米鞒绦蛑泻笈_(tái)調(diào)度以及支持性工作,當(dāng) JVM 中不存在非守護(hù)線程時(shí),JVM 將會(huì)退出,可以通過(guò) setDaemon(true) 將線程設(shè)置為daemon線程,但必須在線程啟動(dòng)之前設(shè)置。守護(hù)線程被用于完成支持性工作,但是在 JVM 退出時(shí)守護(hù)線程中的 finally 塊并不一定會(huì)被執(zhí)行,因?yàn)楫?dāng) JVM 中沒(méi)有非守護(hù)線程時(shí)需要立即退出,所有守護(hù)線程都將立即終止,因此不能依靠 finally 確保執(zhí)行關(guān)閉或清理資源的邏輯。
P13:線程間通信
通信是指線程之間以何種機(jī)制來(lái)交換信息,在命令式編程中線程之間的通信機(jī)制有兩種,共享內(nèi)存和消息傳遞。在共享內(nèi)存的并發(fā)模型里線程之間共享程序的公共狀態(tài),通過(guò)寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱式通信。在消息傳遞的并發(fā)模型里線程之間沒(méi)有公共狀態(tài),線程之間必須通過(guò)發(fā)送消息來(lái)顯示通信。Java 并發(fā)采用共享內(nèi)存模型,線程之間的通信總是隱式進(jìn)行,整個(gè)通信過(guò)程對(duì)程序員完全透明。
volatile 和 synchronized 關(guān)鍵字
volatile 可以修飾字段,告知程序任何對(duì)該變量的訪問(wèn)均需要從共享內(nèi)存中獲取,而對(duì)它的改變必須同步刷新回主內(nèi)存,它能保證所有線程對(duì)變量訪問(wèn)的可見(jiàn)性。
synchronized 可以修飾方法或以同步塊的形式使用,它主要確保多個(gè)線程在同一個(gè)時(shí)刻只能有一個(gè)線程處于方法或同步塊中,保證了線程對(duì)變量訪問(wèn)的可見(jiàn)性和排他性。
等待/通知機(jī)制
等待通知機(jī)制是指一個(gè)線程 A 調(diào)用了對(duì)象 O 的 wait 方法進(jìn)入等待狀態(tài),而另一個(gè)線程 B 調(diào)用了對(duì)象 O 的 notify 或 notifyAll 方法,線程 A 收到通知后從 wait 方法返回,進(jìn)而執(zhí)行后序操作。兩個(gè)線程通過(guò)對(duì)象 O 完成交互,對(duì)象上的 wait 和 notify/notifyAll 就如同開(kāi)關(guān)信號(hào),用來(lái)完成等待方和通知方之間的交互工作。
Thread.join
如果一個(gè)線程執(zhí)行了某個(gè)線程的 join 方法,這個(gè)線程就會(huì)阻塞等待執(zhí)行了 join 方法的線程終止之后才返回,這里涉及了等待/通知機(jī)制。join 方法的底層是通過(guò) wait 方法實(shí)現(xiàn)的,當(dāng)線程終止時(shí)會(huì)調(diào)用自身的 notifyAll 方法,通知所有等待在該線程對(duì)象上的線程。
管道 IO 流
管道 IO 流主要用于線程之間的數(shù)據(jù)傳輸,傳輸?shù)拿浇闉閮?nèi)存。PipedOutputStream 和 PipedWriter 是管道輸出流,相當(dāng)于生產(chǎn)者,PipedInputStream 和 PipedReader 是輸入流,相當(dāng)于消費(fèi)者。管道流使用一個(gè)循環(huán)緩沖數(shù)組來(lái)實(shí)現(xiàn),默認(rèn)大小為 1024B。輸入流從這個(gè)緩沖數(shù)組中讀數(shù)據(jù),輸出流往這個(gè)緩沖數(shù)組中寫入數(shù)據(jù)。當(dāng)數(shù)組已滿時(shí),輸出流所在的線程將阻塞;當(dāng)數(shù)組首次為空時(shí),輸入流所在的線程將阻塞。
ThreadLocal
ThreadLocal 是共享變量,但它可以為每個(gè)線程創(chuàng)建單獨(dú)的副本,副本值是線程私有的,互相之間不會(huì)影響。
P14:ConcurrentHashMap
JDK 8 之前
ConcurrentHashMap 用于解決 HashMap 的線程不安全和 HashTable 的并發(fā)效率低下問(wèn)題,HashTable 之所以效率低下是因?yàn)樗芯€程都必須競(jìng)爭(zhēng)同一把鎖,假如容器里有多把鎖,每一把鎖用于鎖容器一部分?jǐn)?shù)據(jù),那么多線程訪問(wèn)容器不同數(shù)據(jù)段的數(shù)據(jù)時(shí)線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而有效提高并發(fā)效率,這就是 ConcurrentHashMap 的鎖分段技術(shù)。首先將數(shù)據(jù)分成 Segment 數(shù)據(jù)段,然后給每一個(gè)數(shù)據(jù)段配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段的數(shù)據(jù)時(shí),其他段的數(shù)據(jù)也能被其他線程訪問(wèn)。
get 操作實(shí)現(xiàn)簡(jiǎn)單高效,先經(jīng)過(guò)一次再散列,然后使用這個(gè)散列值通過(guò)散列運(yùn)算定位到 Segment,再通過(guò)散列算法定位到元素。get 的高效在于這個(gè)過(guò)程不需要加鎖,除非讀到空值才會(huì)加鎖重讀。get 方法里將要使用的共享變量都定義為 volatile 類型,volatile 保證了多線程的可見(jiàn)性,可以多線程讀,但是只能保證單線程寫,在 get 操作里只需要讀所以不用加鎖。
put 操作必須加鎖,put 方法首先定位到 Segment,然后進(jìn)行插入操作,第一步判斷是否需要對(duì) Segment 里的 HashEntry 數(shù)組進(jìn)行擴(kuò)容,第二步定位添加元素的位置,然后將其放入數(shù)組。
size 操作用于統(tǒng)計(jì)元素的數(shù)量,必須統(tǒng)計(jì)每個(gè) Segment 的大小然后求和,在統(tǒng)計(jì)結(jié)果累加的過(guò)程中,之前累加過(guò)的 count 變化的幾率很小,因此 ConcurrentHashMap 的做法是先嘗試兩次通過(guò)不加鎖的方式統(tǒng)計(jì)結(jié)果,如果統(tǒng)計(jì)過(guò)程中容器大小發(fā)生了變化則再通過(guò)加鎖的方式統(tǒng)計(jì)所有 Segment 的大小。判斷容器是否發(fā)生變化是根據(jù) modCount 變量確定的。
JDK 8 開(kāi)始
主要對(duì) JDK 7 的版本做了三點(diǎn)改造:① 取消分段鎖機(jī)制,進(jìn)一步降低沖突概率。② 引入紅黑樹(shù)結(jié)構(gòu),同一個(gè)哈希槽上的元素個(gè)數(shù)超過(guò)一定閾值后,單向鏈表改為紅黑樹(shù)結(jié)構(gòu)。③ 使用了更加優(yōu)化的方式統(tǒng)計(jì)集合內(nèi)的元素?cái)?shù)量。具體優(yōu)化表現(xiàn)在:在 put、resize 和 size 方法中設(shè)計(jì)元素總數(shù)的更新和計(jì)算都避免了鎖,使用 CAS 操作代替。Map 原有的 size 方法最大只能表示到 231-1,ConcurrentHashMap 提供了 mappingCount 方法用來(lái)返回集合內(nèi)元素的數(shù)量,最大可用表示到 263-1。
get 操作同樣不需要同步控制,put 操作時(shí)如果沒(méi)有出現(xiàn)哈希沖突,就使用 CAS 方式來(lái)添加元素,如果出現(xiàn)了哈希沖突就使用 synchronized 加鎖的方式添加元素。
當(dāng)某個(gè)槽內(nèi)的元素個(gè)數(shù)增加到超過(guò) 8 個(gè)且 table 容量大于等于 64 時(shí),由鏈表轉(zhuǎn)為紅黑樹(shù)。當(dāng)某個(gè)槽內(nèi)的元素減少到 6 個(gè)時(shí),由紅黑樹(shù)重新轉(zhuǎn)為鏈表。鏈表轉(zhuǎn)紅黑樹(shù)的過(guò)程,就是把給定順序的元素構(gòu)造成一棵紅黑樹(shù)的過(guò)程,需要注意的是,當(dāng) table 的容量小于 64 時(shí),只會(huì)擴(kuò)容,并不會(huì)把鏈表轉(zhuǎn)為紅黑樹(shù)。在轉(zhuǎn)化過(guò)程中,使用同步塊鎖住當(dāng)前槽的首元素,防止其他線程對(duì)當(dāng)前槽進(jìn)行增刪改操作,轉(zhuǎn)化完成后利用 CAS 替換原有鏈表。由于 TreeNode 節(jié)點(diǎn)也存儲(chǔ)了 next 引用,因此紅黑樹(shù)轉(zhuǎn)為鏈表很簡(jiǎn)單,只需從 TreeBin 的 first 元素開(kāi)始遍歷所有節(jié)點(diǎn),并把節(jié)點(diǎn)從 TreeNode 類型轉(zhuǎn)為 Node 類型即可,當(dāng)構(gòu)造好新鏈表后同樣會(huì)用 CAS 替換紅黑樹(shù)。
P15:CAS 操作
CAS 表示 Compare And Swap,比較并交換,CAS 需要三個(gè)操作數(shù),分別是內(nèi)存位置 V、舊的預(yù)期值 A 和準(zhǔn)備設(shè)置的新值 B。CAS 指令執(zhí)行時(shí),當(dāng)且僅當(dāng) V 符合 A 時(shí),處理器才會(huì)用 B 更新 V 的值,否則它就不執(zhí)行更新。但不管是否更新都會(huì)返回 V 的舊值,這些處理過(guò)程是原子操作,執(zhí)行期間不會(huì)被其他線程打斷。
在 JDK 5 后,Java 類庫(kù)中才開(kāi)始使用 CAS 操作,該操作由 Unsafe 類里的 compareAndSwapInt 等幾個(gè)方法包裝提供。HotSpot 在內(nèi)部對(duì)這些方法做了特殊處理,即時(shí)編譯的結(jié)果是一條平臺(tái)相關(guān)的處理器 CAS 指令。Unsafe 類不是給用戶程序調(diào)用的類,因此在 JDK 9 之前只有 Java 類庫(kù)可以使用 CAS,譬如 juc 包里的 AtomicInteger類中 compareAndSet 等方法都使用了Unsafe 類的 CAS 操作來(lái)實(shí)現(xiàn)。
盡管 CAS 既簡(jiǎn)單又高效,但這種操作無(wú)法涵蓋互斥同步的所有場(chǎng)景,并且 CAS 從語(yǔ)義上來(lái)說(shuō)存在一個(gè)邏輯漏洞:如果 V 初次讀取的時(shí)候是 A,并且在準(zhǔn)備賦值的時(shí)候檢查到它的值仍為 A,這依舊不能說(shuō)明它的值沒(méi)有被其他線程更改過(guò),因?yàn)檫@段時(shí)間內(nèi)假設(shè)它的值先改為了 B 又改回 A,那么 CAS 操作就會(huì)誤認(rèn)為它從來(lái)沒(méi)有被改變過(guò)。這個(gè)漏洞稱為 ABA 問(wèn)題,juc 包提供了一個(gè) AtomicStampedReference,原子更新帶有版本號(hào)的引用類型,它可以通過(guò)控制變量值的版本來(lái)解決 ABA 問(wèn)題。這個(gè)類并不常用,大部分情況下 ABA 問(wèn)題不會(huì)影響程序并發(fā)的正確性,如果需要解決該問(wèn)題,改用傳統(tǒng)的互斥同步可能會(huì)比原子類更高效。
P16:原子操作類
Java 從 JDK 5 開(kāi)始提供了 java.util.concurrent.atomic 包,這個(gè)包中的原子操作類提供了一種用法簡(jiǎn)單、性能高效、線程安全地更新一個(gè)變量的方式。到 JDK 8 該包共有17個(gè)類,依據(jù)作用分為四種:原子更新基本類型類、原子更新數(shù)組類、原子更新引用類以及原子更新字段類,atomic 包里的類基本都是使用 Unsafe 實(shí)現(xiàn)的包裝類。
原子更新基本類型
AtomicInteger 原子更新整形、 AtomicLong 原子更新長(zhǎng)整型、AtomicBoolean 原子更新布爾類型。
getAndIncrement 以原子方式將當(dāng)前的值加 1,首先在 for 死循環(huán)中取得 AtomicInteger 里存儲(chǔ)的數(shù)值,第二步對(duì) AtomicInteger 當(dāng)前的值進(jìn)行加 1 操作,第三步調(diào)用 compareAndSet 方法進(jìn)行原子更新,該操作先檢查當(dāng)前數(shù)值是否等于 expect,如果等于則說(shuō)明當(dāng)前值沒(méi)有被其他線程修改,則將值更新為 next,否則會(huì)更新失敗返回 false,程序會(huì)進(jìn)入 for 循環(huán)重新進(jìn)行 compareAndSet 操作。
atomic 包中只提供了 三種基本類型的原子更新,atomic 包里的類基本都是使用 Unsafe 實(shí)現(xiàn)的,Unsafe 只提供三種 CAS 方法:compareAndSwapInt、compareAndSwapLong 和 compareAndSwapObject,例如原子更新 Boolean 時(shí)是先轉(zhuǎn)成整形再使用 compareAndSwapInt 進(jìn)行 CAS,所以原子更新 char、float、double 也可以用類似思路實(shí)現(xiàn)。
原子更新數(shù)組
AtomicIntegerArray,原子更新整形數(shù)組里的元素、 AtomicLongArray 原子更新長(zhǎng)整型數(shù)組里的元素、 AtomicReferenceArray 原子更新引用類型數(shù)組里的元素。
原子更新引用
AtomicReference 原子更新引用類型、AtomicMarkableReference 原子更新帶有標(biāo)記位的引用類型,可以綁定一個(gè) boolean 類型的標(biāo)記位、 AtomicStampedReference 原子更新帶有版本號(hào)的引用類型,關(guān)聯(lián)一個(gè)整數(shù)值用于原子更新數(shù)據(jù)和數(shù)據(jù)的版本號(hào),可以解決 ABA 問(wèn)題。
原子更新字段
AtomicIntegerFieldUpdater 原子更新整形字段的更新器、 AtomicLongFieldUpdater 原子更新長(zhǎng)整形字段的更新器AtomicReferenceFieldUpdater 原子更新引用類型字段的更新器。
由于原子更新字段類都是抽象類,每次使用的時(shí)候必須使用靜態(tài)方法 newUpdater 創(chuàng)建一個(gè)更新器,并且需要設(shè)置想要更新的類和字段。并且更新類的字段必須使用 public volatile 修飾。
JDK 8 更新的類
DoubleAccumulator 、 LongAccumulator、DoubleAdder、LongAdder、Striped64。
P17:并發(fā)工具類
等待多線程完成的 CountDownLatch
CountDownLatch 是基于執(zhí)行時(shí)間的同步類,允許一個(gè)或多個(gè)線程等待其他線程完成操作,構(gòu)造方法接收一個(gè) int 類型的參數(shù)作為計(jì)數(shù)器,如果要等待 n 個(gè)點(diǎn)就傳入 n。每次調(diào)用 countDown 方法時(shí)計(jì)數(shù)器減 1,await 方法會(huì)阻塞當(dāng)前線程直到計(jì)數(shù)器變?yōu)?,由于 countDown方法可用在任何地方,所以 n 個(gè)點(diǎn)既可以是 n 個(gè)線程也可以是一個(gè)線程里的 n 個(gè)執(zhí)行步驟。
循環(huán)屏障 CyclicBarrier
循環(huán)屏障是基于同步到達(dá)某個(gè)點(diǎn)的信號(hào)量觸發(fā)機(jī)制,作用是讓一組線程到達(dá)一個(gè)屏障時(shí)被阻塞,直到最后一個(gè)線程到達(dá)屏障時(shí),屏障才會(huì)解除,所有被攔截的線程才會(huì)繼續(xù)運(yùn)行。構(gòu)造方法中的參數(shù)表示屏障攔截的線程數(shù)量,每個(gè)線程調(diào)用 await 方法告訴 CyclicBarrier 自己已到達(dá)屏障,然后當(dāng)前線程被阻塞。還支持在構(gòu)造方法中傳入一個(gè) Runable 類型的任務(wù),當(dāng)線程到達(dá)屏障時(shí)會(huì)優(yōu)先執(zhí)行該任務(wù)。適用于多線程計(jì)算數(shù)據(jù),最后合并計(jì)算結(jié)果的應(yīng)用場(chǎng)景。
CountDownLacth 的計(jì)數(shù)器只能用一次,而 CyclicBarrier 的計(jì)數(shù)器可使用 reset 方法重置,所以 CyclicBarrier 能處理更為復(fù)雜的業(yè)務(wù)場(chǎng)景,例如計(jì)算錯(cuò)誤時(shí)可用重置計(jì)數(shù)器重新計(jì)算。
控制并發(fā)線程數(shù)的 Semaphore
信號(hào)量用來(lái)控制同時(shí)訪問(wèn)特定資源的線程數(shù)量,它通過(guò)協(xié)調(diào)各個(gè)線程以保證合理使用公共資源。信號(hào)量可以用于流量控制,特別是公共資源有限的應(yīng)用場(chǎng)景,比如數(shù)據(jù)庫(kù)連接。Semaphore 的構(gòu)造方法參數(shù)接收一個(gè) int 值,表示可用的許可數(shù)量即最大并發(fā)數(shù)。使用acquire 方法獲得一個(gè)許可證,使用 release 方法歸還許可,還可以用 tryAcquire 嘗試獲得許可。
線程間交換數(shù)據(jù)的 Exchanger
交換者是用于線程間協(xié)作的工具類,用于進(jìn)行線程間的數(shù)據(jù)交換。它提供一個(gè)同步點(diǎn),在這個(gè)同步點(diǎn)兩個(gè)線程可以交換彼此的數(shù)據(jù)。這兩個(gè)線程通過(guò) exchange 方法交換數(shù)據(jù),如果第一個(gè)線程先執(zhí)行exchange方法它會(huì)阻塞等待第二個(gè)線程執(zhí)行exchange方法,當(dāng)兩個(gè)線程都到達(dá)同步點(diǎn)時(shí)這兩個(gè)線程就可以交換數(shù)據(jù),將本線程生產(chǎn)出的數(shù)據(jù)傳遞給對(duì)方。應(yīng)用場(chǎng)景包括遺傳算法、校對(duì)工作等。
P18:線程池
作用
① 降低資源消耗,復(fù)用已創(chuàng)建的線程降低開(kāi)銷、控制最大并發(fā)數(shù)。
② 隔離線程環(huán)境,可以配置獨(dú)立線程池,將較慢的線程與較快的隔離開(kāi),避免相互影響。
③ 實(shí)現(xiàn)任務(wù)線程隊(duì)列緩沖策略和拒絕機(jī)制。
④ 實(shí)現(xiàn)某些與時(shí)間相關(guān)的功能,如定時(shí)執(zhí)行、周期執(zhí)行等。
當(dāng)提交一個(gè)新任務(wù)到線程池時(shí)的處理流程
① 核心線程池未滿,創(chuàng)建一個(gè)新的線程執(zhí)行任務(wù),此時(shí) workCount < corePoolSize,這一步需要獲取全局鎖。
② 如果核心線程池已滿,工作隊(duì)列未滿,將線程存儲(chǔ)在工作隊(duì)列,此時(shí) workCount >= corePoolSize。
③ 如果工作隊(duì)列已滿,線程數(shù)小于最大線程數(shù)就創(chuàng)建一個(gè)新線程處理任務(wù),此時(shí) workCount < maximumPoolSize,這一步也需要獲取全局鎖。
④ 如果超過(guò)大小線程數(shù),按照拒絕策略來(lái)處理任務(wù),此時(shí) workCount > maximumPoolSize。
線程池采取這種設(shè)計(jì)思路是為了在執(zhí)行 execute 方法時(shí)盡可能地避免獲取全局鎖,在線程池完成預(yù)熱之后,即當(dāng)前運(yùn)行的線程數(shù)大于等于corePoolSize 時(shí),幾乎所有的 execute 方法都是執(zhí)行步驟 2,不需要獲取全局鎖。
線程池創(chuàng)建線程時(shí),會(huì)將線程封裝成工作線程 Worker,Worker 在執(zhí)行完任務(wù)后還會(huì)循環(huán)獲取工作隊(duì)列中的任務(wù)來(lái)執(zhí)行。線程池中的線程執(zhí)行任務(wù)分為兩種情況:①在 execute 方法中創(chuàng)建一個(gè)線程時(shí)會(huì)讓這個(gè)線程執(zhí)行當(dāng)前任務(wù)。②這個(gè)線程執(zhí)行完任務(wù)之后,就會(huì)反復(fù)從工作隊(duì)列中獲取任務(wù)并執(zhí)行。
可以使用 execute 和 submit 方法向線程池提交任務(wù)。execute 用于提交不需要返回值的任務(wù),所以無(wú)法判斷任務(wù)是否被線程池執(zhí)行成功了。submit 用于提交需要返回值的任務(wù),線程池會(huì)返回一個(gè) Future 類型的對(duì)象,通過(guò)該對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過(guò)該對(duì)象的 get 方法獲取返回值,get 會(huì)阻塞當(dāng)前線程直到任務(wù)完成,帶超時(shí)參數(shù)的 get 方法會(huì)在阻塞當(dāng)前線程一段時(shí)間后立即返回,這時(shí)任務(wù)可能還沒(méi)有完成。
創(chuàng)建線程池
可以通過(guò) Executors 的靜態(tài)工廠方法創(chuàng)建線程池,其核心方法有五個(gè):
① newFixedThreadPool,固定大小的線程池,輸入的參數(shù)既是核心線程數(shù)也是最大線程數(shù),不存在空閑線程,因此 keepAliveTime 等于 0。該線程池使用的工作隊(duì)列是無(wú)界阻塞隊(duì)列 LinkedBlockingQueue,適用于為了滿足資源管理的需求,而需要限制當(dāng)前線程數(shù)量的應(yīng)用場(chǎng)景,適用于負(fù)載比較重的服務(wù)器。
② newSingleThreadExecutor,使用單線程的線程池,相當(dāng)于單線程串行執(zhí)行所有任務(wù),適用于需要保證順序執(zhí)行任務(wù)的應(yīng)用場(chǎng)景。
③ newCachedThreadPool,maximumPoolSize 設(shè)置為 Integer 最大值,是高度可伸縮的線程池。該線程池使用的工作隊(duì)列是沒(méi)有容量的 SynchronousQueue,如果主線程提交任務(wù)的速度高于線程處理的速度,線程池會(huì)不斷創(chuàng)建新線程,極端情況下會(huì)創(chuàng)建過(guò)多線程而耗盡CPU 和內(nèi)存資源。適用于執(zhí)行很多短期異步任務(wù)的小程序或者負(fù)載較輕的服務(wù)器。
④ newScheduledThreadPool:線程數(shù)最大至 Integer 最大值,存在 OOM 風(fēng)險(xiǎn)。支持定期及周期性任務(wù)執(zhí)行,適用于需要多個(gè)后臺(tái)線程執(zhí)行周期任務(wù),同時(shí)需要限制后臺(tái)線程數(shù)量的應(yīng)用場(chǎng)景。相比 Timer 更加安全,功能更強(qiáng)大,與 newCachedThreadPool 的區(qū)別是不回收工作線程。
⑤ newWorkStealingPool:JDK 8 引入,創(chuàng)建持有足夠線程的線程池支持給定的并行度,并通過(guò)使用多個(gè)隊(duì)列減少競(jìng)爭(zhēng)。
線程池的參數(shù)
① corePoolSize:常駐核心線程數(shù),如果為 0,當(dāng)執(zhí)行完任務(wù)沒(méi)有任何請(qǐng)求時(shí)會(huì)消耗線程池;如果大于 0,即使本地任務(wù)執(zhí)行完,核心線程也不會(huì)被銷毀。該值設(shè)置過(guò)大會(huì)浪費(fèi)資源,過(guò)小會(huì)導(dǎo)致線程的頻繁創(chuàng)建與銷毀。
② maximumPoolSize:線程池能夠容納同時(shí)執(zhí)行的線程最大數(shù),必須大于等于 1,如果與核心線程數(shù)設(shè)置相同代表固定大小線程池。
③ keepAliveTime:線程空閑時(shí)間,線程空閑時(shí)間達(dá)到該值后會(huì)被銷毀,直到只剩下 corePoolSize 個(gè)線程為止,避免浪費(fèi)內(nèi)存資源。
④ unit:keepAliveTime 的時(shí)間單位。
⑤ workQueue:工作隊(duì)列,當(dāng)線程請(qǐng)求數(shù)大于等于 corePoolSize 時(shí)線程會(huì)進(jìn)入阻塞隊(duì)列。
⑥ threadFactory:線程工廠,用來(lái)生產(chǎn)一組相同任務(wù)的線程。可以給線程命名,有利于分析錯(cuò)誤。
⑦ handler:拒絕策略,默認(rèn)策略下使用 AbortPolicy 丟棄任務(wù)并拋出異常,CallerRunsPolicy 表示重新嘗試提交該任務(wù),DiscardOldestPolicy 表示拋棄隊(duì)列里等待最久的任務(wù)并把當(dāng)前任務(wù)加入隊(duì)列,DiscardPolicy 表示直接拋棄當(dāng)前任務(wù)但不拋出異常。
線程池的狀態(tài)
① RUNNING:接受新的任務(wù),處理等待隊(duì)列中的任務(wù)。線程池被一旦被創(chuàng)建,就處于RUNNING狀態(tài)。
② SHUTDOWN:不接受新的任務(wù)提交,但是會(huì)繼續(xù)處理等待隊(duì)列中的任務(wù)。對(duì)應(yīng)線程池的 shutdown方法。
③ STOP:不接受新的任務(wù)提交,不再處理等待隊(duì)列中的任務(wù),中斷正在執(zhí)行任務(wù)的線程。對(duì)應(yīng) shutdownNow 方法。
④ TIDYING:當(dāng)線程池在 SHUTDOWN 狀態(tài)下,阻塞隊(duì)列為空并且線程池中執(zhí)行的任務(wù)也為空時(shí),就會(huì)變?yōu)?TIDYING。當(dāng)線程池在STOP 狀態(tài)下,線程池中執(zhí)行的任務(wù)為空時(shí),也會(huì)變?yōu)?TIDYING。
⑤ TERMINATED:線程池處于 TIDYING 狀態(tài)時(shí),執(zhí)行完 terminated 方法后就會(huì)進(jìn)入終止?fàn)顟B(tài)。
關(guān)閉線程池
可以通過(guò)調(diào)用 shutdown 或 shutdownNow 方法關(guān)閉線程池,原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的 interrupt 方法中斷線程,所以無(wú)法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無(wú)法終止。區(qū)別是 shutdownNow 首先將線程池的狀態(tài)設(shè)為 STOP,然后嘗試停止所有正在執(zhí)行或暫停任務(wù)的線程,并返回等待執(zhí)行任務(wù)的列表,而 shutdown 只是將線程池的狀態(tài)設(shè)為 SHUTDOWN,然后中斷所有沒(méi)有正在執(zhí)行任務(wù)的線程。通常調(diào)用 shutdown 來(lái)關(guān)閉線程池,如果任務(wù)不一定要執(zhí)行完則可以調(diào)用 shutdownNow。
合理設(shè)置線程池
首先可以從以下角度分析:①任務(wù)的性質(zhì):CPU密集型任務(wù)、IO密集型任務(wù)和混合型任務(wù)。②任務(wù)的優(yōu)先級(jí):高、中和低。③任務(wù)的執(zhí)行時(shí)間:長(zhǎng)、中和短。④任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫(kù)連接。
性質(zhì)不同的任務(wù)可以用不同規(guī)模的線程池分開(kāi)處理,CPU密集型任務(wù)應(yīng)配置盡可能小的線程,如配置 Ncpu+1 個(gè)線程的線程池。由于IO密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),則應(yīng)配置盡可能多的線程,如 2 * Ncpu。混合型的任務(wù),如果可以拆分,將其拆分為一個(gè) CPU 密集型任務(wù)和一個(gè) IO 密集型任務(wù),只要這兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不是太大那么分解后的吞吐量將高于串行執(zhí)行的吞吐量,如果相差太大則沒(méi)必要分解。
優(yōu)先級(jí)不同的任務(wù)可以使用優(yōu)先級(jí)隊(duì)列 PriorityBlockingQueue 處理。
執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池處理,或者使用優(yōu)先級(jí)隊(duì)列讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行。
依賴數(shù)據(jù)庫(kù)連接池的任務(wù),由于線程提交 SQL 后需要等待數(shù)據(jù)庫(kù)返回的結(jié)果,等待的時(shí)間越長(zhǎng) CPU 空閑的時(shí)間就越長(zhǎng),因此線程數(shù)應(yīng)該盡可能地設(shè)置大一些提高CPU的利用率。
建議使用有界隊(duì)列,能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力,可以根據(jù)需要設(shè)置的稍微大一些。
P19:阻塞隊(duì)列
阻塞隊(duì)列支持阻塞的插入和移除,當(dāng)隊(duì)列滿時(shí),阻塞插入元素的線程直到隊(duì)列不滿。當(dāng)隊(duì)列為空時(shí),獲取元素的線程會(huì)被阻塞直到隊(duì)列非空。阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場(chǎng)景,阻塞隊(duì)列就是生產(chǎn)者用來(lái)存放元素,消費(fèi)者用來(lái)獲取元素的容器。
Java 中的阻塞隊(duì)列
ArrayBlockingQueue,由數(shù)組組成的有界阻塞隊(duì)列,默認(rèn)情況下不保證線程公平,有可能先阻塞的線程最后才訪問(wèn)隊(duì)列。
LinkedBlockingQueue,由鏈表結(jié)構(gòu)組成的有界阻塞隊(duì)列,隊(duì)列的默認(rèn)和最大長(zhǎng)度為 Integer 的最大值。
PriorityBlockingQueue,支持優(yōu)先級(jí)排序的無(wú)界阻塞隊(duì)列,默認(rèn)情況下元素按照順序升序排序。可以自定義 compareTo 方法指定元素排序規(guī)則,或者初始化時(shí)指定 Comparator 對(duì)元素排序,不能保證同優(yōu)先級(jí)元素的順序。
DelayQueue,支持延時(shí)獲取元素的無(wú)界阻塞隊(duì)列,使用優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn)。創(chuàng)建元素時(shí)可以指定多久才能從隊(duì)列中獲取當(dāng)前元素,只有延遲期滿時(shí)才能從隊(duì)列中獲取元素,適用于緩存系統(tǒng)和定時(shí)任務(wù)調(diào)度。
SynchronousQueue,不存儲(chǔ)元素的阻塞隊(duì)列,每一個(gè) put 操作必須等待一個(gè) take 操作。默認(rèn)使用非公平策略,也支持公平策略,適用于傳遞性場(chǎng)景,吞吐量高于 ArrayBlockingQueue 和 LinkedBlockingQueue。
LinkedTransferQueue,由鏈表組成的無(wú)界阻塞隊(duì)列,相對(duì)于其他阻塞隊(duì)列多了 tryTransfer 和 transfer 方法。transfe方法:如果當(dāng)前有消費(fèi)者正在等待接收元素,可以把生產(chǎn)者傳入的元素立刻傳輸給消費(fèi)者,如果沒(méi)有消費(fèi)者等待接收元素,會(huì)將元素放在隊(duì)列的尾節(jié)點(diǎn)并等到該元素被消費(fèi)者消費(fèi)了才返回。tryTransfer 方法用來(lái)試探生產(chǎn)者傳入的元素能否直接傳給消費(fèi)者,如果沒(méi)有消費(fèi)者等待接收元素則返回 false,和transfer 的區(qū)別是無(wú)論消費(fèi)者是否消費(fèi)都會(huì)立即返回。
LinkedBlockingDeque,由鏈表組成的雙向阻塞隊(duì)列,可以從隊(duì)列的兩端插入和移出元素,在多線程同時(shí)入隊(duì)時(shí)減少了競(jìng)爭(zhēng)。
實(shí)現(xiàn)原理
使用通知模式實(shí)現(xiàn),當(dāng)生產(chǎn)者往滿的隊(duì)列里添加元素時(shí)會(huì)阻塞生產(chǎn)者,當(dāng)消費(fèi)者消費(fèi)了一個(gè)隊(duì)列中的元素后,會(huì)通知生產(chǎn)者當(dāng)前隊(duì)列可用。例如 JDK 中的 ArrayBlockingQueue 使用了 Condition 實(shí)現(xiàn)。當(dāng)往隊(duì)列里插入一個(gè)元素,如果隊(duì)列不可用,那么阻塞生產(chǎn)者主要通過(guò)LockSupport 的 park 方法實(shí)現(xiàn),park 在不同的操作系統(tǒng)中使用不同的方式實(shí)現(xiàn),在 Linux 下使用的是系統(tǒng)方法 pthread_cond_wait 實(shí)現(xiàn)。
P20:ThreadLoacl
ThreadLoacl 是線程變量,主要用于一個(gè)線程內(nèi),跨類、方法傳遞數(shù)據(jù)。ThreadLoacl 有一個(gè)靜態(tài)的內(nèi)部類 ThreadLocalMap,ThreadLocalMap 的 Key 是 ThreadLocal 對(duì)象,值是 Entry 類型的元素,Entry 中只有一個(gè) Object 類型的 vaule 值。ThreadLocal 是線程共享的,但是 ThreadLocalMap 是每個(gè)線程私有的。ThreadLocal 主要有 set、get 和 remove 三個(gè)方法。
set 方法
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}首先獲取當(dāng)前線程,然后再獲取當(dāng)前線程對(duì)應(yīng)的 ThreadLocalMap 類型的對(duì)象 map。如果 map 存在就直接設(shè)置值,key 是當(dāng)前的 ThreadLocal 對(duì)象,value 就是傳入的參數(shù)。如果 map 不存在就通過(guò) createMap 方法為當(dāng)前線程創(chuàng)建一個(gè) ThreadLocalMap 對(duì)象再設(shè)置值。
get 方法
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}首先獲取當(dāng)前線程,然后再獲取當(dāng)前線程對(duì)應(yīng)的 ThreadLocalMap 類型的對(duì)象 map。如果 map 存在就以當(dāng)前 ThreadLocal 對(duì)象作為 key 獲取 Entry 類型的對(duì)象 e,如果 e 存在就返回它的 value 屬性。如果 e 不存在或者 map 不存在,就調(diào)用 setInitialValue 方法先為當(dāng)前線程創(chuàng)建一個(gè) ThreadLocalMap 對(duì)象然后返回默認(rèn)的初始值 null。
remove 方法
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}首先還是通過(guò)當(dāng)前線程獲取其對(duì)應(yīng)的 ThreadLocalMap 類型的對(duì)象 m,如果 m 不為空,就解除 ThreadLocal 這個(gè) key 及其對(duì)應(yīng)的 value 值的聯(lián)系。
存在的問(wèn)題
線程復(fù)用會(huì)產(chǎn)生臟數(shù)據(jù),由于線程池會(huì)重用 Thread 對(duì)象,因此與 Thread 綁定的 ThreadLocal 也會(huì)被重用。如果沒(méi)有調(diào)用 remove 清理與線程相關(guān)的 ThreadLocal 信息,那么假如下一個(gè)線程沒(méi)有調(diào)用 set 設(shè)置初始值就可能 get 到重用的線程信息。
ThreadLocal 還存在內(nèi)存泄漏的問(wèn)題,由于 ThreadLocal 是弱引用,但 Entry 的 value 是強(qiáng)引用,因此當(dāng) ThreadLocal 被垃圾回收后,value 依舊不會(huì)被釋放。因此需要及時(shí)調(diào)用 remove 方法進(jìn)行清理操作。
Spring 10
P1:Spring 框架
Spring 是分層的企業(yè)級(jí)應(yīng)用輕量級(jí)開(kāi)源框架,以 IoC 和 AOP為內(nèi)核。Spring 可以降低企業(yè)級(jí)應(yīng)用開(kāi)發(fā)的復(fù)雜性,對(duì)此主要采取了四個(gè)關(guān)鍵策略:基于 POJO 的輕量級(jí)和最小侵入性編程、通過(guò)依賴注入和面向接口實(shí)現(xiàn)松耦合、基于切面和慣性進(jìn)行聲明式編程、通過(guò)切面和模板減少樣板式代碼。
好處
降低代碼耦合度、簡(jiǎn)化開(kāi)發(fā)。通過(guò) Spring 提供的 IoC 容器可以將對(duì)象間的依賴關(guān)系交由 Spring 進(jìn)行控制,避免硬編碼所造成的過(guò)度程序耦合。用戶也不必再為單例模式類、屬性文件解析等這些底層的需求編寫代碼,可以更專注于上層的應(yīng)用。
AOP 編程以及聲明式事務(wù)的支持。通過(guò) Spring 的 AOP 功能可以方便進(jìn)行面向切面的編程,通過(guò)聲明式事務(wù)可以靈活進(jìn)行事務(wù)管理,提高開(kāi)發(fā)效率和質(zhì)量。
方便程序的測(cè)試和集成各種框架。可以用非容器依賴的編程方式進(jìn)行幾乎所有的測(cè)試工作,可以降低各種框架的使用難度,提供了對(duì) Mybatis 和 Hibernate 等框架的直接支持。
降低了 JavaEE API 的使用難度。Spring 對(duì) JDBC、JavaMail、遠(yuǎn)程調(diào)用等 API 進(jìn)行了封裝,使這些 API 的使用難度大幅降低。
核心容器
核心容器由 spring-beans、spring-core、spring-context 和 spring-expression 四個(gè)模塊組成。
spring-beans 和 spring-core 模塊是 Spring 的核心模塊,包括了控制反轉(zhuǎn)和依賴注入。BeanFactory 使用控制反轉(zhuǎn)對(duì)應(yīng)用程序的配置和依賴性規(guī)范與實(shí)際的應(yīng)用代碼進(jìn)行分離,BeanFactory 實(shí)例化后并不會(huì)自動(dòng)實(shí)例化 Bean,只有當(dāng) Bean 被使用時(shí)才會(huì)對(duì)其進(jìn)行實(shí)例化與依賴關(guān)系的裝配。
spring-context 模塊構(gòu)架于核心模塊之上,擴(kuò)展了 BeanFactory,為它添加了 Bean 的生命周期控制、框架事件體系及資源透明化加載等功能。ApplicationConext 是該模塊的核心接口,它是 BeanFactory 的子接口,它實(shí)例化后會(huì)自動(dòng)對(duì)所有單例 Bean 進(jìn)行實(shí)例化與依賴關(guān)系的裝配,使之處于待用狀態(tài)。
spring-expression 是 EL 語(yǔ)言的擴(kuò)展模塊,可以查詢、管理運(yùn)行中的對(duì)象,同時(shí)也可以方便地調(diào)用對(duì)象方法,以及操作數(shù)組、集合等。
P2:IoC 控制反轉(zhuǎn)
IoC 即控制反轉(zhuǎn),是一種給予應(yīng)用程序中目標(biāo)組件更多控制的設(shè)計(jì)范式,簡(jiǎn)單來(lái)說(shuō)就是把原來(lái)代碼里需要實(shí)現(xiàn)的對(duì)象創(chuàng)建、依賴反轉(zhuǎn)給容器來(lái)幫忙實(shí)現(xiàn),需要?jiǎng)?chuàng)建一個(gè)容器并且需要一種描述來(lái)讓容器知道要?jiǎng)?chuàng)建的對(duì)象之間的關(guān)系,在 Spring 框架中管理對(duì)象及其依賴關(guān)系是通過(guò) Spring 的 IoC 容器實(shí)現(xiàn)的,IoC 的作用是降低代碼耦合度。
IoC 的實(shí)現(xiàn)方式有依賴注入和依賴查找,由于依賴查找使用的很少,因此 IoC 也叫做依賴注入。依賴注入指對(duì)象被動(dòng)地接受依賴類而不用自己主動(dòng)去找,對(duì)象不是從容器中查找它依賴的類,而是在容器實(shí)例化對(duì)象時(shí)主動(dòng)將它依賴的類注入給它。假設(shè)一個(gè) Car 類需要一個(gè) Engine 的對(duì)象,那么一般需要需要手動(dòng) new 一個(gè) Engine,利用 IoC 就只需要定義一個(gè)私有的 Engine 類型的成員變量,容器會(huì)在運(yùn)行時(shí)自動(dòng)創(chuàng)建一個(gè) Engine 的實(shí)例對(duì)象并將引用自動(dòng)注入給成員變量。
基于 XML 的容器初始化
當(dāng)創(chuàng)建一個(gè) ClassPathXmlApplicationContext 時(shí),構(gòu)造方法做了兩件事:首先調(diào)用父容器的構(gòu)造方法為容器設(shè)置好 Bean 資源加載器,然后調(diào)用父類的 setConfigLocations 方法設(shè)置 Bean 配置信息的定位路徑。
ClassPathXmlApplicationContext 通過(guò)調(diào)用父類 AbstractApplicationContext 的 refresh 方法啟動(dòng)整個(gè) IoC 容器對(duì) Bean 定義的載入過(guò)程,refresh 是一個(gè)模板方法,規(guī)定了 IoC 容器的啟動(dòng)流程。refresh 方法的主要作用是:在創(chuàng)建 IoC 容器之前如果已有容器存在,需要把已有的容器銷毀和關(guān)閉,以保證在 refresh 方法之后使用的是新創(chuàng)建的 IoC 容器。
容器創(chuàng)建后通過(guò) loadBeanDefinitions 方法加載 Bean 配置資源,該方法會(huì)做兩件事:首先調(diào)用資源加載器的方法獲取要加載的資源,其次真正執(zhí)行加載功能,由子類 XmlBeanDefinitionReader 實(shí)現(xiàn)。在加載資源時(shí),首先會(huì)解析配置文件路徑,讀取配置文件的內(nèi)容,然后通過(guò) XML 解析器將 Bean 配置信息轉(zhuǎn)換成文檔對(duì)象,之后再按照 Spring Bean 的定義規(guī)則對(duì)文檔對(duì)象進(jìn)行解析。
Spring IoC 容器中注冊(cè)解析的 Bean 信息存放在一個(gè) HashMap 集合中,key 是 String 字符串,值是 BeanDefinition,在注冊(cè)過(guò)程中需要使用 synchronized 同步塊保證線程安全。當(dāng) Bean 配置信息中配置的 Bean 被解析后且被注冊(cè)到 IoC 容器中,初始化就算真正完成了,Bean 定義信息已經(jīng)可以使用,并且可以被檢索。Spring IoC 容器的作用就是對(duì)這些注冊(cè)的 Bean 定義信息進(jìn)行處理和維護(hù),注冊(cè)的 Bean 定義信息是控制反轉(zhuǎn)和依賴注入的基礎(chǔ)。
基于注解的容器初始化
Spring 對(duì)注解的處理分為兩種方式:① 直接將注解 Bean 注冊(cè)到容器中,可以在初始化容器時(shí)注冊(cè),也可以在容器創(chuàng)建之后手動(dòng)注冊(cè),然后刷新容器使其對(duì)注冊(cè)的注解 Bean 進(jìn)行處理。② 通過(guò)掃描指定的包及其子包的所有類處理,在初始化注解容器時(shí)指定要自動(dòng)掃描的路徑。
P3:DI 依賴注入
可注入的數(shù)據(jù)類型
基本數(shù)據(jù)類型和 String、集合類型、Bean 類型。
實(shí)現(xiàn)方式
構(gòu)造方法注入:IoC Service Provider 會(huì)檢查被注入對(duì)象的構(gòu)造方法,取得它所需要的依賴對(duì)象列表,進(jìn)而為其注入相應(yīng)的對(duì)象。這種方法的優(yōu)點(diǎn)是在對(duì)象構(gòu)造完成后就處于就緒狀態(tài),可以馬上使用。缺點(diǎn)是當(dāng)依賴對(duì)象較多時(shí),構(gòu)造方法的參數(shù)列表會(huì)比較長(zhǎng),構(gòu)造方法無(wú)法被繼承,無(wú)法設(shè)置默認(rèn)值。對(duì)于非必需的依賴處理可能需要引入多個(gè)構(gòu)造方法,參數(shù)數(shù)量的變動(dòng)可能會(huì)造成維護(hù)的困難。
setter 方法注入:當(dāng)前對(duì)象只需要為其依賴對(duì)象對(duì)應(yīng)的屬性添加 setter 方法,就可以通過(guò) setter 方法將依賴對(duì)象注入到被依賴對(duì)象中。setter 方法注入在描述性上要比構(gòu)造方法注入強(qiáng),并且可以被繼承,允許設(shè)置默認(rèn)值。缺點(diǎn)是無(wú)法在對(duì)象構(gòu)造完成后馬上進(jìn)入就緒狀態(tài)。
接口注入:必須實(shí)現(xiàn)某個(gè)接口,這個(gè)接口提供一個(gè)方法來(lái)為其注入依賴對(duì)象。使用較少,因?yàn)樗鼜?qiáng)制要求被注入對(duì)象實(shí)現(xiàn)不必要的接口,侵入性強(qiáng)。
相關(guān)注解
@Autowired:自動(dòng)按類型注入,如果有多個(gè)匹配則按照指定 Bean 的 id 查找,查找不到會(huì)報(bào)錯(cuò)。
@Qualifier:在自動(dòng)按照類型注入的基礎(chǔ)上再按照 Bean 的 id 注入,給變量注入時(shí)必須搭配 @Autowired,給方法注入時(shí)可單獨(dú)使用。
@Resource :直接按照 Bean 的 id 注入,只能注入 Bean 類型。
@Value :用于注入基本數(shù)據(jù)類型和 String 類型。
依賴注入的過(guò)程
getBean 方法是獲取 Bean 實(shí)例的方法,該方法會(huì)調(diào)用 doGetBean 方法,doGetBean 真正實(shí)現(xiàn)向 IoC 容器獲取 Bean 的功能,也是觸發(fā)依賴注入的地方。如果 Bean 定義為單例模式,容器在創(chuàng)建之前先從緩存中查找以確保整個(gè)容器中只存在一個(gè)實(shí)例對(duì)象。如果 Bean 定義為原型模式,則容器每次都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
具體創(chuàng)建 Bean 實(shí)例對(duì)象的過(guò)程由 ObjectFactory 的 createBean 方法完成,該方法主要通過(guò) createBeanInstance 方法生成 Bean 包含的 Java 對(duì)象實(shí)例和 populateBean 方法對(duì) Bean 屬性的依賴注入進(jìn)行處理。
在 createBeanInstance 方法中根據(jù)指定的初始化策略,通過(guò)簡(jiǎn)單工廠、工廠方法或容器的自動(dòng)裝配特性生成 Java 實(shí)例對(duì)象,對(duì)工廠方法和自動(dòng)裝配特性的 Bean,調(diào)用相應(yīng)的工廠方法或參數(shù)匹配的構(gòu)造方法即可完成實(shí)例化對(duì)象的工作,但最常用的默認(rèn)無(wú)參構(gòu)造方法需要使用 JDK 的反射或 CGLib 來(lái)進(jìn)行初始化。
在 populateBean 方法中,注入過(guò)程主要分為兩種情況:① 屬性值類型不需要強(qiáng)制轉(zhuǎn)換時(shí),不需要解析屬性值,直接進(jìn)行依賴注入。② 屬性值類型需要強(qiáng)制轉(zhuǎn)換時(shí),首先需要解析屬性值,然后對(duì)解析后的屬性值進(jìn)行依賴注入。依賴注入的過(guò)程就是將 Bean 對(duì)象實(shí)例設(shè)置到它所依賴的 Bean 對(duì)象屬性上,真正的依賴注入是通過(guò) setPropertyValues 方法實(shí)現(xiàn)的,該方法使用了委派模式。
BeanWrapperImpl 類負(fù)責(zé)對(duì)容器完成初始化的 Bean 實(shí)例對(duì)象進(jìn)行屬性的依賴注入,對(duì)于非集合類型的屬性,大量使用 JDK 的反射機(jī)制,通過(guò)屬性的 getter 方法獲取指定屬性注入前的值,同時(shí)調(diào)用屬性的 setter 方法為屬性設(shè)置注入后的值。對(duì)于集合類型的屬性,將屬性值解析為目標(biāo)類型的集合后直接賦值給屬性。
當(dāng) Spring IoC 容器對(duì) Bean 定義資源的定位、載入、解析和依賴注入全部完成后,就不再需要我們手動(dòng)創(chuàng)建所需的對(duì)象,Spring IoC 容器會(huì)自動(dòng)為我們創(chuàng)建對(duì)象并且注入好相關(guān)依賴。
P4:Bean 對(duì)象
生命周期
在 IoC 容器的初始化過(guò)程中會(huì)對(duì) Bean 定義完成資源定位,加載讀取配置并解析,最后將解析的 Bean 信息放在一個(gè) HashMap 集合中。當(dāng) IoC 容器初始化完成后,會(huì)進(jìn)行對(duì) Bean 實(shí)例的創(chuàng)建和依賴注入過(guò)程,注入對(duì)象依賴的各種屬性值,在初始化時(shí)可以指定自定義的初始化方法。經(jīng)過(guò)這一系列初始化操作后 Bean 達(dá)到可用狀態(tài),接下來(lái)就可以使用 Bean 了,當(dāng)使用完成后會(huì)調(diào)用 destroy 方法進(jìn)行銷毀,此時(shí)也可以指定自定義的銷毀方法,最終 Bean 被銷毀且從容器中移除。
指定 Bean 初始化和銷毀的方法:
XML 方式通過(guò)配置 bean 標(biāo)簽中的 init-Method 和 destory-Method 指定自定義初始化和銷毀方法。
注解方式通過(guò) @PreConstruct 和 @PostConstruct 注解指定自定義初始化和銷毀方法。
作用范圍
通過(guò) scope 屬性指定 bean 的作用范圍,包括:① singleton:單例模式,是默認(rèn)作用域,不管收到多少 Bean 請(qǐng)求每個(gè)容器中只有一個(gè)唯一的 Bean 實(shí)例。② prototype:原型模式,和 singleton 相反,每次 Bean 請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的實(shí)例。③ request:每次 HTTP 請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的 Bean 并把它放到 request 域中,在請(qǐng)求完成后 Bean 會(huì)失效并被垃圾收集器回收。④ session:和 request 類似,確保每個(gè) session 中有一個(gè) Bean 實(shí)例,session 過(guò)期后 bean 會(huì)隨之失效。⑤ global session:當(dāng)應(yīng)用部署在 Portlet 容器中時(shí),如果想讓所有 Portlet 共用全局存儲(chǔ)變量,那么這個(gè)變量需要存儲(chǔ)在 global session 中。
創(chuàng)建方式
XML
通過(guò)默認(rèn)無(wú)參構(gòu)造方法,只需要指明 bean 標(biāo)簽中的 id 和 class 屬性,如果沒(méi)有無(wú)參構(gòu)造方法會(huì)報(bào)錯(cuò)。
使用靜態(tài)工廠方法,通過(guò) bean 標(biāo)簽中的 class 屬性指明靜態(tài)工廠,factory-method 屬性指明靜態(tài)工廠方法。
使用實(shí)例工廠方法,通過(guò) bean 標(biāo)簽中的 factory-bean 屬性指明實(shí)例工廠,factory-method 屬性指明實(shí)例工廠方法。
注解
@Component 把當(dāng)前類對(duì)象存入 Spring 容器中,相當(dāng)于在 xml 中配置一個(gè) bean 標(biāo)簽。value 屬性指定 bean 的 id,默認(rèn)使用當(dāng)前類的首字母小寫的類名。
@Controller,@Service,@Repository 三個(gè)注解都是 @Component 的衍生注解,作用及屬性都是一模一樣的。只是提供了更加明確語(yǔ)義,@Controller 用于表現(xiàn)層,@Service用于業(yè)務(wù)層,@Repository用于持久層。如果注解中有且只有一個(gè) value 屬性要賦值時(shí)可以省略 value。
如果想將第三方的類變成組件又沒(méi)有源代碼,也就沒(méi)辦法使用 @Component 進(jìn)行自動(dòng)配置,這種時(shí)候就要使用 @Bean 注解。被 @Bean 注解的方法返回值是一個(gè)對(duì)象,將會(huì)實(shí)例化,配置和初始化一個(gè)新對(duì)象并返回,這個(gè)對(duì)象由 Spring 的 IoC 容器管理。name 屬性用于給當(dāng)前 @Bean 注解方法創(chuàng)建的對(duì)象指定一個(gè)名稱,即 bean 的 id。當(dāng)使用注解配置方法時(shí),如果方法有參數(shù),Spring 會(huì)去容器查找是否有可用 bean對(duì)象,查找方式和 @Autowired 一樣。
@Configuration 用于指定當(dāng)前類是一個(gè) spring 配置類,當(dāng)創(chuàng)建容器時(shí)會(huì)從該類上加載注解,value 屬性用于指定配置類的字節(jié)碼。
@ComponentScan 用于指定 Spring 在初始化容器時(shí)要掃描的包。basePackages 屬性用于指定要掃描的包。
@PropertySource 用于加載 .properties 文件中的配置。value 屬性用于指定文件位置,如果是在類路徑下需要加上 classpath。
@Import 用于導(dǎo)入其他配置類,在引入其他配置類時(shí)可以不用再寫 @Configuration 注解。有 @Import 的是父配置類,引入的是子配置類。value 屬性用于指定其他配置類的字節(jié)碼。
BeanFactory、FactoryBean 和 ApplicationContext 的區(qū)別
BeanFactory 是一個(gè) Bean 工廠,使用了簡(jiǎn)單工廠模式,是 Spring IoC 容器最頂級(jí)的接口,可以理解為含有 Bean 集合的工廠類,它的作用是管理 Bean,包括實(shí)例化、定位、配置應(yīng)用程序中的對(duì)象及建立這些對(duì)象之間的依賴。BeanFactory 實(shí)例化后并不會(huì)自動(dòng)實(shí)例化 Bean,只有當(dāng) Bean 被使用時(shí)才會(huì)對(duì)其進(jìn)行實(shí)例化與依賴關(guān)系的裝配,屬于延遲加載,適合多例模式。
FactoryBean 是一個(gè)工廠 Bean,使用了工廠方法模式,作用是生產(chǎn)其他 Bean 實(shí)例,可以通過(guò)實(shí)現(xiàn)該接口,提供一個(gè)工廠方法來(lái)自定義實(shí)例化 Bean 的邏輯。FactoryBean 接口由 BeanFactory 中配置的對(duì)象實(shí)現(xiàn),這些對(duì)象本身就是用于創(chuàng)建對(duì)象的工廠,如果一個(gè) Bean 實(shí)現(xiàn)了這個(gè)接口,那么它就是創(chuàng)建對(duì)象的工廠 Bean,而不是 Bean 實(shí)例本身。
ApplicationConext 是 BeanFactory 的子接口,擴(kuò)展了 BeanFactory 的功能,提供了支持國(guó)際化的文本消息,統(tǒng)一的資源文件讀取方式,事件傳播以及應(yīng)用層的特別配置等。容器會(huì)在初始化時(shí)對(duì)配置的 Bean 進(jìn)行預(yù)實(shí)例化,Bean 的依賴注入在容器初始化時(shí)就已經(jīng)完成,屬于立即加載,適合單例模式,一般推薦使用 ApplicationContext。
P5:AOP 面向切面編程
概念和原理
AOP 即面向切面編程,簡(jiǎn)單地說(shuō)就是將代碼中重復(fù)的部分抽取出來(lái),在需要執(zhí)行的時(shí)候使用動(dòng)態(tài)代理的技術(shù),在不修改源碼的基礎(chǔ)上對(duì)方法進(jìn)行增強(qiáng)。優(yōu)點(diǎn)是可以減少代碼的冗余,提高開(kāi)發(fā)效率,維護(hù)方便。
Spring 會(huì)根據(jù)類是否實(shí)現(xiàn)了接口來(lái)判斷動(dòng)態(tài)代理的方式,如果實(shí)現(xiàn)了接口會(huì)使用 JDK 的動(dòng)態(tài)代理,核心是 InvocationHandler 接口和 Proxy 類,如果沒(méi)有實(shí)現(xiàn)接口會(huì)使用 CGLib 動(dòng)態(tài)代理,CGLib 是在運(yùn)行時(shí)動(dòng)態(tài)生成某個(gè)類的子類,如果某一個(gè)類被標(biāo)記為 final,是不能使用 CGLib 動(dòng)態(tài)代理的。
JDK 動(dòng)態(tài)代理主要通過(guò)重組字節(jié)碼實(shí)現(xiàn),首先獲得被代理對(duì)象的引用和所有接口,生成新的類必須實(shí)現(xiàn)被代理類的所有接口,動(dòng)態(tài)生成Java 代碼后編譯新生成的 .class 文件并重新加載到 JVM 運(yùn)行。JDK 代理直接寫 Class 字節(jié)碼,CGLib 是采用 ASM 框架寫字節(jié)碼,生成代理類的效率低。但是 CGLib 調(diào)用方法的效率高,因?yàn)?JDK 使用反射調(diào)用方法,CGLib 使用 FastClass 機(jī)制為代理類和被代理類各生成一個(gè)類,這個(gè)類會(huì)為代理類或被代理類的方法生成一個(gè) index,這個(gè) index 可以作為參數(shù)直接定位要調(diào)用的方法。
常用場(chǎng)景包括權(quán)限認(rèn)證、自動(dòng)緩存、錯(cuò)誤處理、日志、調(diào)試和事務(wù)等。
相關(guān)注解
@Aspect:聲明被注解的類是一個(gè)切面 Bean。
@Before:前置通知,指在某個(gè)連接點(diǎn)之前執(zhí)行的通知。
@After:后置通知,指某個(gè)連接點(diǎn)退出時(shí)執(zhí)行的通知(不論正常返回還是異常退出)。
@AfterReturning:返回后通知,指某連接點(diǎn)正常完成之后執(zhí)行的通知,返回值使用returning屬性接收。
@AfterThrowing:異常通知,指方法拋出異常導(dǎo)致退出時(shí)執(zhí)行的通知,和@AfterReturning只會(huì)有一個(gè)執(zhí)行,異常使用throwing屬性接收。
相關(guān)術(shù)語(yǔ)
Aspect:切面,一個(gè)關(guān)注點(diǎn)的模塊化,這個(gè)關(guān)注點(diǎn)可能會(huì)橫切多個(gè)對(duì)象。
Joinpoint:連接點(diǎn),程序執(zhí)行過(guò)程中的某一行為,即業(yè)務(wù)層中的所有方法。。
Advice:通知,指切面對(duì)于某個(gè)連接點(diǎn)所產(chǎn)生的動(dòng)作,包括前置通知、后置通知、返回后通知、異常通知和環(huán)繞通知。
Pointcut:切入點(diǎn),指被攔截的連接點(diǎn),切入點(diǎn)一定是連接點(diǎn),但連接點(diǎn)不一定是切入點(diǎn)。
Proxy:代理,Spring AOP 中有 JDK 動(dòng)態(tài)代理和 CGLib 代理,目標(biāo)對(duì)象實(shí)現(xiàn)了接口時(shí)采用 JDK 動(dòng)態(tài)代理,反之采用 CGLib 代理。
Target:代理的目標(biāo)對(duì)象,指一個(gè)或多個(gè)切面所通知的對(duì)象。
Weaving :織入,指把增強(qiáng)應(yīng)用到目標(biāo)對(duì)象來(lái)創(chuàng)建代理對(duì)象的過(guò)程。
AOP 的過(guò)程
Spring AOP 是由 BeanPostProcessor 后置處理器開(kāi)始的,這個(gè)后置處理器是一個(gè)監(jiān)聽(tīng)器,可以監(jiān)聽(tīng)容器觸發(fā)的 Bean 生命周期事件,向容器注冊(cè)后置處理器以后,容器中管理的 Bean 就具備了接收 IoC 容器回調(diào)事件的能力。BeanPostProcessor 的調(diào)用發(fā)生在 Spring IoC 容器完成 Bean 實(shí)例對(duì)象的創(chuàng)建和屬性的依賴注入之后,為 Bean 對(duì)象添加后置處理器的入口是 initializeBean 方法。
Spring 中 JDK 動(dòng)態(tài)代理生通過(guò) JdkDynamicAopProxy 調(diào)用 Proxy 的 newInstance 方法來(lái)生成代理類,JdkDynamicAopProxy 也實(shí)現(xiàn)了 InvocationHandler 接口,invoke 方法的具體邏輯是先獲取應(yīng)用到此方法上的攔截器鏈,如果有攔截器則創(chuàng)建 MethodInvocation 并調(diào)用其 proceed 方法,否則直接反射調(diào)用目標(biāo)方法。因此 Spring AOP 對(duì)目標(biāo)對(duì)象的增強(qiáng)是通過(guò)攔截器實(shí)現(xiàn)的。
P6:Spring MVC 核心組件
DispatcherServlet:SpringMVC 中的前端控制器,是整個(gè)流程控制的核心,負(fù)責(zé)接收請(qǐng)求并轉(zhuǎn)發(fā)給對(duì)應(yīng)的處理組件。
Handler:處理器,完成具體業(yè)務(wù)邏輯,相當(dāng)于 Servlet 或 Action。
HandlerMapping:完成URL 到 Controller映射的組件,DispatcherServlet 接收到請(qǐng)求之后,通過(guò) HandlerMapping 將不同的請(qǐng)求映射到不同的 Handler。
HandlerInterceptor:處理器攔截器,是一個(gè)接口,如果需要完成一些攔截處理,可以實(shí)現(xiàn)該接口。
HandlerExecutionChain:處理器執(zhí)行鏈,包括兩部分內(nèi)容:Handler 和 HandlerInterceptor。
HandlerAdapter:處理器適配器,Handler執(zhí)行業(yè)務(wù)方法前需要進(jìn)行一系列操作,包括表單數(shù)據(jù)驗(yàn)證、數(shù)據(jù)類型轉(zhuǎn)換、將表單數(shù)據(jù)封裝到JavaBean等,這些操作都由 HandlerAdapter 完成。DispatcherServlet 通過(guò) HandlerAdapter 來(lái)執(zhí)行不同的 Handler。
ModelAndView:裝載了模型數(shù)據(jù)和視圖信息,作為 Handler 的處理結(jié)果返回給 DispatcherServlet。
ViewResolver:視圖解析器,DispatcherServlet 通過(guò)它將邏輯視圖解析為物理視圖,最終將渲染的結(jié)果響應(yīng)給客戶端。
P7:Spring MVC 處理流程
Web 容器啟動(dòng)時(shí)會(huì)通知 Spring 初始化容器,加載 Bean 的定義信息并初始化所有單例 Bean,然后遍歷容器中的 Bean,獲取每一個(gè) Controller 中的所有方法訪問(wèn)的 URL,將 URL 和對(duì)應(yīng)的 Controller 保存到一個(gè) Map 集合中。
所有的請(qǐng)求會(huì)轉(zhuǎn)發(fā)給 DispatcherServlet 前端處理器處理,DispatcherServlet 會(huì)請(qǐng)求 HandlerMapping 找出容器中被 @Controler 注解修飾的 Bean 以及被 @RequestMapping 修飾的方法和類,生成 Handler 和 HandlerInterceptor 并以一個(gè) HandlerExcutionChain 處理器執(zhí)行鏈的形式返回。
之后 DispatcherServlet 使用 Handler 找到對(duì)應(yīng)的 HandlerApapter,通過(guò) HandlerApapter 調(diào)用 Handler 的方法,將請(qǐng)求參數(shù)綁定到方法的形參上,執(zhí)行方法處理請(qǐng)求并得到 ModelAndView。
最后 DispatcherServlet 根據(jù)使用 ViewResolver 試圖解析器對(duì)得到的 ModelAndView 邏輯視圖進(jìn)行解析得到 View 物理視圖,然后對(duì)視圖渲染,將數(shù)據(jù)填充到視圖中并返回給客戶端。
注解
@Controller:在類定義處添加,將類交給IoC容器管理。
@RequtestMapping:將URL請(qǐng)求和業(yè)務(wù)方法映射起來(lái),在類和方法定義上都可以添加該注解。value 屬性指定URL請(qǐng)求的實(shí)際地址,是默認(rèn)值。method 屬性限制請(qǐng)求的方法類型,包括GET、POST、PUT、DELETE等。如果沒(méi)有使用指定的請(qǐng)求方法請(qǐng)求URL,會(huì)報(bào)405 Method Not Allowed 錯(cuò)誤。params 屬性限制必須提供的參數(shù),如果沒(méi)有會(huì)報(bào)錯(cuò)。
@RequestParam:如果 Controller 方法的形參和 URL 參數(shù)名一致可以不添加注解,如果不一致可以使用該注解綁定。value 屬性表示HTTP請(qǐng)求中的參數(shù)名。required 屬性設(shè)置參數(shù)是否必要,默認(rèn)false。defaultValue 屬性指定沒(méi)有給參數(shù)賦值時(shí)的默認(rèn)值。
@PathVariable:Spring MVC 也支持 RESTful 風(fēng)格的 URL,通過(guò) @PathVariable 完成請(qǐng)求參數(shù)與形參的綁定。
P8:Spring Data JPA 框架
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 規(guī)范的基礎(chǔ)上封裝的一套 JPA 應(yīng)用框架,可使開(kāi)發(fā)者用極簡(jiǎn)的代碼實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)和操作。它提供了包括增刪改查等在內(nèi)的常用功能,且易于擴(kuò)展,可以極大提高開(kāi)發(fā)效率。
ORM 即 Object-Relational Mapping ,表示對(duì)象關(guān)系映射,映射的不只是對(duì)象的值還有對(duì)象之間的關(guān)系,通過(guò) ORM 就可以把對(duì)象映射到關(guān)系型數(shù)據(jù)庫(kù)中。操作實(shí)體類就相當(dāng)于操作數(shù)據(jù)庫(kù)表,可以不再重點(diǎn)關(guān)注 SQL 語(yǔ)句。
使用時(shí)只需要持久層接口繼承 JpaRepository 即可,泛型參數(shù)列表中第一個(gè)參數(shù)是實(shí)體類類型,第二個(gè)參數(shù)是主鍵類型。運(yùn)行時(shí)通過(guò) JdkDynamicAopProxy 的 invoke 方法創(chuàng)建了一個(gè)動(dòng)態(tài)代理對(duì)象 SimpleJpaRepository,SimpleJpaRepository 中封裝了 JPA 的操作,通過(guò) hibernate(封裝了JDBC)完成數(shù)據(jù)庫(kù)操作。
注解
@Entity:表明當(dāng)前類是一個(gè)實(shí)體類。
@Table :關(guān)聯(lián)實(shí)體類和數(shù)據(jù)庫(kù)表。
@Column :關(guān)聯(lián)實(shí)體類屬性和數(shù)據(jù)庫(kù)表中字段。
@Id :聲明當(dāng)前屬性為數(shù)據(jù)庫(kù)表主鍵對(duì)應(yīng)的屬性。
@GeneratedValue: 配置主鍵生成策略。
@OneToMany :配置一對(duì)多關(guān)系,mappedBy 屬性值為主表實(shí)體類在從表實(shí)體類中對(duì)應(yīng)的屬性名。
@ManyToOne :配置多對(duì)一關(guān)系,targetEntity 屬性值為主表對(duì)應(yīng)實(shí)體類的字節(jié)碼。
@JoinColumn:配置外鍵關(guān)系,name 屬性值為外鍵名稱,referencedColumnName 屬性值為主表主鍵名稱。
對(duì)象導(dǎo)航查詢
通過(guò) get 方法查詢一個(gè)對(duì)象的同時(shí),通過(guò)此對(duì)象可以查詢它的關(guān)聯(lián)對(duì)象。
對(duì)象導(dǎo)航查詢一到多默認(rèn)使用延遲加載的形式, 關(guān)聯(lián)對(duì)象是集合,因此使用立即加載可能浪費(fèi)資源。
對(duì)象導(dǎo)航查詢多到一默認(rèn)使用立即加載的形式, 關(guān)聯(lián)對(duì)象是一個(gè)對(duì)象,因此使用立即加載。
如果要改變加載方式,在實(shí)體類注解配置加上 fetch 屬性即可,LAZY 表示延遲加載,EAGER 表示立即加載。
P9:Mybatis 框架
Mybatis 是一個(gè)實(shí)現(xiàn)了數(shù)據(jù)持久化的 ORM 框架,簡(jiǎn)單理解就是對(duì) JDBC 進(jìn)行了封裝。
優(yōu)點(diǎn)
相比 JDBC 減少了大量代碼量,減少冗余代碼。
使用靈活,SQL 語(yǔ)句寫在 XML 里,從程序代碼中徹底分離,降低了耦合度,便于管理。
提供 XML 標(biāo)簽,支持編寫動(dòng)態(tài) SQL 語(yǔ)句。
提供映射標(biāo)簽,支持對(duì)象與數(shù)據(jù)庫(kù)的 ORM 字段映射關(guān)系。
缺點(diǎn)
SQL 語(yǔ)句編寫工作量較大,尤其是字段和關(guān)聯(lián)表多時(shí)。
SQL 語(yǔ)句依賴于數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)移植性差,不能隨意更換數(shù)據(jù)庫(kù)。
映射文件標(biāo)簽
select、insert、update、delete 標(biāo)簽分別對(duì)應(yīng)查詢、添加、更新、刪除操作。
parameterType 屬性表示參數(shù)的數(shù)據(jù)類型,包括基本數(shù)據(jù)類型和對(duì)應(yīng)的包裝類型、String 和 Java Bean 類型,當(dāng)有多個(gè)參數(shù)時(shí)可以使用 #{argn} 的形式表示第 n 個(gè)參數(shù)。除了基本數(shù)據(jù)類型都要以全限定類名的形式指定參數(shù)類型。
resultType 表示返回的結(jié)果類型,包括基本數(shù)據(jù)類型和對(duì)應(yīng)的包裝類型、String 和 Java Bean 類型。還可以使用把返回結(jié)果封裝為復(fù)雜類型的 resultMap 。
緩存
使用緩存可以減少程序和數(shù)據(jù)庫(kù)交互的次數(shù),從而提高程序的運(yùn)行效率。第一次查詢后會(huì)自動(dòng)將結(jié)果保存到緩存中,下一次查詢時(shí)直接從緩存中返回結(jié)果無(wú)需再次查詢數(shù)據(jù)庫(kù)。
- 一級(jí)緩存
SqlSession 級(jí)別,默認(rèn)開(kāi)啟且不能關(guān)閉。
操作數(shù)據(jù)庫(kù)時(shí)需要?jiǎng)?chuàng)建 SqlSession 對(duì)象,對(duì)象中有一個(gè) HashMap 存儲(chǔ)緩存數(shù)據(jù),不同 SqlSession 之間緩存數(shù)據(jù)區(qū)域互不影響。
一級(jí)緩存的作用域是 SqlSession 范圍的,在同一個(gè) SqlSession 中執(zhí)行兩次相同的 SQL 語(yǔ)句時(shí),第一次執(zhí)行完畢會(huì)將結(jié)果保存在緩存中,第二次查詢直接從緩存中獲取。
如果 SqlSession 執(zhí)行了 DML 操作(insert、update、delete),Mybatis 必須將緩存清空以保證數(shù)據(jù)的有效性。 - 二級(jí)緩存
Mapper 級(jí)別,默認(rèn)關(guān)閉。
使用二級(jí)緩存時(shí)多個(gè) SqlSession 使用同一個(gè) Mapper 的 SQL 語(yǔ)句操作數(shù)據(jù)庫(kù),得到的數(shù)據(jù)會(huì)存在二級(jí)緩存區(qū),同樣使用 HashMap 進(jìn)行數(shù)據(jù)存儲(chǔ),相比于一級(jí)緩存,二級(jí)緩存范圍更大,多個(gè) SqlSession 可以共用二級(jí)緩存,作用域是 Mapper 的同一個(gè) namespace,不同 SqlSession 兩次執(zhí)行相同的 namespace 下的 SQL 語(yǔ)句,參數(shù)也相等,則第一次執(zhí)行成功后會(huì)將數(shù)據(jù)保存在二級(jí)緩存中,第二次可直接從二級(jí)緩存中取出數(shù)據(jù)。
要使用二級(jí)緩存,先在在全局配置文件中配置: <!-- 開(kāi)啟二級(jí)緩存 --><setting name="cacheEnabled" value="true"/>再在對(duì)應(yīng)的映射文件中配置一個(gè) cache 標(biāo)簽即可。<cache/>
P10:Spring Cloud 框架
單體應(yīng)用存在的問(wèn)題
隨著業(yè)務(wù)發(fā)展,開(kāi)發(fā)越來(lái)越復(fù)雜。
修改、新增某個(gè)功能,需要對(duì)整個(gè)系統(tǒng)進(jìn)行測(cè)試、重新部署。
一個(gè)模塊出現(xiàn)問(wèn)題,可能導(dǎo)致整個(gè)系統(tǒng)崩潰。
多個(gè)開(kāi)發(fā)團(tuán)隊(duì)同時(shí)對(duì)數(shù)據(jù)進(jìn)行管理,容易產(chǎn)生安全漏洞。
各個(gè)模塊使用同一種技術(shù)開(kāi)發(fā),各個(gè)模塊很難根據(jù)實(shí)際情況選擇更合適的技術(shù)框架,局限性很大。
分布式和集群的區(qū)別
集群:一臺(tái)服務(wù)器無(wú)法負(fù)荷高并發(fā)的數(shù)據(jù)訪問(wèn)量,就設(shè)置多臺(tái)服務(wù)器一起分擔(dān)壓力,是在物理層面解決問(wèn)題。
分布式:將一個(gè)復(fù)雜的問(wèn)題拆分成若干簡(jiǎn)單的小問(wèn)題,將一個(gè)大型的項(xiàng)目架構(gòu)拆分成若干個(gè)微服務(wù)來(lái)協(xié)同完成,在軟件設(shè)計(jì)層面解決問(wèn)題。
微服務(wù)的優(yōu)點(diǎn)
各個(gè)服務(wù)的開(kāi)發(fā)、測(cè)試、部署都相互獨(dú)立,用戶服務(wù)可以拆分為獨(dú)立服務(wù),如果用戶量很大,可以很容易對(duì)其實(shí)現(xiàn)負(fù)載。
當(dāng)新需求出現(xiàn)時(shí),使用微服務(wù)不再需要考慮各方面的問(wèn)題,例如兼容性、影響度等。
使用微服務(wù)拆分項(xiàng)目后,各個(gè)服務(wù)之間消除了很多限制,只需要保證對(duì)外提供的接口正常可用,而不限制語(yǔ)言和框架等選擇。
服務(wù)治理 Eureka
服務(wù)治理的核心由三部分組成:服務(wù)提供者、服務(wù)消費(fèi)者、注冊(cè)中心。
服務(wù)注冊(cè):在分布式系統(tǒng)架構(gòu)中,每個(gè)微服務(wù)在啟動(dòng)時(shí),將自己的信息存儲(chǔ)在注冊(cè)中心。
服務(wù)發(fā)現(xiàn):服務(wù)消費(fèi)者從注冊(cè)中心獲取服務(wù)提供者的網(wǎng)絡(luò)信息,通過(guò)該信息調(diào)用服務(wù)。
Spring Cloud 的服務(wù)治理使用 Eureka 實(shí)現(xiàn),Eureka 是 Netflix 開(kāi)源的基于 REST 的服務(wù)治理解決方案,Spring Cloud 集成了 Eureka,提供服務(wù)注冊(cè)和服務(wù)發(fā)現(xiàn)的功能,可以和基于 Spring Boot 搭建的微服務(wù)應(yīng)用輕松完成整合,將 Eureka 二次封裝為 Spring Cloud Eureka。Eureka Server 是注冊(cè)中心,所有要進(jìn)行注冊(cè)的微服務(wù)通過(guò) Eureka Client 連接到 Eureka Server 完成注冊(cè)。
服務(wù)網(wǎng)關(guān) Zuul
Spring Cloud 集成了 Zuul 組件,實(shí)現(xiàn)服務(wù)網(wǎng)關(guān)。Zuul 是 Netflix 提供的一個(gè)開(kāi)源的 API 網(wǎng)關(guān)服務(wù)器,是客戶端和網(wǎng)站后端所有請(qǐng)求的中間層,對(duì)外開(kāi)放一個(gè) API,將所有請(qǐng)求導(dǎo)入統(tǒng)一的入口,屏蔽了服務(wù)端的具體實(shí)現(xiàn)邏輯,可以實(shí)現(xiàn)方向代理功能,在網(wǎng)關(guān)內(nèi)部實(shí)現(xiàn)動(dòng)態(tài)路由、身份認(rèn)證、IP過(guò)濾、數(shù)據(jù)監(jiān)控等。
負(fù)載均衡 Ribbon
Spring Cloud Ribbon 是一個(gè)負(fù)載均衡的解決方案,Ribbon 是 Netflix 發(fā)布的均衡負(fù)載器,Spring Cloud Ribbon是基于 Netflix Ribbon 實(shí)現(xiàn)的,是一個(gè)用于對(duì) HTTP 請(qǐng)求進(jìn)行控制的負(fù)載均衡客戶端。
在注冊(cè)中心對(duì) Ribbon 進(jìn)行注冊(cè)之后,Ribbon 就可以基于某種負(fù)載均衡算法(輪循、隨機(jī)、加權(quán)輪詢、加權(quán)隨機(jī)等)自動(dòng)幫助服務(wù)消費(fèi)者調(diào)用接口,開(kāi)發(fā)者也可以根據(jù)具體需求自定義 Ribbon 負(fù)載均衡算法。實(shí)際開(kāi)發(fā)中 Spring Clooud Ribbon 需要結(jié)合 Spring Cloud Eureka 使用,Eureka 提供所有可以調(diào)用的服務(wù)提供者列表,Ribbon 基于特定的負(fù)載均衡算法從這些服務(wù)提供者中選擇要調(diào)用的具體實(shí)例。
聲明式接口調(diào)用 Feign
Feign 與 Ribbon 一樣也是 Netflix 提供的,Feign 是一個(gè)聲明式、模板化的 Web Service 客戶端,簡(jiǎn)化了開(kāi)發(fā)者編寫 Web 服務(wù)客戶端的操作,開(kāi)發(fā)者可以通過(guò)簡(jiǎn)單的接口和注解來(lái)調(diào)用 HTTP API,Spring Cloud Feign 整合了 Ribbon 和 Hystrix,具有可插拔、基于注解、負(fù)載均衡、服務(wù)熔斷等一系列功能。
相比于 Ribbon + RestTemplate 的方式,Feign 可以大大簡(jiǎn)化代碼開(kāi)發(fā),支持多種注解,包括 Feign 注解、JAX-RS 注解、Spring MVC 注解等。RestTemplate 是 Spring 框架提供的基于 REST 的服務(wù)組件,底層是對(duì) HTTP 請(qǐng)求及響應(yīng)進(jìn)行了封裝,提供了很多訪問(wèn) REST 服務(wù)的方法,可以簡(jiǎn)化代碼開(kāi)發(fā)。
服務(wù)熔斷 Hystrix
熔斷器的作用是在不改變各個(gè)微服務(wù)調(diào)用關(guān)系的前提下,針對(duì)錯(cuò)誤情況進(jìn)行預(yù)先處理。
設(shè)計(jì)原則:服務(wù)隔離機(jī)制、服務(wù)降級(jí)機(jī)制、熔斷機(jī)制、提供實(shí)時(shí)監(jiān)控和報(bào)警功能和提供實(shí)時(shí)配置修改功能
Hystrix 數(shù)據(jù)監(jiān)控需要結(jié)合 Spring Boot Actuator 使用,Actuator 提供了對(duì)服務(wù)的數(shù)據(jù)監(jiān)控、數(shù)據(jù)統(tǒng)計(jì),可以通過(guò) hystirx-stream 節(jié)點(diǎn)獲取監(jiān)控的請(qǐng)求數(shù)據(jù),同時(shí)提供了可視化監(jiān)控界面。
服務(wù)配置 Config
Spring Cloud Config 通過(guò)服務(wù)端可以為多個(gè)客戶端提供配置服務(wù),既可以將配置文件存儲(chǔ)在本地,也可以將配置文件存儲(chǔ)在遠(yuǎn)程的 Git 倉(cāng)庫(kù),創(chuàng)建 Config Server,通過(guò)它管理所有的配置文件。
服務(wù)跟蹤 Zipkin
Spring Cloud Zipkin 是一個(gè)可以采集并跟蹤分布式系統(tǒng)中請(qǐng)求數(shù)據(jù)的組件,讓開(kāi)發(fā)者更直觀地監(jiān)控到請(qǐng)求在各個(gè)微服務(wù)耗費(fèi)的時(shí)間,Zipkin 包括兩部分 Zipkin Server 和 Zipkin Client。
MySQL 15
P1:邏輯架構(gòu)
第一層是服務(wù)器層,主要提供連接處理、授權(quán)認(rèn)證、安全等功能,該層的服務(wù)不是 MySQL 獨(dú)有的,大多數(shù)基于網(wǎng)絡(luò)的 C/S 服務(wù)都有類似架構(gòu)。
第二層實(shí)現(xiàn)了 MySQL 核心服務(wù)功能,包括查詢解析、分析、優(yōu)化、緩存以及日期和時(shí)間等所有內(nèi)置函數(shù),所有跨存儲(chǔ)引擎的功能都在這一層實(shí)現(xiàn),例如存儲(chǔ)過(guò)程、觸發(fā)器、視圖等。
第三層是存儲(chǔ)引擎層,存儲(chǔ)引擎負(fù)責(zé) MySQL 中數(shù)據(jù)的存儲(chǔ)和提取。服務(wù)器通過(guò) API 與存儲(chǔ)引擎通信,這些接口屏蔽了不同存儲(chǔ)引擎的差異,使得差異對(duì)上層查詢過(guò)程透明。除了會(huì)解析外鍵定義的 InnoDB 外,存儲(chǔ)引擎不會(huì)解析 SQL,不同存儲(chǔ)引擎之間也不會(huì)相互通信,只是簡(jiǎn)單響應(yīng)上層服務(wù)器請(qǐng)求。
P2:鎖
當(dāng)有多個(gè)查詢需要在同一時(shí)刻修改數(shù)據(jù)時(shí)就會(huì)產(chǎn)生并發(fā)控制的問(wèn)題,MySQL 在兩個(gè)層面進(jìn)行并發(fā)控制:服務(wù)器層與存儲(chǔ)引擎層。
讀寫鎖
在處理并發(fā)讀或?qū)憰r(shí),可以通過(guò)實(shí)現(xiàn)一個(gè)由兩種類型組成的鎖系統(tǒng)來(lái)解決問(wèn)題。這兩種類型的鎖通常被稱為共享鎖和排它鎖,也叫讀鎖和寫鎖。讀鎖是共享的,相互不阻塞,多個(gè)客戶在同一時(shí)刻可以同時(shí)讀取同一個(gè)資源而不相互干擾。寫鎖則是排他的,也就是說(shuō)一個(gè)寫鎖會(huì)阻塞其他的寫鎖和讀鎖,確保在給定時(shí)間內(nèi)只有一個(gè)用戶能執(zhí)行寫入并防止其他用戶讀取正在寫入的同一資源。
在實(shí)際的數(shù)據(jù)庫(kù)系統(tǒng)中,每時(shí)每刻都在發(fā)生鎖定,當(dāng)某個(gè)用戶在修改某一部分?jǐn)?shù)據(jù)時(shí),MySQL 會(huì)通過(guò)鎖定防止其他用戶讀取同一數(shù)據(jù)。寫鎖比讀鎖有更高的優(yōu)先級(jí),一個(gè)寫鎖請(qǐng)求可能會(huì)被插入到讀鎖隊(duì)列的前面,但是讀鎖不能插入到寫鎖前面。
鎖策略
一種提高共享資源并發(fā)性的方法就是讓鎖定對(duì)象更有選擇性,盡量只鎖定需要修改的部分?jǐn)?shù)據(jù)而不是所有資源,更理想的方式是只對(duì)會(huì)修改的數(shù)據(jù)進(jìn)行精確鎖定。任何時(shí)刻在給定的資源上,鎖定的數(shù)據(jù)量越少,系統(tǒng)的并發(fā)程度就越高,只要不發(fā)生沖突即可。
鎖策略就是在鎖的開(kāi)銷和數(shù)據(jù)安全性之間尋求平衡,這種平衡也會(huì)影響性能。大多數(shù)商業(yè)數(shù)據(jù)庫(kù)系統(tǒng)沒(méi)有提供更多選擇,一般都是在表上加行鎖,而 MySQL 提供了多種選擇,每種MySQL存儲(chǔ)引擎都可以實(shí)現(xiàn)自己的鎖策略和鎖粒度。MySQL 最重要的兩種鎖策略:
- 表鎖是MySQL中最基本的鎖策略,并且是開(kāi)銷最小的策略。表鎖會(huì)鎖定整張表,一個(gè)用戶在對(duì)表進(jìn)行寫操作前需要先獲得寫鎖,這會(huì)阻塞其他用戶對(duì)該表的所有讀寫操作。只有沒(méi)有寫鎖時(shí),其他讀取的用戶才能獲取讀鎖,讀鎖之間不相互阻塞。
- 行鎖可以最大程度地支持并發(fā)處理,同時(shí)也帶來(lái)了最大的鎖開(kāi)銷。InnoDB 和 XtraDB 以及一些其他存儲(chǔ)引擎實(shí)現(xiàn)了行鎖。行鎖只在存儲(chǔ)引擎層實(shí)現(xiàn),而服務(wù)器層沒(méi)有實(shí)現(xiàn)。
死鎖
死鎖是指兩個(gè)或者多個(gè)事務(wù)在同一資源上相互占用并請(qǐng)求鎖定對(duì)方占用的資源,從而導(dǎo)致惡性循環(huán)的現(xiàn)象。當(dāng)多個(gè)事務(wù)試圖以不同順序鎖定資源時(shí)就可能會(huì)產(chǎn)生死鎖,多個(gè)事務(wù)同時(shí)鎖定同一個(gè)資源時(shí)也會(huì)產(chǎn)生死鎖。
為了解決死鎖問(wèn)題,數(shù)據(jù)庫(kù)系統(tǒng)實(shí)現(xiàn)了各種死鎖檢測(cè)和死鎖超時(shí)機(jī)制。越復(fù)雜的系統(tǒng),例如InnoDB 存儲(chǔ)引擎,越能檢測(cè)到死鎖的循環(huán)依賴,并立即返回一個(gè)錯(cuò)誤。這種解決方式很有效,否則死鎖會(huì)導(dǎo)致出現(xiàn)非常慢的查詢。還有一種解決方法,就是當(dāng)查詢的時(shí)間達(dá)到鎖等待超時(shí)的設(shè)定后放棄鎖請(qǐng)求,這種方式通常來(lái)說(shuō)不太好。InnoDB 目前處理死鎖的方法是將持有最少行級(jí)排它鎖的事務(wù)進(jìn)行回滾。
鎖的行為與順序是和存儲(chǔ)引擎相關(guān)的,以同樣的順序執(zhí)行語(yǔ)句,有些存儲(chǔ)引擎會(huì)產(chǎn)生死鎖有些則不會(huì)。死鎖的產(chǎn)生有雙重原因:有些是真正的數(shù)據(jù)沖突,這種情況很難避免,有些則完全是由于存儲(chǔ)引擎的實(shí)現(xiàn)方式導(dǎo)致的。
死鎖發(fā)生之后,只有部分或者完全回滾其中一個(gè)事務(wù),才能打破死鎖。對(duì)于事務(wù)型系統(tǒng)這是無(wú)法避免的,所以應(yīng)用程序在設(shè)計(jì)時(shí)必須考慮如何處理死鎖。大多數(shù)情況下只需要重新執(zhí)行因死鎖回滾的事務(wù)即可。
P3:事務(wù)
事務(wù)就是一組原子性的 SQL 查詢,或者說(shuō)一個(gè)獨(dú)立的工作單元。如果數(shù)據(jù)庫(kù)引擎能夠成功地對(duì)數(shù)據(jù)庫(kù)應(yīng)用該組查詢的全部語(yǔ)句,那么就執(zhí)行該組查詢。如果其中有任何一條語(yǔ)句因?yàn)楸罎⒒蚱渌驘o(wú)法執(zhí)行,那么所有的語(yǔ)句都不會(huì)執(zhí)行。也就是說(shuō)事務(wù)內(nèi)的語(yǔ)句要么全部執(zhí)行成功,要么全部執(zhí)行失敗。
ACID 特性
一個(gè)運(yùn)行良好的事務(wù)處理系統(tǒng)必須具備 ACID 特性,實(shí)現(xiàn)了 ACID 的數(shù)據(jù)庫(kù)需要更強(qiáng)的CPU處理能力、更大的內(nèi)存和磁盤空間。
- 原子性 atomicity
一個(gè)事務(wù)在邏輯上是必須不可分割的最小工作單元,整個(gè)事務(wù)中的所有操作要么全部提交成功,要么全部失敗回滾,對(duì)于一個(gè)事務(wù)來(lái)說(shuō)不可能只執(zhí)行其中的一部分。 - 一致性 consistency
數(shù)據(jù)庫(kù)總是從一個(gè)一致性的狀態(tài)轉(zhuǎn)換到另一個(gè)一致性的狀態(tài)。 - 隔離性 isolation
針對(duì)并發(fā)事務(wù)而言,隔離性就是要隔離并發(fā)運(yùn)行的多個(gè)事務(wù)之間的相互影響,一般來(lái)說(shuō)一個(gè)事務(wù)所做的修改在最終提交以前,對(duì)其他事務(wù)是不可見(jiàn)的。 - 持久性 durability
一旦事務(wù)提交成功,其修改就會(huì)永久保存到數(shù)據(jù)庫(kù)中,此時(shí)即使系統(tǒng)崩潰,修改的數(shù)據(jù)也不會(huì)丟失。
隔離級(jí)別
在 SQL 標(biāo)準(zhǔn)中定義了四種隔離級(jí)別,每一種隔離級(jí)別都規(guī)定了一個(gè)事務(wù)中所做的修改,哪些在事務(wù)內(nèi)和事務(wù)間是可見(jiàn)的,哪些是不可見(jiàn)的。較低級(jí)別的隔離通常可以執(zhí)行更高的并發(fā),系統(tǒng)的開(kāi)銷也更低。
- 未提交讀 READ UNCOMMITTED
在該級(jí)別事務(wù)中的修改即使沒(méi)有被提交,對(duì)其他事務(wù)也是可見(jiàn)的。事務(wù)可以讀取其他事務(wù)修改完但未提交的數(shù)據(jù),這種問(wèn)題稱為臟讀。這個(gè)級(jí)別還會(huì)導(dǎo)致不可重復(fù)讀和幻讀,從性能上說(shuō)也沒(méi)有比其他級(jí)別好很多,因此很少使用。 - 提交讀 READ COMMITTED
大多數(shù)數(shù)據(jù)庫(kù)系統(tǒng)默認(rèn)的隔離級(jí)別就是提交讀,但 MySQL 不是。提交讀滿足了隔離性的簡(jiǎn)單定義:一個(gè)事務(wù)開(kāi)始時(shí)只能"看見(jiàn)"已經(jīng)提交的事務(wù)所做的修改。換句話說(shuō),一個(gè)事務(wù)從開(kāi)始直到提交之前的任何修改對(duì)其他事務(wù)都是不可見(jiàn)的。這個(gè)級(jí)別有時(shí)也叫不可重復(fù)讀,因?yàn)閮纱螆?zhí)行同樣的查詢可能會(huì)得到不同結(jié)果。提交讀存在不可重復(fù)讀和幻讀的問(wèn)題。 - 可重復(fù)讀 REPEATABLE READ(MySQL默認(rèn)的隔離級(jí)別)
可重復(fù)讀解決了不可重復(fù)讀的問(wèn)題,該級(jí)別保證了在同一個(gè)事務(wù)中多次讀取同樣的記錄結(jié)果是一致的。但可重復(fù)讀隔離級(jí)別還是無(wú)法解決幻讀的問(wèn)題,所謂幻讀,指的是當(dāng)某個(gè)事務(wù)在讀取某個(gè)范圍內(nèi)的記錄時(shí),會(huì)產(chǎn)生幻行。InnoDB 存儲(chǔ)引擎通過(guò)多版本并發(fā)控制MVCC 解決幻讀的問(wèn)題。 - 可串行化 SERIALIZABLE
該級(jí)別是最高的隔離級(jí)別,通過(guò)強(qiáng)制事務(wù)串行執(zhí)行,避免了幻讀的問(wèn)題。可串行化會(huì)在讀取的每一行數(shù)據(jù)上都加鎖,可能導(dǎo)致大量的超時(shí)和鎖爭(zhēng)用的問(wèn)題。實(shí)際應(yīng)用中很少用到這個(gè)隔離級(jí)別,只有非常需要確保數(shù)據(jù)一致性且可以接受沒(méi)有并發(fā)的情況下才考慮該級(jí)別。
MySQL 中的事務(wù)
MySQL 提供了兩種事務(wù)型的存儲(chǔ)引擎:InnoDB 和 NDB Cluster。
MySQL 事務(wù)默認(rèn)采用自動(dòng)提交模式,如果不是顯式地開(kāi)始一個(gè)事務(wù),則每個(gè)查詢都將被當(dāng)作一個(gè)事務(wù)執(zhí)行提交操作。在當(dāng)前連接中,可以通過(guò)設(shè)置 AUTOCOMMIT 變量來(lái)啟用或禁用自動(dòng)提交模式。
1 或 ON 表示啟用,0 或 OFF 表示禁用,當(dāng)禁用自動(dòng)提交時(shí),所有的查詢都是在一個(gè)事務(wù)中,直到顯式地執(zhí)行 COMMIT 或 ROLLBACK 后該事務(wù)才會(huì)結(jié)束,同時(shí)又開(kāi)始了一個(gè)新事務(wù)。修改 AUTOCOMMIT 對(duì)非事務(wù)型表,例如 MyISAM 或內(nèi)存表不會(huì)有任何影響,對(duì)這類表來(lái)說(shuō)沒(méi)有 COMMIT 或 ROLLBACK 的概念,也可以理解為一直處于啟用自動(dòng)提交的模式
有一些命令在執(zhí)行之前會(huì)強(qiáng)制執(zhí)行提交當(dāng)前的活動(dòng)事務(wù),例如ALTER TABLE和LOCK TABLES等。
MySQL能夠識(shí)別所有的 4個(gè) ANSI 隔離級(jí)別,InnoDB 引擎也支持所有隔離級(jí)別。
P4:MVCC 多版本并發(fā)控制
可以認(rèn)為 MVCC 是行級(jí)鎖的一個(gè)變種,但它在很多情況下避免了加鎖操作,因此開(kāi)銷更低。雖然實(shí)現(xiàn)機(jī)制有所不同,但大都實(shí)現(xiàn)了非阻塞的讀操作,寫操作也只鎖定必要的行。
MVCC 的實(shí)現(xiàn),是通過(guò)保存數(shù)據(jù)在某個(gè)時(shí)間點(diǎn)的快照來(lái)實(shí)現(xiàn)的。也就是說(shuō)不管需要執(zhí)行多長(zhǎng)時(shí)間,每個(gè)事務(wù)看到的數(shù)據(jù)都是一致的。根據(jù)事務(wù)開(kāi)始的時(shí)間不同,每個(gè)事務(wù)對(duì)同一張表,同一時(shí)刻看到的數(shù)據(jù)可能是不一樣的。
不同的存儲(chǔ)引擎的 MVCC 實(shí)現(xiàn)是不同的,典型的有樂(lè)觀并發(fā)控制和悲觀并發(fā)控制。
InnoDB 的 MVCC 實(shí)現(xiàn)
InnoDB 的MVCC 通過(guò)在每行記錄后面保存兩個(gè)隱藏的列來(lái)實(shí)現(xiàn),這兩個(gè)列一個(gè)保存了行的創(chuàng)建時(shí)間,一個(gè)保存行的過(guò)期時(shí)間間。不過(guò)存儲(chǔ)的不是實(shí)際的時(shí)間值而是系統(tǒng)版本號(hào),每開(kāi)始一個(gè)新的事務(wù)系統(tǒng)版本號(hào)都會(huì)自動(dòng)遞增,事務(wù)開(kāi)始時(shí)刻的系統(tǒng)版本號(hào)會(huì)作為事務(wù)的版本號(hào),用來(lái)和查詢到的每行記錄的版本號(hào)進(jìn)行比較。
REPEATABLE READ 級(jí)別下 MVCC 的具體實(shí)現(xiàn)
SELECT:InnoDB 會(huì)根據(jù)以下兩個(gè)條件檢查每行記錄:
- 只查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行,可以確保事務(wù)讀取的行要么是事務(wù)開(kāi)始前已經(jīng)存在的,要么是事物自身插入或修改過(guò)的。
- 行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號(hào),可以確保事務(wù)讀取到的行在事務(wù)開(kāi)始前未被刪除。
INSERT :為新插入的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào)。
DELETE:為刪除的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行刪除標(biāo)識(shí)。
UPDATE:為插入的每一行新記錄保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào),同時(shí)保存當(dāng)前系統(tǒng)版本號(hào)到原來(lái)的行作為行刪除標(biāo)識(shí)。
保存這兩個(gè)額外系統(tǒng)版本號(hào)使大多數(shù)讀操作都可以不用加鎖。這樣設(shè)計(jì)使讀數(shù)據(jù)操作簡(jiǎn)單且高效,并且能保證只會(huì)讀取到符合標(biāo)準(zhǔn)的行。不足之處是每行記錄都需要額外存儲(chǔ)空間,需要做更多行檢查工作以及一些額外維護(hù)工作。
MVCC 只能在 READ COMMITTED 和 REPEATABLE READ 兩個(gè)隔離級(jí)別下工作,因?yàn)?READ UNCOMMITTED 總是讀取最新的數(shù)據(jù)行,而不是符合當(dāng)前事務(wù)版本的數(shù)據(jù)行,而 SERIALIZABLE 則會(huì)對(duì)所有讀取的行都加鎖。
P5:InnoDB 存儲(chǔ)引擎
InnoDB 是 MySQL 的默認(rèn)事務(wù)型引擎,它被設(shè)計(jì)用來(lái)處理大量短期事務(wù)。InnoDB 的性能和自動(dòng)崩潰恢復(fù)特性使得它在非事務(wù)型存儲(chǔ)需求中也很流行,除非有特別原因否則應(yīng)該優(yōu)先考慮 InnoDB 引擎。
InnoDB 的數(shù)據(jù)存儲(chǔ)在表空間中,表空間由一系列數(shù)據(jù)文件組成。MySQL4.1 后 InnoDB 可以將每個(gè)表的數(shù)據(jù)和索引放在單獨(dú)的文件中。
InnoDB 采用 MVCC 來(lái)支持高并發(fā),并且實(shí)現(xiàn)了四個(gè)標(biāo)準(zhǔn)的隔離級(jí)別。其默認(rèn)級(jí)別是 REPEATABLE READ,并且通過(guò)間隙鎖策略防止幻讀,間隙鎖使 InnoDB 不僅僅鎖定查詢涉及的行,還會(huì)對(duì)索引中的間隙進(jìn)行鎖定防止幻行的插入。
InnoDB 表是基于聚簇索引建立的,InnoDB 的索引結(jié)構(gòu)和其他存儲(chǔ)引擎有很大不同,聚簇索引對(duì)主鍵查詢有很高的性能,不過(guò)它的二級(jí)索引中必須包含主鍵列,所以如果主鍵很大的話其他所有索引都會(huì)很大,因此如果表上索引較多的話主鍵應(yīng)當(dāng)盡可能小。
InnoDB 的存儲(chǔ)格式是平臺(tái)獨(dú)立的,可以將數(shù)據(jù)和索引文件從一個(gè)平臺(tái)復(fù)制到另一個(gè)平臺(tái)。
InnoDB 內(nèi)部做了很多優(yōu)化,包括從磁盤讀取數(shù)據(jù)時(shí)采用的可預(yù)測(cè)性預(yù)讀,能夠自動(dòng)在內(nèi)存中創(chuàng)建加速讀操作的自適應(yīng)哈希索引,以及能夠加速插入操作的插入緩沖區(qū)等。
選擇合適的存儲(chǔ)引擎
MySQL5.5 將 InnoDB 作為默認(rèn)存儲(chǔ)引擎,除非需要用到某些 InnoDB 不具備的特性,并且沒(méi)有其他方法可以代替,否則都應(yīng)該優(yōu)先選用InnoDB。
如果應(yīng)用需要事務(wù)支持,那么 InnoDB 是目前最穩(wěn)定并且經(jīng)過(guò)驗(yàn)證的選擇。如果不需要事務(wù)并且主要是 SELECT 和 INSERT 操作,那么MyISAM 是不錯(cuò)的選擇。相對(duì)而言,MyISAM 崩潰后發(fā)生損壞的概率要比 InnoDB 大很多而且恢復(fù)速度也要慢,因此即使不需要事務(wù)支持,也可以選擇InnoDB。
如果可以定期地關(guān)閉服務(wù)器來(lái)執(zhí)行備份,那么備份的因素可以忽略。反之如果需要在線熱備份,那么 InnoDB 就是基本的要求。
P6:MyISAM 存儲(chǔ)引擎
在 MySQL5.1及之前,MyISAM 是默認(rèn)的存儲(chǔ)引擎,MyISAM 提供了大量的特性,包括全文索引、壓縮、空間函數(shù)等,但不支持事務(wù)和行鎖,最大的缺陷就是崩潰后無(wú)法安全恢復(fù)。對(duì)于只讀的數(shù)據(jù)或者表比較小、可以忍受修復(fù)操作的情況仍然可以使用 MyISAM。
MyISAM 將表存儲(chǔ)在數(shù)據(jù)文件和索引文件中,分別以 .MYD 和 .MYI 作為擴(kuò)展名。MyISAM 表可以包含動(dòng)態(tài)或者靜態(tài)行,MySQL 會(huì)根據(jù)表的定義決定行格式。MyISAM 表可以存儲(chǔ)的行記錄數(shù)一般受限于可用磁盤空間或者操作系統(tǒng)中單個(gè)文件的最大尺寸。
MyISAM 對(duì)整張表進(jìn)行加鎖,讀取時(shí)會(huì)對(duì)需要讀到的所有表加共享鎖,寫入時(shí)則對(duì)表加排它鎖。但是在表有讀取查詢的同時(shí),也支持并發(fā)往表中插入新的記錄。
對(duì)于MyISAM 表,MySQL 可以手動(dòng)或自動(dòng)執(zhí)行檢查和修復(fù)操作,這里的修復(fù)和事務(wù)恢復(fù)以及崩潰恢復(fù)的概念不同。執(zhí)行表的修復(fù)可能導(dǎo)致一些數(shù)據(jù)丟失,而且修復(fù)操作很慢。
對(duì)于 MyISAM 表,即使是 BLOB 和 TEXT 等長(zhǎng)字段,也可以基于其前 500 個(gè)字符創(chuàng)建索引。MyISAM 也支持全文索引,這是一種基于分詞創(chuàng)建的索引,可以支持復(fù)雜的查詢。
創(chuàng)建 MyISAM 表時(shí)如果指定了 DELAY_KEY_WRITE 選項(xiàng),在每次修改執(zhí)行完成時(shí)不會(huì)立刻將修改的索引數(shù)據(jù)寫入磁盤,而是會(huì)寫到內(nèi)存中的鍵緩沖區(qū),只有在清理緩沖區(qū)或關(guān)閉表的時(shí)候才會(huì)將對(duì)應(yīng)的索引庫(kù)寫入磁盤。這種方式可以極大提升寫性能,但在數(shù)據(jù)庫(kù)或主機(jī)崩潰時(shí)會(huì)造成索引損壞,需要執(zhí)行修復(fù)。延遲更新索引鍵的特性可以在全局設(shè)置也可以單個(gè)表設(shè)置。
MyISAM 設(shè)計(jì)簡(jiǎn)單,數(shù)據(jù)以緊密格式存儲(chǔ),所以在某些場(chǎng)景下性能很好。MyISAM 最典型的性能問(wèn)題還是表鎖問(wèn)題,如果所有的查詢長(zhǎng)期處于 Locked 狀態(tài),那么原因毫無(wú)疑問(wèn)就是表鎖。
P7:Memory 存儲(chǔ)引擎
如果需要快速訪問(wèn)數(shù)據(jù),并且這些數(shù)據(jù)不會(huì)被修改,重啟以后丟失也沒(méi)有關(guān)系,那么使用 Memory 表是非常有用的。Memory 表至少要比 MyISAM 表快一個(gè)數(shù)量級(jí),因?yàn)樗械臄?shù)據(jù)都保存在內(nèi)存中,不需要進(jìn)行磁盤 IO,Memory 表的結(jié)構(gòu)在重啟以后還會(huì)保留,但數(shù)據(jù)會(huì)丟失。
Memory 表適合的場(chǎng)景:查找或者映射表、緩存周期性聚合數(shù)據(jù)的結(jié)果、保存數(shù)據(jù)分析中產(chǎn)生的中間數(shù)據(jù)。
Memory 表支持哈希索引,因此查找速度極快。雖然速度很快但還是無(wú)法取代傳統(tǒng)的基于磁盤的表,Memory 表使用表級(jí)鎖,因此并發(fā)寫入的性能較低。它不支持 BLOB 和 TEXT 類型的列,并且每行的長(zhǎng)度是固定的,所以即使指定了 VARCHAR 列,實(shí)際存儲(chǔ)時(shí)也會(huì)轉(zhuǎn)換成CHAR,這可能導(dǎo)致部分內(nèi)存的浪費(fèi)。
如果 MySQL 在執(zhí)行查詢的過(guò)程中需要使用臨時(shí)表來(lái)保持中間結(jié)果,內(nèi)部使用的臨時(shí)表就是 Memory 表。如果中間結(jié)果太大超出了Memory 表的限制,或者含有 BLOB 或 TEXT 字段,臨時(shí)表會(huì)轉(zhuǎn)換成 MyISAM 表。
P8:數(shù)據(jù)類型
整數(shù)類型
如果存儲(chǔ)整數(shù)可以使用這幾種整數(shù)類型:TINYINT、SMALLINT、MEDIUMINT、INT,BIGINT,它們分別使用8、16、24、32、64 位存儲(chǔ)空間。
整數(shù)類型有可選的 UNSIGNED 屬性,表示不允許負(fù)值,可以使整數(shù)的上限提高一倍。有符號(hào)和無(wú)符號(hào)類型使用相同的存儲(chǔ)空間并具有相同的性能,可以根據(jù)實(shí)際情況選擇合適的類型。
MySQL 可以為整數(shù)類型指定寬度,例如 INT(11),這對(duì)大多數(shù)應(yīng)用沒(méi)有意義,不會(huì)限制值的范圍,只是規(guī)定了 MySQL 的交互工具顯示字符的個(gè)數(shù),對(duì)于存儲(chǔ)和計(jì)算來(lái)說(shuō) INT(1) 和 INT(11) 是相同的。
實(shí)數(shù)類型
實(shí)數(shù)是帶有小數(shù)部分的數(shù)字,但它們不只是為了存儲(chǔ)小數(shù),也可以使用 DECIMAL 存儲(chǔ)比 BIGINT 還大的整數(shù)。MySQL既支持精確類型,也支持不精確類型。
FLOAT 和 DOUBLE 支持使用標(biāo)準(zhǔn)的浮點(diǎn)運(yùn)算進(jìn)行近似運(yùn)算,DECIMAL 用于存儲(chǔ)精確的小數(shù)。
浮點(diǎn)類型在存儲(chǔ)同樣范圍的值時(shí),通常比 DECIMAL 使用更少的空間。FLOAT 使用 4 字節(jié)存儲(chǔ),DOUBLE 占用8字節(jié),MySQL 內(nèi)部使用DOUBLE 作為內(nèi)部浮點(diǎn)計(jì)算的類型。
因?yàn)樾枰~外空間和計(jì)算開(kāi)銷,所以應(yīng)當(dāng)盡量只在對(duì)小數(shù)進(jìn)行精確計(jì)算時(shí)才使用 DECIMAL。在數(shù)據(jù)量較大時(shí)可以考慮 BIGINT 代替DECIMAL,將需要存儲(chǔ)的貨幣單位根據(jù)小數(shù)的位數(shù)乘以相應(yīng)的倍數(shù)即可。假設(shè)要存儲(chǔ)的數(shù)據(jù)精確到萬(wàn)分之一分,則可以把所有金額乘以一百萬(wàn)將結(jié)果存儲(chǔ)在 BIGINT 中,這樣可以同時(shí)避免浮點(diǎn)存儲(chǔ)計(jì)算不精確和 DECIMAL 精確計(jì)算代價(jià)高的問(wèn)題。
VARCHAR
VARCHAR 用于存儲(chǔ)可變字符串,是最常見(jiàn)的字符串?dāng)?shù)據(jù)類型。它比定長(zhǎng)字符串更節(jié)省空間,因?yàn)樗鼉H使用必要的空間。VARCHAR 需要 1或 2 個(gè)額外字節(jié)記錄字符串長(zhǎng)度,如果列的最大長(zhǎng)度不大于 255 字節(jié)則只需要1 字節(jié)。VARCHAR 不會(huì)刪除末尾空格。
VARCHAR 節(jié)省了存儲(chǔ)空間,但由于行是變長(zhǎng)的,在 UPDATE 時(shí)可能使行變得比原來(lái)更長(zhǎng),這就導(dǎo)致需要做額外的工作。如果一個(gè)行占用的空間增長(zhǎng)并且頁(yè)內(nèi)沒(méi)有更多的空間可以存儲(chǔ),這種情況下不同存儲(chǔ)引擎處理不同,InnoDB 會(huì)分裂頁(yè)而 MyISAM 會(huì)將行拆分成不同片。
適用場(chǎng)景:字符串列的最大長(zhǎng)度比平均長(zhǎng)度大很多、列的更新很少、使用了 UTF8 這種復(fù)雜字符集,每個(gè)字符都使用不同的字節(jié)數(shù)存儲(chǔ)。
InnoDB 可以把過(guò)長(zhǎng)的 VARCHAR 存儲(chǔ)為 BLOB。
CHAR
CHAR 是定長(zhǎng)的,根據(jù)定義的字符串長(zhǎng)度分配足夠的空間。CHAR 會(huì)刪除末尾空格。
CHAR 適合存儲(chǔ)很短的字符串,或所有值都接近同一個(gè)長(zhǎng)度,例如存儲(chǔ)密碼的 MD5 值。對(duì)于經(jīng)常變更的數(shù)據(jù),CHAR 也比 VARCHAR更好,因?yàn)槎ㄩL(zhǎng)的 CHAR 不容易產(chǎn)生碎片。對(duì)于非常短的列,CHAR 在存儲(chǔ)空間上也更有效率,例如用 CHAR 來(lái)存儲(chǔ)只有 Y 和 N 的值只需要一個(gè)字節(jié),但是 VARCHAR 需要兩個(gè)字節(jié),因?yàn)檫€有一個(gè)記錄長(zhǎng)度的額外字節(jié)。
BLOB 和 TEXT 類型
BLOB 和 TEXT 都是為了存儲(chǔ)大數(shù)據(jù)而設(shè)計(jì)的字符串?dāng)?shù)據(jù)類型,分別采用二進(jìn)制和字符串方式存儲(chǔ)。MySQL會(huì)把每個(gè) BLOB 和 TEXT 值當(dāng)作一個(gè)獨(dú)立的對(duì)象處理,存儲(chǔ)引擎在存儲(chǔ)時(shí)通常會(huì)做特殊處理。當(dāng)值太大時(shí),InnoDB 會(huì)使用專門的外部存儲(chǔ)區(qū)來(lái)進(jìn)行存儲(chǔ)。BLOB 和TEXT 僅有的不同是 BLOB 存儲(chǔ)的是二進(jìn)制數(shù)據(jù),沒(méi)有排序規(guī)則或字符集,而 TEXT 有字符集和排序規(guī)則。
MySQL 對(duì) BLOB 和TEXT 列進(jìn)行排序與其他類型不同:它只對(duì)每個(gè)列最前 max_sort_length 字節(jié)而不是整個(gè)字符串做排序,如果只需要排序前面一小部分字符,則可以減小 max_sort_length 的配置。MySQL 不能將 BLOB 和 TEXT 列全部長(zhǎng)度的字符串進(jìn)行索引,也不能使用這些索引消除排序。
DATETIME
能保存大范圍的值,從 1001 年到 9999 年,精度為秒。它把日期和時(shí)間封裝到了一個(gè)整數(shù)中,與時(shí)區(qū)無(wú)關(guān),使用 8 字節(jié)的存儲(chǔ)空間。
TIMESTAMP
和 UNIX 時(shí)間戳相同,只使用 4 字節(jié)的存儲(chǔ)空間,因此范圍比 DATETIME 小得多,只能表示 1970 年到 2038 年,并且依賴于時(shí)區(qū)。
P9:索引的分類
索引在也叫做鍵,是存儲(chǔ)引擎用于快速找到記錄的一種數(shù)據(jù)結(jié)構(gòu)。索引對(duì)于良好的性能很關(guān)鍵,尤其是當(dāng)表中數(shù)據(jù)量越來(lái)越大時(shí),索引對(duì)性能的影響愈發(fā)重要。在數(shù)據(jù)量較小且負(fù)載較低時(shí),不恰當(dāng)?shù)乃饕龑?duì)性能的影響可能還不明顯,但數(shù)據(jù)量逐漸增大時(shí),性能會(huì)急劇下降。
索引大大減少了服務(wù)器需要掃描的數(shù)據(jù)量、可以幫助服務(wù)器避免排序和臨時(shí)表、可以將隨機(jī) IO 變成順序 IO。但索引并不總是最好的工具,對(duì)于非常小的表,大部分情況下會(huì)采用全表掃描。對(duì)于中到大型的表,索引就非常有效。但對(duì)于特大型的表,建立和使用索引的代價(jià)也隨之增長(zhǎng),這種情況下應(yīng)該使用分區(qū)技術(shù)。
在MySQL中,首先在索引中找到對(duì)應(yīng)的值,然后根據(jù)匹配的索引記錄找到對(duì)應(yīng)的數(shù)據(jù)行。索引可以包括一個(gè)或多個(gè)列的值,如果索引包含多個(gè)列,那么列的順序也十分重要,因?yàn)?MySQL 只能高效地使用索引的最左前綴列。
B-Tree 索引
大多數(shù) MySQL 引擎都支持這種索引,不過(guò)底層的存儲(chǔ)引擎可能使用不同的存儲(chǔ)結(jié)構(gòu),例如 NDB 集群實(shí)際使用 T-Tree,而 InnoDB 則使用 B+Tree。
存儲(chǔ)引擎以不同方式使用 B-Tree 索引,性能也不同。例如 MyISAM 使用前綴壓縮技術(shù)使得索引更小,但 InnoDB 則按照原數(shù)據(jù)格式進(jìn)行存儲(chǔ)。再例如 MyISAM 索引通過(guò)數(shù)據(jù)的物理位置引用被索引的行,而 InnoDB 則根據(jù)主鍵引用被索引的行。
B-Tree 通常意味著所有的值都是按順序存儲(chǔ)的,并且每個(gè)葉子頁(yè)到根的距離相同。B-Tree 索引能夠加快訪問(wèn)數(shù)據(jù)的速度,因?yàn)榇鎯?chǔ)引擎不再需要進(jìn)行全表掃描來(lái)獲取需要的數(shù)據(jù),取而代之的是從索引的根節(jié)點(diǎn)開(kāi)始進(jìn)行搜索。根節(jié)點(diǎn)的槽中存放了指向子節(jié)點(diǎn)的指針,存儲(chǔ)引擎根據(jù)這些指針向下層查找。通過(guò)比較節(jié)點(diǎn)頁(yè)的值和要查找的值可以找到合適的指針進(jìn)入下層子節(jié)點(diǎn),這些指針實(shí)際上定義了子節(jié)點(diǎn)頁(yè)中值的上限和下限。最終存儲(chǔ)引擎要么找到對(duì)應(yīng)的值,要么該記錄不存在。葉子節(jié)點(diǎn)的指針指向的是被索引的數(shù)據(jù),而不是其他的節(jié)點(diǎn)頁(yè)。
B-Tree索引適用于全鍵值、鍵值范圍或鍵前綴查找,其中鍵前綴查找只適用于最左前綴查找。索引對(duì)如下類型的查詢有效:
- 全值匹配:全值匹配指的是和索引中的所有列進(jìn)行匹配。
- 匹配最左前綴:只使用索引的第一列。
- 匹配列前綴:只匹配某一列的值的開(kāi)頭部分。
- 匹配范圍值:查找某兩個(gè)值之間的范圍。
- 精確匹配某一列并范圍匹配另一列:有一列全匹配而另一列范圍匹配。
- 只訪問(wèn)索引的查詢:B-Tree 通常可以支持只訪問(wèn)索引的查詢,即查詢只需要訪問(wèn)索引而無(wú)需訪問(wèn)數(shù)據(jù)行。
因?yàn)樗饕龢?shù)中的節(jié)點(diǎn)有序,所以除了按值查找之外索引還可以用于查詢中的 ORDER BY 操作。一般如果 B-Tree 可以按照某種方式查找到值,那么也可以按照這種方式排序。
B-Tree索引的限制:
- 如果不是按照索引的最左列開(kāi)始查找,則無(wú)法使用索引。
- 不能跳過(guò)索引中的列,例如索引為 (id,name,sex),不能只使用 id 和 sex 而跳過(guò) name。
- 如果查詢中有某個(gè)列的范圍查詢,則其右邊的所有列都無(wú)法使用索引。
B-Tree 和 B+Tree 的區(qū)別:
B-Tree 中每個(gè)節(jié)點(diǎn)同時(shí)存儲(chǔ) key 和 data,而 B+Tree 中只有葉子節(jié)點(diǎn)才存儲(chǔ) data,非葉子節(jié)點(diǎn)只存儲(chǔ) key。InnoDB 對(duì) B+Tree 進(jìn)行了優(yōu)化,在每個(gè)葉子節(jié)點(diǎn)上增加了一個(gè)指向相鄰葉子節(jié)點(diǎn)的鏈表指針,形成了帶有順序指針的 B+Tree,提高區(qū)間訪問(wèn)的性能。
B+Tree 的優(yōu)點(diǎn)在于:① 由于 B+Tree 在非葉子節(jié)點(diǎn)上不含數(shù)據(jù)信息,因此在內(nèi)存頁(yè)中能夠存放更多的 key,數(shù)據(jù)存放得更加緊密,具有更好的空間利用率,訪問(wèn)葉子節(jié)點(diǎn)上關(guān)聯(lián)的數(shù)據(jù)也具有更好的緩存命中率。② B+Tree 的葉子結(jié)點(diǎn)都是相連的,因此對(duì)整棵樹(shù)的遍歷只需要一次線性遍歷葉子節(jié)點(diǎn)即可。而 B-Tree 則需要進(jìn)行每一層的遞歸遍歷,相鄰的元素可能在內(nèi)存中不相鄰,所以緩存命中性沒(méi)有 B+Tree 好。但是 B-Tree 也有優(yōu)點(diǎn),由于每一個(gè)節(jié)點(diǎn)都包含 key 和 value,因此經(jīng)常訪問(wèn)的元素可能離根節(jié)點(diǎn)更近,訪問(wèn)也更迅速。
哈希索引
哈希索引基于哈希表實(shí)現(xiàn),只有精確匹配索引所有列的查詢才有效。對(duì)于每一行數(shù)據(jù),存儲(chǔ)引擎都會(huì)對(duì)所有的索引列計(jì)算一個(gè)哈希碼,哈希碼是一個(gè)較小的值,并且不同鍵值的行計(jì)算出的哈希碼也不一樣。哈希索引將所有的哈希碼存儲(chǔ)在索引中,同時(shí)在哈希表中保存指向每個(gè)數(shù)據(jù)行的指針。
只有 Memory 引擎顯式支持哈希索引,這也是 Memory 引擎的默認(rèn)索引類型。
因?yàn)樗饕陨碇恍璐鎯?chǔ)對(duì)應(yīng)的哈希值,所以索引的結(jié)構(gòu)十分緊湊,這讓哈希索引的速度非常快,但它也有一些限制:
- 哈希索引只包含哈希值和行指針而不存儲(chǔ)字段值,所以不能使用索引中的值來(lái)避免讀取行。
- 哈希索引數(shù)據(jù)并不是按照索引值順序存儲(chǔ)的,因此無(wú)法用于排序。
- 哈希索引不支持部分索引列匹配查找,因?yàn)楣K饕冀K是使用索引列的全部?jī)?nèi)容來(lái)計(jì)算哈希值的。例如在數(shù)據(jù)列(a,b)上建立哈希索引,如果查詢的列只有a就無(wú)法使用該索引。
- 哈希索引只支持等值比較查詢,不支持任何范圍查詢。
- 訪問(wèn)哈希索引的數(shù)據(jù)非常快,除非有很多哈希沖突。當(dāng)出現(xiàn)哈希沖突時(shí),存儲(chǔ)引擎必須遍歷鏈表中所有的行指針,逐行進(jìn)行比較直到找到所有符合條件的行。
- 如果哈希沖突很高的話,索引維護(hù)的代價(jià)也會(huì)很高。
自適應(yīng)哈希索引是 InnoDB 引擎的一個(gè)特殊功能,當(dāng)它注意到某些索引值被使用的非常頻繁時(shí),會(huì)在內(nèi)存中基于 B-Tree 索引之上再創(chuàng)鍵一個(gè)哈希索引,這樣就讓 B-Tree 索引也具有哈希索引的一些優(yōu)點(diǎn),比如快速哈希查找。這是一個(gè)完全自動(dòng)的內(nèi)部行為,用戶無(wú)法控制或配置,但如果有必要可以關(guān)閉該功能。
如果存儲(chǔ)引擎不支持哈希索引,可以創(chuàng)建自定義哈希索引,在 B-Tree基礎(chǔ) 上創(chuàng)建一個(gè)偽哈希索引,它使用哈希值而不是鍵本身進(jìn)行索引查找,需要在查詢的 WHERE 子句中手動(dòng)指定哈希函數(shù)。當(dāng)數(shù)據(jù)表非常大時(shí),CRC32 會(huì)出現(xiàn)大量的哈希沖突,可以考慮自己實(shí)現(xiàn) 64 位哈希函數(shù),或者使用 MD5 函數(shù)返回值的一部分作為自定義哈希函數(shù)。
空間索引
MyISAM 表支持空間索引,可以用作地理數(shù)據(jù)存儲(chǔ)。和 B-Tree 索引不同,這類索引無(wú)需前綴查詢。空間索引會(huì)從所有維度來(lái)索引數(shù)據(jù),查詢時(shí)可以有效地使用任意維度來(lái)組合查詢。必須使用 MySQL 的 GIS 即地理信息系統(tǒng)的相關(guān)函數(shù)來(lái)維護(hù)數(shù)據(jù),但 MySQL 對(duì) GIS 的支持并不完善,因此大部分人都不會(huì)使用這個(gè)特性。
全文索引
通過(guò)數(shù)值比較、范圍過(guò)濾等就可以完成絕大多數(shù)需要的查詢,但如果希望通過(guò)關(guān)鍵字的匹配進(jìn)行查詢過(guò)濾,那么就需要基于相似度的查詢,而不是精確的數(shù)值比較,全文索引就是為這種場(chǎng)景設(shè)計(jì)的。全文索引有自己獨(dú)特的語(yǔ)法,沒(méi)有索引也可以工作,如果有索引效率會(huì)更高。
全文索引可以支持各種字符內(nèi)容的搜索,包括 CHAR、VARCHAR 和 TEXT 類型,也支持自然語(yǔ)言搜索和布爾搜索。在 MySQL 中全文索引有很多限制,例如表鎖對(duì)性能的影響、數(shù)據(jù)文件的崩潰恢復(fù)等,這使得 MyISAM 的全文索引對(duì)很多應(yīng)用場(chǎng)景并不合適。MyISAM 的全文索引作用對(duì)象是一個(gè)"全文集合",可能是某個(gè)數(shù)據(jù)表的一列,也可能是多個(gè)列。具體的對(duì)某一條記錄,MySQL 會(huì)將需要索引的列全部拼接成一個(gè)字符串然后進(jìn)行索引。
MyISAM 的全文索引是一種特殊的 B-Tree 索引,一共有兩層。第一層是所有關(guān)鍵字,然后對(duì)于每一個(gè)關(guān)鍵字的第二層,包含的是一組相關(guān)的"文檔指針"。全文索引不會(huì)索引文檔對(duì)象中的所有詞語(yǔ),它會(huì)根據(jù)規(guī)則過(guò)濾掉一些詞語(yǔ),例如停用詞列表中的詞都不會(huì)被索引。
聚簇索引
聚簇索引不是一種索引類型,而是一種數(shù)據(jù)存儲(chǔ)方式。InnoDB 的聚簇索引實(shí)際上在同一個(gè)結(jié)構(gòu)中保存了 B-Tree 索引和數(shù)據(jù)行。當(dāng)表有聚餐索引時(shí),它的行數(shù)據(jù)實(shí)際上存放在索引的葉子頁(yè)中,因?yàn)闊o(wú)法同時(shí)把數(shù)據(jù)行存放在兩個(gè)不同的地方,所以一個(gè)表只能有一個(gè)聚簇索引。
優(yōu)點(diǎn):① 可以把相關(guān)數(shù)據(jù)保存在一起,例如實(shí)現(xiàn)電子郵箱時(shí)可以根據(jù)用戶 ID 聚集數(shù)據(jù),這樣只需要從磁盤讀取少數(shù)數(shù)據(jù)頁(yè)就能獲取某個(gè)用戶的全部郵件,如果沒(méi)有使用聚簇索引,每封郵件可能都導(dǎo)致一次磁盤 IO。② 數(shù)據(jù)訪問(wèn)更快,聚簇索引將索引和數(shù)據(jù)保存在同一個(gè) B-Tree 中,因此獲取數(shù)據(jù)比非聚簇索引要更快。③ 使用覆蓋索引掃描的查詢可以直接使用頁(yè)節(jié)點(diǎn)中的主鍵值。
缺點(diǎn):① 聚簇索引最大限度提高了 IO 密集型應(yīng)用的性能,如果數(shù)據(jù)全部在內(nèi)存中將會(huì)失去優(yōu)勢(shì)。② 插入速度驗(yàn)證依賴于插入順序,按照主鍵的順序插入是加載數(shù)據(jù)到 InnoDB 引擎最快的方式。③ 更新聚簇索引列的代價(jià)很高,因?yàn)闀?huì)強(qiáng)制每個(gè)被更新的行移動(dòng)到新位置。④ 基于聚簇索引的表插入新行或主鍵被更新導(dǎo)致行移動(dòng)時(shí),可能導(dǎo)致頁(yè)分裂,表會(huì)占用更多磁盤空間。④ 當(dāng)行稀疏或由于頁(yè)分裂導(dǎo)致數(shù)據(jù)存儲(chǔ)不連續(xù)時(shí),全表掃描可能很慢。
覆蓋索引
覆蓋索引指一個(gè)索引包含或覆蓋了所有需要查詢的字段的值,不再需要根據(jù)索引回表查詢數(shù)據(jù)。覆蓋索引必須要存儲(chǔ)索引列的值,因此 MySQL 只能使用 B-Tree 索引做覆蓋索引。
優(yōu)點(diǎn):① 索引條目通常遠(yuǎn)小于數(shù)據(jù)行大小,可以極大減少數(shù)據(jù)訪問(wèn)量。② 因?yàn)樗饕凑樟兄淀樞虼鎯?chǔ),所以對(duì)于 IO 密集型防偽查詢回避隨機(jī)從磁盤讀取每一行數(shù)據(jù)的 IO 少得多。③ 由于 InnoDB 使用聚簇索引,覆蓋索引對(duì) InnoDB 很有幫助。InnoDB 的二級(jí)索引在葉子節(jié)點(diǎn)保存了行的主鍵值,如果二級(jí)主鍵能覆蓋查詢那么可以避免對(duì)主鍵索引的二次查詢。
P10:索引使用原則
建立索引
對(duì)查詢頻次較高,且數(shù)據(jù)量比較大的表建立索引。索引字段的選擇,最佳候選列應(yīng)當(dāng)從 WHERE 子句的條件中提取,如果 WHERE 子句中的組合比較多,那么應(yīng)當(dāng)挑選最常用、過(guò)濾效果最好的列的組合。業(yè)務(wù)上具有唯一特性的字段,即使是多個(gè)字段的組合,也必須建成唯一索引。
使用前綴索引
索引列開(kāi)始的部分字符,索引創(chuàng)建后也是使用硬盤來(lái)存儲(chǔ)的,因此短索引可以提升索引訪問(wèn)的 IO 效率。對(duì)于 BLOB、TEXT 或很長(zhǎng)的 VARCHAR 列必須使用前綴索引,MySQL 不允許索引這些列的完整長(zhǎng)度。前綴索引是一種能使索引更小更快的有效方法,但缺點(diǎn)是 MySQL 無(wú)法使用前綴索引做 ORDER BY 和 GROUP BY,也無(wú)法使用前綴索引做覆蓋掃描。
選擇合適的索引順序
當(dāng)不需要考慮排序和分組時(shí),將選擇性最高的列放在前面。索引的選擇性是指不重復(fù)的索引值和數(shù)據(jù)表的記錄總數(shù)之比,索引的選擇性越高則查詢效率越高,唯一索引的選擇性是 1,因此也可以使用唯一索引提升查詢效率。
刪除無(wú)用索引
MySQL 允許在相同列上創(chuàng)建多個(gè)索引,重復(fù)的索引需要單獨(dú)維護(hù),并且優(yōu)化器在優(yōu)化查詢時(shí)也需要逐個(gè)考慮,這會(huì)影響性能。重復(fù)索引是指在相同的列上按照相同的順序創(chuàng)建的相同類型的索引,應(yīng)該避免創(chuàng)建重復(fù)索引。如果創(chuàng)建了索引 (A,B) 再創(chuàng)建索引 (A) 就是冗余索引,因?yàn)檫@只是前一個(gè)索引的前綴索引,對(duì)于 B-Tree 索引來(lái)說(shuō)是冗余的。解決重復(fù)索引和冗余索引的方法就是刪除這些索引。除了重復(fù)索引和冗余索引,可能還會(huì)有一些服務(wù)器永遠(yuǎn)不用的索引,也應(yīng)該考慮刪除。
減少碎片
B-Tree 索引可能會(huì)碎片化,碎片化的索引可能會(huì)以很差或無(wú)序的方式存儲(chǔ)在磁盤上,這會(huì)降低查詢的效率。表的數(shù)據(jù)存儲(chǔ)也可能碎片化,包括行碎片、行間碎片、剩余空間碎片,對(duì)于 MyISAM 這三類碎片化都有可能發(fā)生,對(duì)于 InnoDB 不會(huì)出現(xiàn)短小的行碎片,它會(huì)移動(dòng)短小的行重寫到一個(gè)片段中。可以通過(guò)執(zhí)行 OPTIMIZE TABLE 或者導(dǎo)出再導(dǎo)入的方式重新整理數(shù)據(jù),對(duì)于 MyISAM 可以通過(guò)排序重建索引消除碎片。InnoDB 可以通過(guò)先刪除再重新創(chuàng)建索引的方式消除索引碎片。
索引失效情況
如果索引列出現(xiàn)了隱式類型轉(zhuǎn)換,則 MySQL 不會(huì)使用索引。常見(jiàn)的情況是在 SQL 的 WHERE 條件中字段類型為字符串,其值為數(shù)值,如果沒(méi)有加引號(hào)那么 MySQL 不會(huì)使用索引。
如果 WHERE 條件中含有 OR,除非 OR 前使用了索引列而 OR 之后是非索引列,索引會(huì)失效。
MySQL 不能在索引中執(zhí)行 LIKE 操作,這是底層存儲(chǔ)引擎 API 的限制,最左匹配的 LIKE 比較會(huì)被轉(zhuǎn)換為簡(jiǎn)單的比較操作,但如果是以通配符開(kāi)頭的 LIKE 查詢,存儲(chǔ)引擎就無(wú)法做筆記。這種情況下 MySQL 服務(wù)器只能提取數(shù)據(jù)行的值而不是索引值來(lái)做比較。
如果查詢中的列不是獨(dú)立的,則 MySQL 不會(huì)使用索引。獨(dú)立的列是指索引列不能是表達(dá)式的一部分,也不能是函數(shù)的參數(shù)。
對(duì)于多個(gè)范圍條件查詢,MySQL 無(wú)法使用第一個(gè)范圍列后面的其他索引列,對(duì)于多個(gè)等值查詢則沒(méi)有這種限制。
如果 MySQL 判斷全表掃描比使用索引查詢更快,則不會(huì)使用索引。
頁(yè)面搜索嚴(yán)禁左模糊或者全模糊,如果需要請(qǐng)走搜索引擎來(lái)解決。索引文件具有 B-Tree 的最左前綴匹配特性,如果左邊的值未確定,那么無(wú)法使用此索引
P11:優(yōu)化數(shù)據(jù)類型
更小的通常更好
一般情況下盡量使用可以正確存儲(chǔ)數(shù)據(jù)的最小數(shù)據(jù)類型,更小的數(shù)據(jù)類型通常也更快,因?yàn)樗鼈冋加酶俚拇疟P、內(nèi)存和 CPU 緩存。
盡可能簡(jiǎn)單
簡(jiǎn)單數(shù)據(jù)類型的操作通常需要更少的 CPU 周期,例如整數(shù)比字符操作代價(jià)更低,因?yàn)樽址托?duì)規(guī)則使字符相比整形更復(fù)雜。應(yīng)該使用 MySQL 的內(nèi)建類型 date、time 和 datetime 而不是字符串來(lái)存儲(chǔ)日期和時(shí)間,另一點(diǎn)是應(yīng)該使用整形存儲(chǔ) IP 地址。
盡量避免 NULL
通常情況下最好指定列為 NOT NULL,除非需要存儲(chǔ) NULL值。因?yàn)槿绻樵冎邪蔀?NULL 的列對(duì) MySQL 來(lái)說(shuō)更難優(yōu)化,可為 NULL 的列使索引、索引統(tǒng)計(jì)和值比較都更復(fù)雜,并且會(huì)使用更多存儲(chǔ)空間。當(dāng)可為 NULL 的列被索引時(shí),每個(gè)索引記錄需要一個(gè)額外字節(jié),在MyISAM 中還可能導(dǎo)致固定大小的索引變成可變大小的索引。
通常把可為 NULL 的列設(shè)置為 NOT NULL 帶來(lái)的性能提升較小,因此調(diào)優(yōu)時(shí)沒(méi)必要首先查找并修改這種情況。但如果計(jì)劃在列上建索引,就應(yīng)該盡量避免設(shè)計(jì)成可為 NULL 的列。
在為列選擇數(shù)據(jù)類型時(shí),第一步需要確定合適的大類型:數(shù)字、字符串、時(shí)間等。下一步是選擇具體類型,很多 MySQL 數(shù)據(jù)類型可以存儲(chǔ)相同類型的數(shù)據(jù),只是存儲(chǔ)的長(zhǎng)度和范圍不一樣,允許的精度不同或需要的物理空間不同。
P12:優(yōu)化查詢概述
優(yōu)化數(shù)據(jù)訪問(wèn)
如果把查詢看作一個(gè)任務(wù),那么它由一系列子任務(wù)組成,每個(gè)子任務(wù)都會(huì)消耗一定時(shí)間。如果要優(yōu)化查詢,要么消除一些子任務(wù),要么減少子任務(wù)的執(zhí)行次數(shù)。查詢性能低下最基本的原因是訪問(wèn)的數(shù)據(jù)太多,大部分性能低下的查詢都可以通過(guò)減少訪問(wèn)的數(shù)據(jù)量進(jìn)行優(yōu)化。可以通過(guò)以下兩個(gè)步驟分析。
是否向數(shù)據(jù)庫(kù)請(qǐng)求了不需要的數(shù)據(jù):有些查詢會(huì)請(qǐng)求超過(guò)實(shí)際需要的數(shù)據(jù),然后這些多余的數(shù)據(jù)會(huì)被應(yīng)用程序丟棄,這會(huì)給 MySQL 服務(wù)器造成額外負(fù)擔(dān)并增加網(wǎng)絡(luò)開(kāi)銷,另外也會(huì)消耗應(yīng)用服務(wù)器的 CPU 和內(nèi)存資源。例如多表關(guān)聯(lián)時(shí)返回全部列,取出全部列會(huì)讓優(yōu)化器無(wú)法完成索引覆蓋掃描這類優(yōu)化,還會(huì)為服務(wù)器帶來(lái)額外的 IO、內(nèi)存和 CPU 的消耗,因此使用 SELECT * 時(shí)需要仔細(xì)考慮是否真的需要返回全部列。再例如總是重復(fù)查詢相同的數(shù)據(jù),比較好的解決方案是初次查詢時(shí)將數(shù)據(jù)緩存起來(lái),需要的時(shí)候從緩存中取出。
MySQL 是否在掃描額外的記錄:在確定查詢只返回需要的數(shù)據(jù)后,應(yīng)該看看查詢?yōu)榱朔祷亟Y(jié)果是否掃描了過(guò)多的數(shù)據(jù),最簡(jiǎn)單的三個(gè)衡量指標(biāo)時(shí)響應(yīng)時(shí)間、掃描的行數(shù)和返回的行數(shù)。如果發(fā)現(xiàn)查詢需要掃描大量數(shù)據(jù)但只返回少數(shù)的行,可以使用以下手動(dòng)優(yōu)化:① 使用覆蓋索引掃描,把所有需要用的列都放到索引中,這樣存儲(chǔ)引擎無(wú)需回表查詢對(duì)應(yīng)行就可以返回結(jié)果。② 改變庫(kù)表結(jié)構(gòu)。 ③ 重寫這個(gè)復(fù)雜的查詢,讓 MySQL 優(yōu)化器能夠以更優(yōu)化的方式執(zhí)行這個(gè)查詢。
重構(gòu)查詢方式
在優(yōu)化有問(wèn)題的查詢時(shí),目標(biāo)應(yīng)該是找到一個(gè)更優(yōu)的方法獲取實(shí)際需要的結(jié)果,而不一定總是需要從 MySQL 獲取一模一樣的結(jié)果集。
切分查詢:有時(shí)候?qū)τ谝粋€(gè)大查詢可以將其切分成小查詢,每個(gè)查詢功能完全一樣,只完成一小部分,每次只返回一小部分查詢結(jié)果。例如刪除舊數(shù)據(jù),定期清除大量數(shù)據(jù)時(shí),如果用一個(gè)大的語(yǔ)句一次性完成的話可能需要一次鎖住很多數(shù)據(jù)、占滿整個(gè)事務(wù)日志、耗盡系統(tǒng)資源、阻塞很多小的但重要的查詢。將一個(gè)大的 DELETE 語(yǔ)句切分成多個(gè)較小的查詢可以盡可能小地影響 MySQL 的性能,同時(shí)還可以減少M(fèi)ySQL 復(fù)制的延遲。
分解關(guān)聯(lián)查詢:很多高性能應(yīng)用都會(huì)對(duì)關(guān)聯(lián)查詢進(jìn)行分解,可以對(duì)每一個(gè)表進(jìn)行單表查詢,然后將結(jié)果在應(yīng)用程序中進(jìn)行關(guān)聯(lián)。分解關(guān)聯(lián)查詢可以讓緩存的效率更高、減少鎖的競(jìng)爭(zhēng)、提升查詢效率、還可以減少冗余記錄的查詢。
P13:查詢執(zhí)行流程
簡(jiǎn)單來(lái)說(shuō)分為五步:① 客戶端發(fā)送一條查詢給服務(wù)器。② 服務(wù)器先檢查查詢緩存,如果命中了緩存則立刻返回存儲(chǔ)在緩存中的結(jié)果,否則進(jìn)入下一階段。③ 服務(wù)器端進(jìn)行 SQL 解析、預(yù)處理,再由優(yōu)化器生成對(duì)應(yīng)的執(zhí)行計(jì)劃。④ MySQL 根據(jù)優(yōu)化器生成的執(zhí)行計(jì)劃,調(diào)用存儲(chǔ)引擎的 API 來(lái)執(zhí)行查詢。⑤ 將結(jié)果返回給客戶端。
查詢緩存
在解析一個(gè)查詢語(yǔ)句之前,如果查詢緩存是打開(kāi)的,那么 MySQL 會(huì)優(yōu)先檢查這個(gè)查詢是否命中查詢緩存中的數(shù)據(jù)。這個(gè)檢查是通過(guò)一個(gè)對(duì)大小寫敏感的哈希查找實(shí)現(xiàn)的。查詢和緩存中的查詢即使只有一個(gè)字節(jié)不同,也不會(huì)匹配緩存結(jié)果,這種情況下會(huì)進(jìn)行下一個(gè)階段的處理。如果當(dāng)前的查詢恰好命中了查詢緩存,那么在返回查詢結(jié)果之前 MySQL 會(huì)檢查一次用戶權(quán)限。如果權(quán)限沒(méi)有問(wèn)題,MySQL 會(huì)跳過(guò)其他階段,直接從緩沖中拿到結(jié)果并返回給客戶端,這種情況下查詢不會(huì)被解析,不用生成執(zhí)行計(jì)劃,不會(huì)被執(zhí)行。
查詢優(yōu)化處理
該階段包括多個(gè)子階段:解析 SQL、預(yù)處理、優(yōu)化 SQL 執(zhí)行計(jì)劃。首先 MySQL 通過(guò)關(guān)鍵字將 SQL 語(yǔ)句進(jìn)行解析,并生成一顆對(duì)應(yīng)的解析樹(shù),MySQL 解析器將使用 MySQL 語(yǔ)法規(guī)則驗(yàn)證和解析查詢。例如它將驗(yàn)證是否使用了錯(cuò)誤的關(guān)鍵字,或者使用關(guān)鍵字的順序是否正確等。預(yù)處理器則根據(jù)一些 MySQL 規(guī)則進(jìn)一步檢查解析樹(shù)是否合法,例如檢查數(shù)據(jù)表和數(shù)據(jù)列是否存在,還會(huì)解析名字和別名看它們是否有歧義。下一步預(yù)處理器會(huì)驗(yàn)證權(quán)限,這一步通常很快,除非服務(wù)器上有非常多的權(quán)限配置。
語(yǔ)法樹(shù)被認(rèn)為合法后,查詢優(yōu)化器將其轉(zhuǎn)成執(zhí)行計(jì)劃。一條查詢可以有多種查詢方式,最后都返回相同的結(jié)果,優(yōu)化器的作用就是找到這其中最好的執(zhí)行計(jì)劃。MySQL 使用基于成本的優(yōu)化器,它將嘗試預(yù)測(cè)一個(gè)查詢使用某種執(zhí)行計(jì)劃時(shí)的成本,并選擇其中成本最小的一個(gè)。優(yōu)化策略可以簡(jiǎn)單分為兩種,一種是靜態(tài)優(yōu)化,可以直接對(duì)解析樹(shù)分析并完成優(yōu)化,不依賴于特別的數(shù)值,可以認(rèn)為是一種編譯時(shí)優(yōu)化。另一種是動(dòng)態(tài)優(yōu)化,和查詢的上下文有關(guān),每次查詢時(shí)都需要重新評(píng)估。
MySQL 可以處理的優(yōu)化類型包括:重新定義表的關(guān)聯(lián)順序、將外連接轉(zhuǎn)化成內(nèi)連接、使用等價(jià)變換規(guī)則、優(yōu)化 COUNT() 和 MIN() 以及 MAX() 函數(shù)、預(yù)估并轉(zhuǎn)為常數(shù)表達(dá)式、覆蓋索引掃描、子查詢優(yōu)化等。
查詢執(zhí)行引擎
在解析和優(yōu)化階段,MySQL 將生成查詢對(duì)應(yīng)的執(zhí)行計(jì)劃,MySQL 的查詢執(zhí)行引擎則根據(jù)這個(gè)計(jì)劃來(lái)完成整個(gè)查詢。執(zhí)行計(jì)劃是一個(gè)數(shù)據(jù)結(jié)構(gòu),而不是其他關(guān)系型數(shù)據(jù)庫(kù)那樣會(huì)生成對(duì)應(yīng)的字節(jié)碼。查詢執(zhí)行階段并不復(fù)雜,MySQL 只是簡(jiǎn)單的根據(jù)執(zhí)行計(jì)劃給出的指令逐步執(zhí)行,再根據(jù)執(zhí)行計(jì)劃執(zhí)行的過(guò)程中,有大量操作需要通過(guò)調(diào)用存儲(chǔ)引擎實(shí)現(xiàn)的接口來(lái)完成。
返回結(jié)果給客戶端
查詢執(zhí)行的最后一個(gè)階段是將結(jié)果返回給客戶端,即使查詢不需要返回結(jié)果集,MySQL 仍然會(huì)返回這個(gè)查詢的一些信息,如該查詢影響到的行數(shù)。如果查詢可以被緩存,那么 MySQL 會(huì)在這個(gè)階段將結(jié)果存放到查詢緩沖中。MySQL 將結(jié)果集返回客戶端是一個(gè)增量、逐步返回的過(guò)程,這樣做的好處是服務(wù)器無(wú)需存儲(chǔ)太多的結(jié)果,減少內(nèi)存消耗,也可以讓客戶端第一時(shí)間獲得響應(yīng)結(jié)果。結(jié)果集中的每一行給都會(huì)以一個(gè)滿足 MySQL 客戶端/服務(wù)器通信協(xié)議的包發(fā)送,再通過(guò) TCP 協(xié)議進(jìn)行傳輸,在 TCP 傳輸過(guò)程中可能對(duì)包進(jìn)行緩存然后批量傳輸。
P14:優(yōu)化 SQL
定位低效 SQL
可以通過(guò)兩種方式來(lái)定位執(zhí)行效率較低的 SQL 語(yǔ)句。一種是通過(guò)慢查詢?nèi)罩径ㄎ?#xff0c;可以通過(guò)慢查詢?nèi)罩径ㄎ荒切┮呀?jīng)執(zhí)行完畢的 SQL 語(yǔ)句。另一種是使用 SHOW PROCESSLIST 查詢,慢查詢?nèi)罩驹诓樵兘Y(jié)束以后才記錄,所以在應(yīng)用反應(yīng)執(zhí)行效率出現(xiàn)問(wèn)題的時(shí)候查詢慢查詢?nèi)罩静荒芏ㄎ粏?wèn)題,此時(shí)可以使用 SHOW PROCESSLIST 命令查看當(dāng)前 MySQL 正在進(jìn)行的線程,包括線程的狀態(tài)、是否鎖表等,可以實(shí)時(shí)查看 SQL 的執(zhí)行情況,同時(shí)對(duì)一些鎖表操作進(jìn)行優(yōu)化。找到執(zhí)行效率低的 SQL 語(yǔ)句后,就可以通過(guò) SHOW PROFILE、EXPLAIN 或 trace 等豐富來(lái)繼續(xù)優(yōu)化語(yǔ)句。
SHOW PROFILE
通過(guò) SHOW PROFILE 可以分析 SQL 語(yǔ)句性能消耗,例如查詢到 SQL 會(huì)執(zhí)行多少時(shí)間,并顯示 CPU、內(nèi)存使用量,執(zhí)行過(guò)程中系統(tǒng)鎖及表鎖的花費(fèi)時(shí)間等信息。例如 SHOW PROFILE CPU/MEMORY/BLOCK IO FOR QUERY N 分別查詢 id 為 N 的 SQL 語(yǔ)句的 CPU、內(nèi)存以及 IO 的消耗情況。
TRACE
從 MySQL 5.6 版本開(kāi)始,可以通過(guò) trace 文件進(jìn)一步獲取優(yōu)化器是是如何選擇執(zhí)行計(jì)劃的,在使用時(shí)需要先打開(kāi)設(shè)置,然后執(zhí)行一次 SQL,最后查看 information_schema.optimizer_trace 表而都內(nèi)容,該表為聯(lián)合i表,只能在當(dāng)前會(huì)話進(jìn)行查詢,每次查詢后返回的都是最近一次執(zhí)行的 SQL 語(yǔ)句。
EXPLAIN
執(zhí)行計(jì)劃是 SQL 調(diào)優(yōu)的一個(gè)重要依據(jù),可以通過(guò) EXPLAIN 命令查看 SQL 語(yǔ)句的執(zhí)行計(jì)劃,如果作用在表上,那么該命令相當(dāng)于 DESC。EXPLAIN 的指標(biāo)及含義如下:
指標(biāo)名 含義
id 表示 SELECT 子句或操作表的順序,執(zhí)行順序從大到小執(zhí)行,當(dāng) id 一樣時(shí),執(zhí)行順序從上往下。
select_type 表示查詢中每個(gè) SELECT 子句的類型,例如 SIMPLE 表示不包含子查詢、表連接或其他復(fù)雜語(yǔ)法的簡(jiǎn)單查詢,PRIMARY 表示復(fù)雜查詢的最外層查詢,SUBQUERY 表示在 SELECT 或 WHERE 列表中包含了子查詢。
type 表示訪問(wèn)類型,性能由差到好為:ALL 全表掃描、index 索引全掃描、range 索引范圍掃描、ref 返回匹配某個(gè)單獨(dú)值得所有行,常見(jiàn)于使用非唯一索引或唯一索引的非唯一前綴進(jìn)行的查找,也經(jīng)常出現(xiàn)在 join 操作中、eq_ref 唯一性索引掃描,對(duì)于每個(gè)索引鍵只有一條記錄與之匹配、const 當(dāng) MySQL 對(duì)查詢某部分進(jìn)行優(yōu)化,并轉(zhuǎn)為一個(gè)常量時(shí),使用這些訪問(wèn)類型,例如將主鍵或唯一索引置于 WHERE 列表就能將該查詢轉(zhuǎn)為一個(gè) const、system 表中只有一行數(shù)據(jù)或空表,只能用于 MyISAM 和 Memory 表、NULL 執(zhí)行時(shí)不用訪問(wèn)表或索引就能得到結(jié)果。SQL 性能優(yōu)化的目標(biāo):至少要達(dá)到 range 級(jí)別,要求是 ref 級(jí)別,如果可以是consts 最好。
possible_keys 表示查詢時(shí)可能用到的索引,但不一定使用。列出大量可能索引時(shí)意味著備選索引數(shù)量太多了。
key 顯示 MySQL 在查詢時(shí)實(shí)際使用的索引,如果沒(méi)有使用則顯示為 NULL。
key_len 表示使用到索引字段的長(zhǎng)度,可通過(guò)該列計(jì)算查詢中使用的索引的長(zhǎng)度,對(duì)于確認(rèn)索引有效性以及多列索引中用到的列數(shù)目很重要。
ref 表示上述表的連接匹配條件,即哪些列或常量被用于查找索引列上的值。
rows 表示 MySQL 根據(jù)表統(tǒng)計(jì)信息及索引選用情況,估算找到所需記錄所需要讀取的行數(shù)。
Extra 表示額外信息,例如 Using temporary 表示需要使用臨時(shí)表存儲(chǔ)結(jié)果集,常見(jiàn)于排序和分組查詢。Using filesort 表示無(wú)法利用索引完成的文件排序,這是 ORDER BY 的結(jié)果,可以通過(guò)合適的索引改進(jìn)性能。Using index 表示只需要使用索引就可以滿足查詢表得要求,說(shuō)明表正在使用覆蓋索引。
優(yōu)化 COUNT 查詢
COUNT 是一個(gè)特殊的函數(shù),它可以統(tǒng)計(jì)某個(gè)列值的數(shù)量,在統(tǒng)計(jì)列值時(shí)要求列值是非空的,不會(huì)統(tǒng)計(jì) NULL 值。如果在 COUNT 中指定了列或列的表達(dá)式,則統(tǒng)計(jì)的就是這個(gè)表達(dá)式有值的結(jié)果數(shù),而不是 NULL。
COUNT 的另一個(gè)作用是統(tǒng)計(jì)結(jié)果集的行數(shù),當(dāng) MySQL 確定括號(hào)內(nèi)的表達(dá)式不可能為 NULL 時(shí),實(shí)際上就是在統(tǒng)計(jì)行數(shù)。當(dāng)使用 COUNT() 時(shí), 不會(huì)擴(kuò)展成所有列,它會(huì)忽略所有的列而直接統(tǒng)計(jì)所有的行數(shù)。
某些業(yè)務(wù)場(chǎng)景并不要求完全精確的 COUNT 值,此時(shí)可以使用近似值來(lái)代替,EXPLAIN 出來(lái)的優(yōu)化器估算的行數(shù)就是一個(gè)不錯(cuò)的近似值,因?yàn)閳?zhí)行 EXPLAIN 并不需要真正地執(zhí)行查詢。
通常來(lái)說(shuō) COUNT 都需要掃描大量的行才能獲取精確的結(jié)果,因此很難優(yōu)化。在 MySQL 層還能做的就只有覆蓋掃描了,如果還不夠就需要修改應(yīng)用的架構(gòu),可以增加匯總表或者外部緩存系統(tǒng)。
優(yōu)化關(guān)聯(lián)查詢
確保 ON 或 USING 子句中的列上有索引,在創(chuàng)建索引時(shí)就要考慮到關(guān)聯(lián)的順序。
確保任何 GROUP BY 和 ORDER BY 的表達(dá)式只涉及到一個(gè)表中的列,這樣 MySQL 才有可能使用索引來(lái)優(yōu)化這個(gè)過(guò)程。
在 MySQL 5.5 及以下版本盡量避免子查詢,可以用關(guān)聯(lián)查詢代替,因?yàn)閳?zhí)行器會(huì)先執(zhí)行外部的 SQL 再執(zhí)行內(nèi)部的 SQL。
優(yōu)化 GROUP BY
如果沒(méi)有通過(guò) ORDER BY 子句顯式指定要排序的列,當(dāng)查詢使用 GROUP BY 子句的時(shí)候,結(jié)果集會(huì)自動(dòng)按照分組的字段進(jìn)行排序,如果不關(guān)心結(jié)果集的順序,可以使用 ORDER BY NULL 禁止排序。
優(yōu)化 LIMIT 分頁(yè)
在偏移量非常大的時(shí)候,需要查詢很多條數(shù)據(jù)再舍棄,這樣的代價(jià)非常高。要優(yōu)化這種查詢,要么是在頁(yè)面中限制分頁(yè)的數(shù)量,要么是優(yōu)化大偏移量的性能。最簡(jiǎn)單的辦法是盡可能地使用覆蓋索引掃描,而不是查詢所有的列,然后根據(jù)需要做一次關(guān)聯(lián)操作再返回所需的列。
還有一種方法是從上一次取數(shù)據(jù)的位置開(kāi)始掃描,這樣就可以避免使用 OFFSET。其他優(yōu)化方法還包括使用預(yù)先計(jì)算的匯總表,或者關(guān)聯(lián)到一個(gè)冗余表,冗余表只包含主鍵列和需要做排序的數(shù)據(jù)列。
優(yōu)化 UNION 查詢
MySQL 通過(guò)創(chuàng)建并填充臨時(shí)表的方式來(lái)執(zhí)行 UNION 查詢,除非確實(shí)需要服務(wù)器消除重復(fù)的行,否則一定要使用 UNION ALL,如果沒(méi)有 ALL 關(guān)鍵字,MySQL 會(huì)給臨時(shí)表加上 DISTINCT 選項(xiàng),這會(huì)導(dǎo)致對(duì)整個(gè)臨時(shí)表的數(shù)據(jù)做唯一性檢查,這樣做的代價(jià)非常高。
使用用戶自定義變量
在查詢中混合使用過(guò)程化和關(guān)系化邏輯的時(shí)候,自定義變量可能會(huì)非常有用。用戶自定義變量是一個(gè)用來(lái)存儲(chǔ)內(nèi)容的臨時(shí)容器,在連接 MySQL 的整個(gè)過(guò)程中都存在,可以在任何可以使用表達(dá)式的地方使用自定義變量。例如可以使用變量來(lái)避免重復(fù)查詢剛剛更新過(guò)的數(shù)據(jù)、統(tǒng)計(jì)更新和插入的數(shù)量等。
優(yōu)化 INSERT
需要對(duì)一張表插入很多行數(shù)據(jù)時(shí),應(yīng)該盡量使用一次性插入多個(gè)值的 INSERT 語(yǔ)句,這種方式將縮減客戶端與數(shù)據(jù)庫(kù)之間的連接、關(guān)閉等消耗,效率比多條插入單個(gè)值的 INSERT 語(yǔ)句高。也可以關(guān)閉事務(wù)的自動(dòng)提交,在插入完數(shù)據(jù)后提交。當(dāng)插入的數(shù)據(jù)是按主鍵的順序插入時(shí),效率更高。
P15:復(fù)制
復(fù)制解決的基本問(wèn)題是讓一臺(tái)服務(wù)器的數(shù)據(jù)與其他服務(wù)器保持同步,一臺(tái)主庫(kù)的數(shù)據(jù)可以同步到多臺(tái)備庫(kù)上,備庫(kù)本身也可以被配置成另外一臺(tái)服務(wù)器的主庫(kù)。主庫(kù)和備庫(kù)之間可以有多種不同的組合方式。
MySQL 支持兩種復(fù)制方式:基于行的復(fù)制和基于語(yǔ)句的復(fù)制,基于語(yǔ)句的復(fù)制也稱為邏輯復(fù)制,從 MySQL 3.23 版本就已存在,基于行的復(fù)制方式在 5.1 版本才被加進(jìn)來(lái)。這兩種方式都是通過(guò)在主庫(kù)上記錄二進(jìn)制日志、在備庫(kù)重放日志的方式來(lái)實(shí)現(xiàn)異步的數(shù)據(jù)復(fù)制。因此同一時(shí)刻備庫(kù)的數(shù)據(jù)可能與主庫(kù)存在不一致,并且無(wú)法包裝主備之間的延遲。
MySQL 復(fù)制大部分是向后兼容的,新版本的服務(wù)器可以作為老版本服務(wù)器的備庫(kù),但是老版本不能作為新版本服務(wù)器的備庫(kù),因?yàn)樗赡軣o(wú)法解析新版本所用的新特性或語(yǔ)法,另外所使用的二進(jìn)制文件格式也可能不同。
復(fù)制解決的問(wèn)題:數(shù)據(jù)分布、負(fù)載均衡、備份、高可用性和故障切換、MySQL 升級(jí)測(cè)試。
復(fù)制步驟
概述:① 在主庫(kù)上把數(shù)據(jù)更改記錄到二進(jìn)制日志中。② 備庫(kù)將主庫(kù)的日志復(fù)制到自己的中繼日志中。 ③ 備庫(kù)讀取中繼日志中的事件,將其重放到備庫(kù)數(shù)據(jù)之上。
第一步是在主庫(kù)上記錄二進(jìn)制日志,每次準(zhǔn)備提交事務(wù)完成數(shù)據(jù)更新前,主庫(kù)將數(shù)據(jù)更新的事件記錄到二進(jìn)制日志中。MySQL 會(huì)按事務(wù)提交的順序而非每條語(yǔ)句的執(zhí)行順序來(lái)記錄二進(jìn)制日志,在記錄二進(jìn)制日志后,主庫(kù)會(huì)告訴存儲(chǔ)引擎可以提交事務(wù)了。
下一步,備庫(kù)將主庫(kù)的二進(jìn)制日志復(fù)制到其本地的中繼日志中。備庫(kù)首先會(huì)啟動(dòng)一個(gè)工作的 IO 線程,IO 線程跟主庫(kù)建立一個(gè)普通的客戶端連接,然后在主庫(kù)上啟動(dòng)一個(gè)特殊的二進(jìn)制轉(zhuǎn)儲(chǔ)線程,這個(gè)線程會(huì)讀取主庫(kù)上二進(jìn)制日志中的事件。它不會(huì)對(duì)事件進(jìn)行輪詢。如果該線程追趕上了主庫(kù)將進(jìn)入睡眠狀態(tài),直到主庫(kù)發(fā)送信號(hào)量通知其有新的事件產(chǎn)生時(shí)才會(huì)被喚醒,備庫(kù) IO 線程會(huì)將接收到的事件記錄到中繼日志中。
備庫(kù)的 SQL 線程執(zhí)行最后一步,該線程從中繼日志中讀取事件并在備庫(kù)執(zhí)行,從而實(shí)現(xiàn)備庫(kù)數(shù)據(jù)的更新。當(dāng) SQL 線程追趕上 IO 線程時(shí),中繼日志通常已經(jīng)在系統(tǒng)緩存中,所以中繼日志的開(kāi)銷很低。SQL 線程執(zhí)行的時(shí)間也可以通過(guò)配置選項(xiàng)來(lái)決定是否寫入其自己的二進(jìn)制日志中。
這種復(fù)制架構(gòu)實(shí)現(xiàn)了獲取事件和重放事件的解耦,允許這兩個(gè)過(guò)程異步進(jìn)行,也就是說(shuō) IO 線程能夠獨(dú)立于 SQL 線程工作。但這種架構(gòu)也限制了復(fù)制的過(guò)程,在主庫(kù)上并發(fā)允許的查詢?cè)趥鋷?kù)只能串行化執(zhí)行,因?yàn)橹挥幸粋€(gè) SQL 線程來(lái)重放中繼日志中的事件。
Redis 10
P1:特點(diǎn)
基于鍵值對(duì)的數(shù)據(jù)結(jié)構(gòu)服務(wù)器
Redis 中的值不僅可以是字符串,還可以是具體的數(shù)據(jù)結(jié)構(gòu),這樣不僅能應(yīng)用于多種場(chǎng)景開(kāi)發(fā),也可以提高開(kāi)發(fā)效率。它主要提供五種數(shù)據(jù)結(jié)構(gòu):字符串、哈希、列表、集合、有序集合,同時(shí)在字符串的基礎(chǔ)上演變出了 Bitmaps 和 HyperLogLog 兩種數(shù)據(jù)結(jié)構(gòu),Redis 3.2 還加入了有關(guān) GEO 地理信息定位的功能。
豐富的功能
① 提供了鍵過(guò)期功能,可以實(shí)現(xiàn)緩存。② 提供了發(fā)布訂閱功能,可以實(shí)現(xiàn)消息系統(tǒng)。③ 支持 Lua 腳本,可以創(chuàng)造新的 Redis 命令。④ 提供了簡(jiǎn)單的事務(wù)功能,能在一定程度上保證事務(wù)特性。⑤ 提供了流水線功能,客戶端能將一批命令一次性傳到 Redis,減少網(wǎng)絡(luò)開(kāi)銷。
簡(jiǎn)單穩(wěn)定
Redis 的簡(jiǎn)單主要體現(xiàn)在三個(gè)方面:① 源碼很少,早期只有 2 萬(wàn)行左右,在 3.0 版本由于添加了集群特性,增加到了 5 萬(wàn)行左右,相對(duì)于很多 NoSQL 數(shù)據(jù)庫(kù)來(lái)說(shuō)代碼量要少很多。② 采用單線程模型,使得服務(wù)端處理模型更簡(jiǎn)單,也使客戶端開(kāi)發(fā)更簡(jiǎn)單。③ 不依賴底層操作系統(tǒng)的類庫(kù),自己實(shí)現(xiàn)了事件處理的相關(guān)功能。雖然 Redis 比較簡(jiǎn)單,但也很穩(wěn)定。
客戶端語(yǔ)言多
Redis 提供了簡(jiǎn)單的 TCP 通信協(xié)議,很多編程語(yǔ)言可以方便地接入 Redis,例如 Java、PHP、Python、C、C++ 等。
持久化
通常來(lái)說(shuō)數(shù)據(jù)放在內(nèi)存中是不安全的,一旦發(fā)生斷電或故障數(shù)據(jù)就可能丟失,因此 Redis 提供了兩種持久化方式 RDB 和 AOF 將內(nèi)存的數(shù)據(jù)保存到硬盤中。
數(shù)據(jù)結(jié)構(gòu)和內(nèi)部編碼
可以使用 type 命令查看當(dāng)前鍵的數(shù)據(jù)類型結(jié)構(gòu),它們分別是:string、hash、list、set、zset,但這些只是 Redis 對(duì)外的數(shù)據(jù)結(jié)構(gòu)。實(shí)際上每種數(shù)據(jù)結(jié)構(gòu)都有自己底層的內(nèi)部編碼實(shí)現(xiàn),這樣 Redis 會(huì)在合適的場(chǎng)景選擇合適的內(nèi)部編碼,string 包括了 raw、int 和 embstr,hash 包括了 hashtable 和 ziplist,list 包括了 linkedlist 和 ziplist,set 包括了 hashtable 和 intset,zset 包括了 skiplist 和 ziplist。可以使用 object encoding 查看內(nèi)部編碼。
Redis 這樣設(shè)計(jì)的好處是:① 可以改進(jìn)內(nèi)部編碼,而對(duì)外的數(shù)據(jù)結(jié)構(gòu)和命令沒(méi)有影響。② 多種內(nèi)部編碼實(shí)現(xiàn)可以在不同場(chǎng)景下發(fā)揮各自的優(yōu)勢(shì),例如 ziplist 比較節(jié)省內(nèi)存,但在列表元素較多的情況下性能有所下降,這時(shí) Redis 會(huì)根據(jù)配置選項(xiàng)將列表類型的內(nèi)部實(shí)現(xiàn)轉(zhuǎn)換為 linkedlist。
高性能
Redis 使用了單線程架構(gòu)和 IO 多路復(fù)用模型來(lái)實(shí)現(xiàn)高性能的內(nèi)存數(shù)據(jù)庫(kù)服務(wù)。
每次客戶端調(diào)用都經(jīng)歷了發(fā)送命令、執(zhí)行命令、返回結(jié)果三個(gè)過(guò)程,因?yàn)?Redis 是單線程處理命令的,所以一條命令從客戶端到達(dá)服務(wù)器不會(huì)立即執(zhí)行,所有命令都會(huì)進(jìn)入一個(gè)隊(duì)列中,然后逐個(gè)被執(zhí)行。客戶端的執(zhí)行順序可能不確定,但是可以確定不會(huì)有兩條命令被同時(shí)執(zhí)行,不存在并發(fā)問(wèn)題。
通常來(lái)說(shuō)單線程處理能力要比多線程差,Redis 快的原因:① 純內(nèi)存訪問(wèn),Redis 將所有數(shù)據(jù)放在內(nèi)存中。② 非阻塞 IO,Redis 使用 epoll 作為 IO 多路復(fù)用技術(shù)的實(shí)現(xiàn),再加上 Redis 本身的事件處理模型將 epoll 中的連接、讀寫、關(guān)閉都轉(zhuǎn)換為時(shí)間,不在網(wǎng)絡(luò) IO 上浪費(fèi)過(guò)多的時(shí)間。③ 單線程避免了線程切換和競(jìng)爭(zhēng)產(chǎn)生的消耗。單線程的一個(gè)問(wèn)題是對(duì)于每個(gè)命令的執(zhí)行時(shí)間是有要求的,如果某個(gè)命令執(zhí)行時(shí)間過(guò)長(zhǎng)會(huì)造成其他命令的阻塞,對(duì)于 Redis 這種高性能服務(wù)來(lái)說(shuō)是致命的,因此 Redis 是面向快速執(zhí)行場(chǎng)景的數(shù)據(jù)庫(kù)。
P2:字符串
字符串類型是 Redis 最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),鍵都是字符串類型,而且其他幾種數(shù)據(jù)結(jié)構(gòu)都是在字符串類型的基礎(chǔ)上構(gòu)建的。字符串類型的值可以實(shí)際可以是字符串(簡(jiǎn)單的字符串、復(fù)雜的字符串如 JSON、XML)、數(shù)字(整形、浮點(diǎn)數(shù))、甚至二進(jìn)制(圖片、音頻、視頻),但是值最大不能超過(guò) 512 MB。
設(shè)置值
set key value [ex seconds] [px millseconds] [nx|xx]
- ex seconds:為鍵設(shè)置秒級(jí)過(guò)期時(shí)間,跟 setex 效果一樣
- px millseconds:為鍵設(shè)置毫秒級(jí)過(guò)期時(shí)間
- nx:鍵必須不存在才可以設(shè)置成功,用于添加,跟 setnx 效果一樣。由于 Redis 的單線程命令處理機(jī)制,如果多個(gè)客戶端同時(shí)執(zhí)行,則只有一個(gè)客戶端能設(shè)置成功,可以用作分布式鎖的一種實(shí)現(xiàn)。
- xx:鍵必須存在才可以設(shè)置成功,用于更新
獲取值
get key,如果不存在返回 nil
批量設(shè)置值
mset key value [key value…]
批量獲取值
mget key [key…]
批量操作命令可以有效提高開(kāi)發(fā)效率,假如沒(méi)有 mget,執(zhí)行 n 次 get 命令需要 n 次網(wǎng)絡(luò)時(shí)間 + n 次命令時(shí)間,使用 mget 只需要 1 次網(wǎng)絡(luò)時(shí)間 + n 次命令時(shí)間。Redis 可以支持每秒數(shù)萬(wàn)的讀寫操作,但這指的是 Redis 服務(wù)端的處理能力,對(duì)于客戶端來(lái)說(shuō)一次命令處理命令時(shí)間還有網(wǎng)絡(luò)時(shí)間。因?yàn)?Redis 的處理能力已足夠高,對(duì)于開(kāi)發(fā)者來(lái)說(shuō),網(wǎng)絡(luò)可能會(huì)成為性能瓶頸。
計(jì)數(shù)
incr key
incr 命令用于對(duì)值做自增操作,返回結(jié)果分為三種:① 值不是整數(shù)返回錯(cuò)誤。② 值是整數(shù),返回自增后的結(jié)果。③ 值不存在,按照值為 0 自增,返回結(jié)果 1。除了 incr 命令,還有自減 decr、自增指定數(shù)字 incrby、自減指定數(shù)組 decrby、自增浮點(diǎn)數(shù) incrbyfloat。
內(nèi)部編碼
- int:8 個(gè)字節(jié)的長(zhǎng)整形
- embstr:小于等于 39 個(gè)字節(jié)的字符串
- raw:大于 39 個(gè)字節(jié)的字符串
典型使用場(chǎng)景
- 緩存功能
Redis 作為緩存層,MySQL 作為存儲(chǔ)層,首先從 Redis 獲取數(shù)據(jù),如果失敗就從 MySQL 獲取并將結(jié)果寫回 Redis 并添加過(guò)期時(shí)間。 - 計(jì)數(shù)
Redis 可以實(shí)現(xiàn)快速計(jì)數(shù)功能,例如視頻每播放一次就用 incy 把播放數(shù)加 1。 - 共享 Session
一個(gè)分布式 Web 服務(wù)將用戶的 Session 信息保存在各自服務(wù)器,但會(huì)造成一個(gè)問(wèn)題,出于負(fù)載均衡的考慮,分布式服務(wù)會(huì)將用戶的訪問(wèn)負(fù)載到不同服務(wù)器上,用戶刷新一次可能會(huì)發(fā)現(xiàn)需要重新登陸。為解決該問(wèn)題,可以使用 Redis 將用戶的 Session 進(jìn)行集中管理,在這種模式下只要保證 Redis 是高可用和擴(kuò)展性的,每次用戶更新或查詢登錄信息都直接從 Redis 集中獲取。 - 限速
例如為了短信接口不被頻繁訪問(wèn)會(huì)限制用戶每分鐘獲取驗(yàn)證碼的次數(shù)或者網(wǎng)站限制一個(gè) IP 地址不能在一秒內(nèi)訪問(wèn)超過(guò) n 次。可以使用鍵過(guò)期策略和自增計(jì)數(shù)實(shí)現(xiàn)。
P3:哈希
哈希類型是指鍵值本身又是一個(gè)鍵值對(duì)結(jié)構(gòu),哈希類型中的映射關(guān)系叫做 field-value,這里的 value 是指 field 對(duì)于的值而不是鍵對(duì)于的值。
設(shè)置值
hset key field value,如果設(shè)置成功會(huì)返回 1,反之會(huì)返回 0,此外還提供了 hsetnx 命令,作用和 setnx 類似,只是作用于由鍵變?yōu)?field。
獲取值
hget key field,如果不存在會(huì)返回 nil。
刪除 field
hdel key field [field…],會(huì)刪除一個(gè)或多個(gè) field,返回結(jié)果為刪除成功 field 的個(gè)數(shù)。
計(jì)算 field 個(gè)數(shù)
hlen key
批量設(shè)置或獲取 field-value
hmget key field [field...] hmset key field value [field value...]判斷 field 是否存在
hexists key field,存在返回 1,否則返回 0。
獲取所有的 field
hkeys key,返回指定哈希鍵的所有 field。
獲取所有 value
hvals key,獲取指定鍵的所有 value。
獲取所有的 field-value
hgetall key,獲取指定鍵的所有 field-value。
內(nèi)部編碼
- ziplist 壓縮列表:當(dāng)哈希類型元素個(gè)數(shù)和值小于配置值(默認(rèn) 512 個(gè)和 64 字節(jié))時(shí)會(huì)使用 ziplist 作為內(nèi)部實(shí)現(xiàn),使用更緊湊的結(jié)構(gòu)實(shí)現(xiàn)多個(gè)元素的連續(xù)存儲(chǔ),在節(jié)省內(nèi)存方面比 hashtable 更優(yōu)秀。
- hashtable 哈希表:當(dāng)哈希類型無(wú)法滿足 ziplist 的條件時(shí)會(huì)使用 hashtable 作為哈希的內(nèi)部實(shí)現(xiàn),因?yàn)榇藭r(shí) ziplist 的讀寫效率會(huì)下降,而 hashtable 的讀寫時(shí)間復(fù)雜度都為 O(1)。
使用場(chǎng)景
緩存用戶信息,有三種實(shí)現(xiàn):
- 原生字符串類型:每個(gè)屬性一個(gè)鍵。
優(yōu)點(diǎn):簡(jiǎn)單直觀,每個(gè)屬性都支持更新操作。
缺點(diǎn):占用過(guò)多的鍵,內(nèi)存占用量較大,用戶信息內(nèi)聚性差,一般不會(huì)在生產(chǎn)環(huán)境使用。 - 序列化字符串類型:將用戶信息序列化后用一個(gè)鍵保存。
優(yōu)點(diǎn):編程簡(jiǎn)單,如果合理使用序列化可以提高內(nèi)存使用率。
缺點(diǎn):序列化和反序列化有一定開(kāi)銷,同時(shí)每次更新屬性都需要把全部數(shù)據(jù)取出進(jìn)行反序列化,更新后再序列化到 Redis。 - 哈希類型:每個(gè)用戶屬性使用一對(duì) field-value,但只用一個(gè)鍵保存。
優(yōu)點(diǎn):簡(jiǎn)單直觀,如果合理使用可以減少內(nèi)存空間使用。
缺點(diǎn):要控制哈希在 ziplist 和 hashtable 兩種內(nèi)部編碼的轉(zhuǎn)換,hashtable 會(huì)消耗更多內(nèi)存。
P4:列表
列表類型是用來(lái)存儲(chǔ)多個(gè)有序的字符串,列表中的每個(gè)字符串稱為元素,一個(gè)列表最多可以存儲(chǔ) 232-1 個(gè)元素。可以對(duì)列表兩端插入(push)和彈出(pop),還可以獲取指定范圍的元素列表、獲取指定索引下標(biāo)的元素等。列表是一種比較靈活的數(shù)據(jù)結(jié)構(gòu),它可以充當(dāng)棧和隊(duì)列的角色,在實(shí)際開(kāi)發(fā)中有很多應(yīng)用場(chǎng)景。
列表類型有兩個(gè)特點(diǎn):① 列表中的元素是有序的,可以通過(guò)索引下標(biāo)獲取某個(gè)元素或者某個(gè)范圍內(nèi)的元素列表。② 列表中的元素可以重復(fù)。
添加操作
從右邊插入元素:rpush key value [value…]
從左到右獲取列表的所有元素:lrange 0 -1
從左邊插入元素:lpush key value [value…]
向某個(gè)元素前或者后插入元素:linsert key before|after pivot value,會(huì)在列表中找到等于 pivot 的元素,在其前或后插入一個(gè)新的元素 value。
查找
獲取指定范圍內(nèi)的元素列表:lrange key start end,索引從左到右的范圍是 0~N-1,從右到左是 -1~-N,lrange 中的 end 包含了自身。
獲取列表指定索引下標(biāo)的元素:lindex key index,獲取最后一個(gè)元素可以使用 lindex key -1。
獲取列表長(zhǎng)度:llen key
刪除
從列表左側(cè)彈出元素:lpop key
從列表右側(cè)彈出元素:rpop key
刪除指定元素:lrem key count value,如果 count 大于 0,從左到右刪除最多 count 個(gè)元素,如果 count 小于 0,從右到左刪除最多個(gè) count 絕對(duì)值個(gè)元素,如果 count 等于 0,刪除所有。
按照索引范圍修剪列表:ltrim key start end,只會(huì)保留 start ~ end 范圍的元素。
修改
修改指定索引下標(biāo)的元素:lset key index newValue。
阻塞操作
阻塞式彈出:blpop/brpop key [key…] timeout,timeout 表示阻塞時(shí)間。
當(dāng)列表為空時(shí),如果 timeout = 0,客戶端會(huì)一直阻塞,如果在此期間添加了元素,客戶端會(huì)立即返回。
如果是多個(gè)鍵,那么brpop會(huì)從左至右遍歷鍵,一旦有一個(gè)鍵能彈出元素,客戶端立即返回。
如果多個(gè)客戶端對(duì)同一個(gè)鍵執(zhí)行 brpop,那么最先執(zhí)行該命令的客戶端可以獲取彈出的值。
內(nèi)部編碼
- ziplist 壓縮列表:跟哈希的 zipilist 相同,元素個(gè)數(shù)和大小小于配置值(默認(rèn) 512 個(gè)和 64 字節(jié))時(shí)使用。
- linkedlist 鏈表:當(dāng)列表類型無(wú)法滿足 ziplist 的條件時(shí)會(huì)使用linkedlist。
Redis 3.2 提供了 quicklist 內(nèi)部編碼,它是以一個(gè) ziplist 為節(jié)點(diǎn)的 linkedlist,它結(jié)合了兩者的優(yōu)勢(shì),為列表類提供了一種更為優(yōu)秀的內(nèi)部編碼實(shí)現(xiàn)。
使用場(chǎng)景
- 消息隊(duì)列
Redis 的 lpush + brpop 即可實(shí)現(xiàn)阻塞隊(duì)列,生產(chǎn)者客戶端使用 lpush 從列表左側(cè)插入元素,多個(gè)消費(fèi)者客戶端使用 brpop 命令阻塞式地?fù)屃斜砦膊康脑?#xff0c;多個(gè)客戶端保證了消費(fèi)的負(fù)載均衡和高可用性。 - 文章列表
每個(gè)用戶有屬于自己的文章列表,現(xiàn)在需要分頁(yè)展示文章列表,就可以考慮使用列表。因?yàn)榱斜聿坏行?#xff0c;同時(shí)支持按照索引范圍獲取元素。每篇文章使用哈希結(jié)構(gòu)存儲(chǔ)。
lpush + lpop = 棧、lpush + rpop = 隊(duì)列、lpush + ltrim = 優(yōu)先集合、lpush + brpop = 消息隊(duì)列
P5:集合
集合類型也是用來(lái)保存多個(gè)字符串元素,和列表不同的是集合不允許有重復(fù)元素,并且集合中的元素是無(wú)序的,不能通過(guò)索引下標(biāo)獲取元素。一個(gè)集合最多可以存儲(chǔ) 232-1 個(gè)元素。Redis 除了支持集合內(nèi)的增刪改查,還支持多個(gè)集合取交集、并集、差集。
添加元素
sadd key element [element…],返回結(jié)果為添加成功的元素個(gè)數(shù)。
刪除元素
srem key element [element…],返回結(jié)果為成功刪除的元素個(gè)數(shù)。
計(jì)算元素個(gè)數(shù)
scard key,時(shí)間復(fù)雜度為 O(1),會(huì)直接使用 Redis 內(nèi)部的遍歷。
判斷元素是否在集合中
sismember key element,如果存在返回 1,否則返回 0。
隨機(jī)從集合返回指定個(gè)數(shù)個(gè)元素
srandmember key [count],如果不指定 count 默認(rèn)為 1。
從集合隨機(jī)彈出元素
spop key,可以從集合中隨機(jī)彈出一個(gè)元素。
獲取所有元素
smembers key
求多個(gè)集合的交集/并集/差集
sinter key [key…]
sunion key [key…]
sdiff key [key…]
保存交集、并集、差集的結(jié)果
sinterstore/sunionstore/sdiffstore destination key [key…]
集合間運(yùn)算在元素較多情況下比較耗時(shí),Redis 提供這三個(gè)指令將集合間交集、并集、差集的結(jié)果保存在 destination key 中。
內(nèi)部編碼
- intset 整數(shù)集合:當(dāng)集合中的元素個(gè)數(shù)小于配置值(默認(rèn) 512 個(gè)時(shí)),使用 intset。
- hashtable 哈希表:當(dāng)集合類型無(wú)法滿足 intset 條件時(shí)使用 hashtable。當(dāng)某個(gè)元素不為整數(shù)時(shí),也會(huì)使用 hashtable。
使用場(chǎng)景
集合類型比較典型的使用場(chǎng)景是標(biāo)簽,例如一個(gè)用戶可能與娛樂(lè)、體育比較感興趣,另一個(gè)用戶可能對(duì)例時(shí)、新聞比較感興趣,這些興趣點(diǎn)就是標(biāo)簽。這些數(shù)據(jù)對(duì)于用戶體驗(yàn)以及增強(qiáng)用戶黏度比較重要。
sadd = 標(biāo)簽、spop/srandmember = 生成隨機(jī)數(shù),比如抽獎(jiǎng)、sadd + sinter = 社交需求
P6:有序集合
有序集合保留了集合不能有重復(fù)成員的特性,不同的是可以排序。但是它和列表使用索引下標(biāo)作為排序依據(jù)不同的是,他給每個(gè)元素設(shè)置一個(gè)分?jǐn)?shù)(score)作為排序的依據(jù)。有序集合提供了獲取指定分?jǐn)?shù)和元素查詢范圍、計(jì)算成員排名等功能。
數(shù)據(jù)結(jié)構(gòu) 是否允許元素重復(fù) 是否有序 有序?qū)崿F(xiàn)方式 應(yīng)用場(chǎng)景
列表 是 是 下標(biāo) 時(shí)間軸,消息隊(duì)列
集合 否 否 / 標(biāo)簽,社交
有序集合 否 是 分值 排行榜,社交
添加成員
zadd key score member [score member…],返回結(jié)果是成功添加成員的個(gè)數(shù)
Redis 3.2 為 zadd 命令添加了 nx、xx、ch、incr 四個(gè)選項(xiàng):
- nx:member 必須不存在才可以設(shè)置成功,用于添加。
- xx:member 必須存在才能設(shè)置成功,用于更新。
- ch:返回此次操作后,有序集合元素和分?jǐn)?shù)變化的個(gè)數(shù)。
- incr:對(duì) score 做增加,相當(dāng)于 zincrby。
zadd 的時(shí)間復(fù)雜度為 O(logn),sadd 的時(shí)間復(fù)雜度為 O(1)。
計(jì)算成員個(gè)數(shù)
zcard key,時(shí)間復(fù)雜度為 O(1)。
計(jì)算某個(gè)成員的分?jǐn)?shù)
zscore key member ,如果不存在則返回 nil。
計(jì)算成員排名
zrank key member,從低到高返回排名。
zrevrank key member,從高到低返回排名。
刪除成員
zrem key member [member…],返回結(jié)果是成功刪除的個(gè)數(shù)。
增加成員的分?jǐn)?shù)
zincrby key increment member
返回指定排名范圍的成員
zrange key start end [withscores],從低到高返回
zrevrange key start end [withscores], 從高到底返回
返回指定分?jǐn)?shù)范圍的成員
zrangebyscore key min max [withscores] [limit offset count],從低到高返回
zrevrangebyscore key min max [withscores] [limit offset count], 從高到底返回
返回指定分?jǐn)?shù)范圍成員個(gè)數(shù)
zcount key min max
刪除指定分?jǐn)?shù)范圍內(nèi)的成員
zremrangebyscore key min max
交集和并集
zinterstore destination numkeys key [key…] [weights weight [weight…]] [aggregate sum|min|max]
zunionstore destination numkeys key [key…] [weights weight [weight…]] [aggregate sum|min|max]
- destination:交集結(jié)果保存到這個(gè)鍵
- numkeys:要做交集計(jì)算鍵的個(gè)數(shù)
- key:需要做交集計(jì)算的鍵
- weight:每個(gè)鍵的權(quán)重,默認(rèn) 1
- aggregate sum|min|max:計(jì)算交集后,分值可以按和、最小值、最大值匯總,默認(rèn) sum。
內(nèi)部編碼
- ziplist 壓縮列表:當(dāng)有序集合元素個(gè)數(shù)和值小于配置值(默認(rèn)128 個(gè)和 64 字節(jié))時(shí)會(huì)使用 ziplist 作為內(nèi)部實(shí)現(xiàn)。
- skiplist 跳躍表:當(dāng) ziplist 不滿足條件時(shí)使用,因?yàn)榇藭r(shí) ziplist 的讀寫效率會(huì)下降。
使用場(chǎng)景
有序集合的典型使用場(chǎng)景就是排行榜系統(tǒng),例如用戶上傳了一個(gè)視頻并獲得了贊,可以使用 zadd 和 zincrby。如果需要將用戶從榜單刪除,可以使用 zrem。如果要展示獲取贊數(shù)最多的十個(gè)用戶,可以使用 zrange。
P7:鍵和數(shù)據(jù)庫(kù)管理
鍵重命名
rename key newkey
如果 rename 前鍵已經(jīng)存在,那么它的值也會(huì)被覆蓋。為了防止強(qiáng)行覆蓋,Redis 提供了 renamenx 命令,確保只有 newkey 不存在時(shí)才被覆蓋。由于重命名鍵期間會(huì)執(zhí)行 del 命令刪除舊的鍵,如果鍵對(duì)應(yīng)值比較大會(huì)存在阻塞的可能。
隨機(jī)返回一個(gè)鍵
random key
鍵過(guò)期
expire key seconds:鍵在 seconds 秒后過(guò)期。
如果過(guò)期時(shí)間為負(fù)值,鍵會(huì)被立即刪除,和 del 命令一樣。persist 命令可以將鍵的過(guò)期時(shí)間清除。
對(duì)于字符串類型鍵,執(zhí)行 set 命令會(huì)去掉過(guò)期時(shí)間,set 命令對(duì)應(yīng)的函數(shù) setKey 最后執(zhí)行了 removeExpire 函數(shù)去掉了過(guò)期時(shí)間。setex 命令作為 set + expire 的組合,不單是原子執(zhí)行并且減少了一次網(wǎng)絡(luò)通信的時(shí)間。
鍵遷移
- move
move 命令用于在 Redis 內(nèi)部進(jìn)行數(shù)據(jù)遷移,move key db 把指定的鍵從源數(shù)據(jù)庫(kù)移動(dòng)到目標(biāo)數(shù)據(jù)庫(kù)中。 - dump + restore
可以實(shí)現(xiàn)在不同的 Redis 實(shí)例之間進(jìn)行數(shù)據(jù)遷移,分為兩步:
① dump key ,在源 Redis 上,dump 命令會(huì)將鍵值序列化,格式采用 RDB 格式。
② restore key ttl value,在目標(biāo) Redis 上,restore 命令將序列化的值進(jìn)行復(fù)原,ttl 代表過(guò)期時(shí)間, ttl = 0 則沒(méi)有過(guò)期時(shí)間。
整個(gè)遷移并非原子性的,而是通過(guò)客戶端分步完成,并且需要兩個(gè)客戶端。 - migrate
實(shí)際上 migrate 命令就是將 dump、restore、del 三個(gè)命令進(jìn)行組合,從而簡(jiǎn)化操作流程。migrate 具有原子性,支持多個(gè)鍵的遷移,有效提高了遷移效率。實(shí)現(xiàn)過(guò)程和 dump + restore 類似,有三點(diǎn)不同:
① 整個(gè)過(guò)程是原子執(zhí)行,不需要在多個(gè) Redis 實(shí)例開(kāi)啟客戶端。
② 數(shù)據(jù)傳輸直接在源 Redis 和目標(biāo) Redis 完成。
③ 目標(biāo) Redis 完成 restore 后會(huì)發(fā)送 OK 給源 Redis,源 Redis 接收后根據(jù) migrate 對(duì)應(yīng)選項(xiàng)來(lái)決定是否在源 Redis 上刪除對(duì)應(yīng)鍵。
切換數(shù)據(jù)庫(kù)
select dbIndex,Redis 中默認(rèn)配置有 16 個(gè)數(shù)據(jù)庫(kù),例如 select 0 將切換到第一個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)之間的數(shù)據(jù)是隔離的。
flushdb/flushall
用于清除數(shù)據(jù)庫(kù),flushdb 只清除當(dāng)前數(shù)據(jù)庫(kù),flushall 會(huì)清除所有數(shù)據(jù)庫(kù)。如果當(dāng)前數(shù)據(jù)庫(kù)鍵值數(shù)量比較多,flushdb/flushall 存在阻塞 Redis 的可能性。
P8:發(fā)布訂閱
Redis 提供了基于發(fā)布/訂閱模式的消息機(jī)制,該模式下消息發(fā)布者和訂閱者不進(jìn)行直接通信,發(fā)布者客戶端向指定的頻道(channel)發(fā)送消息,訂閱該頻道的每個(gè)客戶端都可以收到該消息。
發(fā)布消息
publish channel message,返回結(jié)果為訂閱者的個(gè)數(shù)。
訂閱消息
subscribe channel [channel…],訂閱者可以訂閱一個(gè)或多個(gè)頻道。
客戶端在執(zhí)行訂閱命令后會(huì)進(jìn)入訂閱狀態(tài),只能接收 subscribe、psubscribe、unsubscribe、punsubscribe 的四個(gè)命令。新開(kāi)啟的訂閱客戶端,無(wú)法收到該頻道之前的消息,因?yàn)?Redis 不會(huì)對(duì)發(fā)布的消息進(jìn)行持久化。
和很多專業(yè)的消息隊(duì)列系統(tǒng)如 Kafka、RocketMQ 相比,Redis 的發(fā)布訂閱略顯粗糙,例如無(wú)法實(shí)現(xiàn)消息堆積和回溯,但勝在足夠簡(jiǎn)單,如果當(dāng)前場(chǎng)景可以容忍這些缺點(diǎn),也是一個(gè)不錯(cuò)的選擇。
取消訂閱
unsubscribe [channel [channel…]]
客戶端可以通過(guò) unsubscribe 命令取消對(duì)指定頻道的訂閱,取消成功后不會(huì)再收到該頻道的發(fā)布消息。
按照模式訂閱和取消訂閱
psubscribe/unsubscribe pattern [pattern…],例如訂閱所有以 it 開(kāi)頭的頻道:psubscribe it*
P9:RDB 持久化
RDB 持久化是把當(dāng)前進(jìn)程數(shù)據(jù)生成快照保存到硬盤的過(guò)程,觸發(fā) RDB 持久化過(guò)程分為手動(dòng)觸發(fā)和自動(dòng)觸發(fā)。
觸發(fā)機(jī)制
手動(dòng)觸發(fā)分別對(duì)應(yīng) save 和 bgsave 命令:
- save:阻塞當(dāng)前 Redis 服務(wù)器,直到 RDB 過(guò)程完成為止,對(duì)于內(nèi)存比較大的實(shí)例會(huì)造成長(zhǎng)時(shí)間阻塞,線上環(huán)境不建議使用。
- bgasve:Redis 進(jìn)程執(zhí)行 fork 操作創(chuàng)建子進(jìn)程,RDB 持久化過(guò)程由子進(jìn)程負(fù)責(zé),完成后自動(dòng)結(jié)束。阻塞只發(fā)生在 fork 階段,一般時(shí)間很短。bgsave 是針對(duì) save 阻塞問(wèn)題做的優(yōu)化,因此 Redis 內(nèi)部所有涉及 RDB 的操作都采用 bgsave 的方式,而 save 方式已經(jīng)廢棄。
除了手動(dòng)觸發(fā)外,Redis 內(nèi)部還存在自動(dòng)觸發(fā) RDB 的持久化機(jī)制,例如:
- 使用 save 相關(guān)配置,如 save m n,表示 m 秒內(nèi)數(shù)據(jù)集存在 n 次修改時(shí),自動(dòng)觸發(fā) bgsave。
- 如果從節(jié)點(diǎn)執(zhí)行全量復(fù)制操作,主節(jié)點(diǎn)自動(dòng)執(zhí)行 bgsave 生成 RDB 文件并發(fā)送給從節(jié)點(diǎn)。
- 執(zhí)行 debug reload 命令重新加載 Redis 時(shí)也會(huì)自動(dòng)觸發(fā) save 操作。
- 默認(rèn)情況下執(zhí)行 shutdown 命令時(shí),如果沒(méi)有開(kāi)啟 AOF 持久化功能則自動(dòng)執(zhí)行 bgsave。
bgsave 是主流的觸發(fā) RDB 持久化的方式,運(yùn)作流程如下:
① 執(zhí)行 bgsave 命令,Redis 父進(jìn)程判斷當(dāng)前是否存在正在執(zhí)行的子進(jìn)程,如 RDB/AOF 子進(jìn)程,如果存在 bgsave 命令直接返回。
② 父進(jìn)程執(zhí)行 fork 操作創(chuàng)建子進(jìn)程,fork 操作過(guò)程中父進(jìn)程會(huì)阻塞。
③ 父進(jìn)程 fork 完成后,bgsave 命令返回并不再阻塞父進(jìn)程,可以繼續(xù)響應(yīng)其他命令。
④ 子進(jìn)程創(chuàng)建 RDB 文件,根據(jù)父進(jìn)程內(nèi)存生成臨時(shí)快照文件,完成后對(duì)原有文件進(jìn)行原子替換。
⑤ 進(jìn)程發(fā)送信號(hào)給父進(jìn)程表示完成,父進(jìn)程更新統(tǒng)計(jì)信息。
優(yōu)點(diǎn)
RDB 是一個(gè)緊湊壓縮的二進(jìn)制文件,代表 Redis 在某個(gè)時(shí)間點(diǎn)上的數(shù)據(jù)快照。非常適合于備份,全量復(fù)制等場(chǎng)景。例如每 6 個(gè)消時(shí)執(zhí)行 bgsave 備份,并把 RDB 文件拷貝到遠(yuǎn)程機(jī)器或者文件系統(tǒng)中,用于災(zāi)難恢復(fù)。
Redis 加載 RDB 恢復(fù)數(shù)據(jù)遠(yuǎn)遠(yuǎn)快于 AOF 的方式。
缺點(diǎn)
RDB 方式數(shù)據(jù)無(wú)法做到實(shí)時(shí)持久化/秒級(jí)持久化,因?yàn)?bgsave 每次運(yùn)行都要執(zhí)行 fork 操作創(chuàng)建子進(jìn)程,屬于重量級(jí)操作,頻繁執(zhí)行成本過(guò)高。針對(duì) RDB 不適合實(shí)時(shí)持久化的問(wèn)題,Redis 提供了 AOF 持久化方式。
RDB 文件使用特定二進(jìn)制格式保存,Redis 版本演進(jìn)過(guò)程中有多個(gè)格式的 RDB 版本,存在老版本 Redis 服務(wù)無(wú)法兼容新版 RDB 格式的問(wèn)題。
P10:AOF 持久化
AOF 持久化以獨(dú)立日志的方式記錄每次寫命令,重啟時(shí)再重新執(zhí)行 AOF 文件中的命令達(dá)到恢復(fù)數(shù)據(jù)的目的。AOF 的主要作用是解決了數(shù)據(jù)持久化的實(shí)時(shí)性,目前是 Redis 持久化的主流方式。
開(kāi)啟 AOF 功能需要設(shè)置:appendonly yes,默認(rèn)不開(kāi)啟。保存路徑同 RDB 方式一致,通過(guò) dir 配置指定。
AOF 的工作流程操作:命令寫入 append、文件同步 sync、文件重寫 rewrite、重啟加載 load:
- 所有的寫入命令會(huì)追加到 aof_buf 緩沖區(qū)中。
- AOF 緩沖區(qū)根據(jù)對(duì)應(yīng)的策略向硬盤做同步操作。
- 隨著 AOF 文件越來(lái)越大,需要定期對(duì) AOF 文件進(jìn)行重寫,達(dá)到壓縮的目的。
- 當(dāng)服務(wù)器重啟時(shí),可以加載 AOF 文件進(jìn)行數(shù)據(jù)恢復(fù)。
命令寫入
AOF 命令寫入的內(nèi)容直接是文本協(xié)議格式,采用文本協(xié)議格式的原因:
- 文本協(xié)議具有很好的兼容性。
- 開(kāi)啟 AOF 后所有寫入命令都包含追加操作,直接采用協(xié)議格式避免了二次處理開(kāi)銷。
- 文本協(xié)議具有可讀性,方便直接修改和處理。
AOF 把命令追加到緩沖區(qū)的原因:
Redis 使用單線程響應(yīng)命令,如果每次寫 AOF 文件命令都直接追加到硬盤,那么性能完全取決于當(dāng)前硬盤負(fù)載。先寫入緩沖區(qū)中還有另一個(gè)好處,Redis 可以提供多種緩沖區(qū)同步硬盤策略,在性能和安全性方面做出平衡。
文件同步
Redis 提供了多種 AOF 緩沖區(qū)文件同步策略,由參數(shù) appendfsync 控制,不同值的含義如下:
- always:命令寫入緩沖區(qū)后調(diào)用系統(tǒng) fsync 操作同步到 AOF 文件,fsync 完成后線程返回。每次寫入都要同步 AOF,性能較低,不建議配置。
- everysec:命令寫入緩沖區(qū)后調(diào)用系統(tǒng) write 操作,write 完成后線程返回。fsync 同步文件操作由專門線程每秒調(diào)用一次。是建議的策略,也是默認(rèn)配置,兼顧性能和數(shù)據(jù)安全。
- no:命令寫入緩沖區(qū)后調(diào)用系統(tǒng) write 操作,不對(duì) AOF 文件做 fsync 同步,同步硬盤操作由操作系統(tǒng)負(fù)責(zé),周期通常最長(zhǎng) 30 秒。由于操作系統(tǒng)每次同步 AOF 文件的周期不可控,而且會(huì)加大每次同步硬盤的數(shù)據(jù)量,雖然提升了性能,但安全性無(wú)法保證。
文件重寫
文件重寫是把 Redis 進(jìn)程內(nèi)的數(shù)據(jù)轉(zhuǎn)化為寫命令同步到新 AOF 文件的過(guò)程,可以降低文件占用空間,更小的文件可以更快地被 加載。
重寫后 AOF 文件變小的原因:
- 進(jìn)程內(nèi)已經(jīng)超時(shí)的數(shù)據(jù)不再寫入文件。
- 舊的 AOF 文件含有無(wú)效命令,重寫使用進(jìn)程內(nèi)數(shù)據(jù)直接生成,這樣新的 AOF 文件只保留最終數(shù)據(jù)寫入命令。
- 多條寫命令可以合并為一個(gè),為了防止單條命令過(guò)大造成客戶端緩沖區(qū)溢出,對(duì)于 list、set、hash、zset 等類型操作,以 64 個(gè)元素為界拆分為多條。
AOF 重寫分為手動(dòng)觸發(fā)和自動(dòng)觸發(fā),手動(dòng)觸發(fā)直接調(diào)用 bgrewriteaof 命令,自動(dòng)觸發(fā)根據(jù) auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 參數(shù)確定自動(dòng)觸發(fā)時(shí)機(jī)。
重寫流程:
① 執(zhí)行 AOF 重寫請(qǐng)求,如果當(dāng)前進(jìn)程正在執(zhí)行 AOF 重寫,請(qǐng)求不執(zhí)行并返回,如果當(dāng)前進(jìn)程正在執(zhí)行 bgsave 操作,重寫命令延遲到 bgsave 完成之后再執(zhí)行。
② 父進(jìn)程執(zhí)行 fork 創(chuàng)建子進(jìn)程,開(kāi)銷等同于 bgsave 過(guò)程。
③ 父進(jìn)程 fork 操作完成后繼續(xù)響應(yīng)其他命令,所有修改命令依然寫入 AOF 緩沖區(qū)并同步到硬盤,保證原有 AOF 機(jī)制正確性。
④ 子進(jìn)程根據(jù)內(nèi)存快照,按命令合并規(guī)則寫入到新的 AOF 文件。每次批量寫入數(shù)據(jù)量默認(rèn)為 32 MB,防止單次刷盤數(shù)據(jù)過(guò)多造成阻塞。
⑤ 新 AOF 文件寫入完成后,子進(jìn)程發(fā)送信號(hào)給父進(jìn)程,父進(jìn)程更新統(tǒng)計(jì)信息。
⑥ 父進(jìn)程把 AOF 重寫緩沖區(qū)的數(shù)據(jù)寫入到新的 AOF 文件并替換舊文件,完成重寫。
重啟加載
AOF 和 RDB 文件都可以用于服務(wù)器重啟時(shí)的數(shù)據(jù)恢復(fù)。Redis 持久化文件的加載流程:
① AOF 持久化開(kāi)啟且存在 AOF 文件時(shí),優(yōu)先加載 AOF 文件。
② AOF 關(guān)閉時(shí)且存在 RDB 文件時(shí),記載 RDB 文件。
③ 加載 AOF/RDB 文件成功后,Redis 啟動(dòng)成功。
④ AOF/RDB 文件存在錯(cuò)誤導(dǎo)致加載失敗時(shí),Redis 啟動(dòng)失敗并打印錯(cuò)誤信息。
總結(jié)
以上是生活随笔為你收集整理的【备战秋招系列-4】Java高频知识——并发、Spring、MySQL、redis的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: web前端(2):了解CSS和常见的属性
- 下一篇: linux cmake编译源码,linu