日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

【备战秋招系列-4】Java高频知识——并发、Spring、MySQL、redis

發布時間:2024/3/24 数据库 62 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【备战秋招系列-4】Java高频知识——并发、Spring、MySQL、redis 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

并發 20

P1:Java 內存模型

Java 線程的通信由 JMM 控制,JMM 的主要目的是定義程序中各種變量的訪問規則,關注在虛擬機中把變量值存儲到內存和從內存中取出變量值這樣的底層細節。此處的變量包括實例字段、靜態字段和構成數組元素的對象,但不包括局部變量與方法參數,因為它們是線程私有的,不存在多線程競爭問題。為了獲得更好的執行效率,JMM 沒有限制執行引擎使用處理器的特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器是否要進行調整代碼執行順序這類優化措施,JMM 遵循一個基本原則:只要不改變程序執行結果,編譯器和處理器怎么優化都行。例如編譯器分析某個鎖只會單線程訪問就消除該鎖,某個 volatile 變量只會單線程訪問就把它當作普通變量。

JMM 規定了所有變量都存儲在主內存中,每條線程還有自己的工作內存,工作內存中保存了被該線程使用的變量的主內存副本,線程對變量的所有操作都必須在工作空間中進行,而不能直接讀寫主內存中的數據。不同線程之間也無法直接訪問對方工作內存中的變量,兩個線程之間的通信必須經過主內存,JMM 通過控制主內存與每個線程的工作內存之間的交互來提供內存可見性保證。

關于主內存與工作內存之間的交互,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存等實現細節,JMM 定義了 8 種原子操作:

  • lock:作用于主內存變量,把變量標識為一條線程獨占的狀態。
  • unlock:作用于主內存變量,把處于鎖定狀態的變量釋放出來,釋放后的變量才能被其他線程鎖定。
  • read:作用于主內存變量,把變量值從主內存傳到工作內存。
  • load:作用于工作內存變量,把 read 從主存中得到的值放入工作內存的變量副本。
  • use:作用于工作內存變量,把工作內存中的變量值傳給執行引擎,每當虛擬機遇到需要使用變量值的字節碼指令時執行該操作。
  • assign:作用于工作內存變量,把從執行引擎接收的值賦給工作內存變量,每當虛擬機遇到給變量賦值的字節碼指令時執行該操作。
  • store:作用于工作內存變量,把工作內存中的變量值傳送到主內存。
  • write:作用于主內存變量,把 store 從工作內存取到的變量值放入主內存變量中。

如果要把一個變量從主內存拷貝到工作內存,就要按順序執行 read 和 load ,如果要把變量從工作內存同步回主內存,就要按順序執行 store 和 write 。JMM 只要求這兩種操作必須按順序執行,但不要求連續,也就是說 read 和 load、store 和 write 之間可插入其他指令。這種定義十分嚴謹但過于復雜,之后 Java 將內存操作簡化為 lock、unlock、read 和 write 四種,但這只是語言描述上的等價化簡。


P2:as-if-serial 和 happens-before 規則

as-if-serial

as-if-serial 的語義是:不管怎么重排序,單線程程序的執行結果不能被改變,編譯器和處理器必須遵循 as-if-serial 語義。

為了遵循 as-if-serial 語義,編譯器和處理器不會對存在數據依賴關系的操作重排序,因為這種重排序會改變執行結果。但是如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。

as-if-serial 語義把單線程程序保護了起來,給了程序員一種幻覺:單線程程序是按程序的順序執行的,使程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。

happens-before

先行發生原則,是 JMM 中定義的兩項操作之間的偏序關系,它是判斷數據是否存在競爭,線程是否安全的重要手段。

JMM 將 happens-before 要求禁止的重排序按是否會改變程序執行結果分為兩類。對于會改變結果的重排序 JMM 要求編譯器和處理器必須禁止這種重排序,對于不會改變結果的重排序,JMM 對編譯器和處理器不做要求。

JMM 存在一些天然的 happens-before 關系,無需任何同步器協助就已經存在。如果兩個操作的關系不在此列,并且無法從這些規則推導出來,它們就沒有順序性保障,虛擬機可以對它們隨意進行重排序。

  • 程序次序規則:在一個線程內,按照控制流順序,書寫在前面的操作先行發生于書寫在后面的操作。
  • 管程鎖定規則:一個 unlock 操作先行發生于后面對同一個鎖的 lock 操作。
  • volatile 規則:對一個 volatile 變量的寫操作先行發生于后面對這個變量的讀操作。
  • 線程啟動規則:線程對象的 start 方法先行發生于此線程的每一個動作。
  • 線程終止規則:線程中的所有操作都先行發生于對此線程的終止檢測。
  • 線程中斷規則:對線程 interrupt 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生。
  • 對象終結規則:一個對象的初始化完成先行發生于它的 finalize 方法的開始。
  • 傳遞性:如果操作 A 先行發生于操作 B,操作 B 先行發生于操作 C,那么操作 A 先行發生于操作 C 。

區別

as-if-serial 保證單線程程序的執行結果不被改變,happens-before 保證正確同步的多線程程序的執行結果不被改變。這兩種語義的目的都是為了在不改變程序執行結果的前提下盡可能提高程序執行的并行度。


P3:指令重排序

重排序指從源代碼到指令序列的重排序,在執行程序時為了提高性能,編譯器和處理器通常會對指令進行重排序,分為三種:

  • 編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下可以重排語句的執行順序。
  • 指令級并行的重排序:如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  • 內存系統的重排序:由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作操作看上去可能是亂序執行。

從 Java 源代碼到最終實際執行的指令序列,會分別經歷編譯器優化重排序、指令級并行重排序和內存系統重排序,這些重排序可能會導致多線程程序出現內存可見性問題。

對于編譯器,JMM 的編譯器重排序規則會禁止特定類型的編譯器重排序。對于處理器重排序,JMM 的處理器重排序規則會要求 Java 編譯器在生成指令序列時,插入特定類型的內存屏障指令,即一組用于實現對內存操作順序限制的處理器指令,通過內存屏障指令來禁止特定類型的處理器重排序。JMM 屬于語言級的內存模型,它確保在不同的編譯器和處理器平臺上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。


P4:原子性、可見性和有序性

原子性

由 JMM 直接保證的原子性變量操作包括 read、load、assign、use、store 和 write,基本數據類型的訪問都是具備原子性的,例外就是 long 和 double 的非原子性協定,允許虛擬機將沒有被 volatile 修飾的 64 位數據的操作劃分為兩次 32 位的操作。

如果應用場景需要更大范圍的原子性保證,JMM 還提供了 lock 和 unlock 操作滿足這種需求,盡管 JVM 沒有把這兩種操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操作,這兩個字節碼指令反映到 Java 代碼中就是 synchronized 關鍵字。

可見性

可見性就是指當一個線程修改了共享變量的值時,其他線程能夠立即得知修改。JMM 通過在變量修改后將值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式實現可見性,無論是普通變量還是volatile變量都是如此,區別是 volatile 保證新值能立即同步到主內存以及每次使用前立即從主內存刷新,因此說 volatile 保證了多線程操作變量的可見性,而普通變量則不能保證。

除了 volatile 之外,還有兩個關鍵字能實現可見性,分別是 synchronized 和 final,同步塊的可見性是由"對一個變量執行unlock 前必須先把此變量同步回主內存中,即先執行 store 和 write"這條規則獲得的。final 的可見性是指:被 final 修飾的字段在構造方法中一旦被初始化完成,并且構造方法沒有把"this"引用傳遞出去,那么其他線程就能看到 final 字段的值。

有序性

有序性可以總結為:在本線程內觀察所有操作是有序的,在一個線程內觀察另一個線程,所有操作都是無序的。前半句是指"as-if-serial 語義",后半句是指"指令重排序"和"工作內存與主內存同步延遲"現象。

Java 提供了 volatile 和 synchronized 保證線程間操作的有序性,volatile 本身就包含了禁止指令重排序的語義,而 synchronized 則是由"一個變量在同一個時刻只允許一條線程對其進行lock操作"這條規則獲得的,該規則決定了持有同一個鎖的兩個同步塊只能串行進入。


P5:volatile 關鍵字

輕量級的線程操作可見方式,JMM 為 volatile 定義了一些特殊的訪問規則,當一個變量被定義為 volatile 后具備兩種特性:

  • 保證此變量對所有線程的可見性
    可見性是指當一條線程修改了這個變量的值,新值對于其他線程來說是立即可以得知的。而普通變量并不能做到這一點,普通變量的值在線程間傳遞時均需要通過主內存來完成。
    volatile 變量在各個線程的工作內存中不存在一致性問題,但 Java 的運算操作符并非原子操作,這導致 volatile 變量運算在并發下仍是不安全的。
  • 禁止指令重排序優化
    使用 volatile 變量進行寫操作,匯編指令操作是帶有 lock 前綴的,相當于一個內存屏障,后面的指令不能重排到內存屏障之前的位置。只有一個處理器時不需要使用內存屏障,但如果有兩個或更多的處理器訪問同一塊內存,且其中有一個在觀測另一個,就需要使用內存屏障來保證一致性了。
    使用 lock 前綴的指令在多核處理器中會引發兩件事:① 將當前處理器緩存行的數據寫回到系統內存。② 這個寫回內存的操作會使其他在CPU里緩存了該內存地址的數據無效。
    這種操作相當于對緩存中的變量做了一次 store 和 write 操作,可以讓 volatile 變量的修改對其他處理器立即可見。

靜態變量 i 執行多線程 i++ 的不安全問題

通過反編譯會發現一個自增語句是由 4 條字節碼指令構成的,依次為getstatic、iconst_1、iadd、putstatic,當getstatic把 i 的值取到操作棧頂時,volatile保證了 i 的值在此刻是正確的,但是在執行iconst_1、iadd這些指令時,其他線程可能已經改變了i的值,而操作棧頂的值就變成了過期的數據,所以 putstatic 指令執行后就可能把較小的 i 值同步回了主內存。

即使編譯出來只有一條字節碼指令也不能意味著這條指令就是一個原子操作,一條字節碼指令在解釋執行時,解釋器要運行很多行代碼才能實現它的語義。如果是編譯執行,一條字節碼指令也可能轉化成若干條本地機器碼指令。

適用場景

運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。

變量不需要與其他狀態變量共同參與不變約束。

volatile的內存語義

從內存語義角度來說,volatile的寫-讀與鎖的釋放-獲取具有相同的內存效果。

  • 寫內存語義:當寫一個volatile變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到主內存。
  • 讀內存語義:當讀一個volatile變量時,JMM 會把該線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量。

volatile指令重排序的特點

當第二個操作是volatile 寫時,不管第一個操作是什么都不能重排序,確保寫之前的操作不會被編譯器重排序到寫之后。

當第一個操作是volatile 讀時,不管第二個操作是什么都不能重排序,確保讀之后的操作不會被編譯器重排序到讀之前。

當第一個操作是volatile 寫,第二個操作是 volatile 讀時不能重排序。

JSR-133 增強 volatile 語義的原因

在舊的內存模型中,雖然不允許 volatile 變量之間重排序,但允許 volatile 變量與普通變量重排序,可能導致內存不可見問題。為了提供一種比鎖更輕量級的線程通信機制,嚴格限制了編譯器和處理器對 volatile 變量與普通變量的重排序,確保 volatile 的寫-讀和鎖的釋放-獲取具有相同的內存語義。


P6:final 關鍵字

final 可以保證可見性,被 final 修飾的字段在構造方法中一旦被初始化完成,并且構造方法沒有把 this 的引用傳遞出去,那么在其他線程中就能看見 final 字段的值。

JSR-133 增強 final語義的原因

在舊的 JMM 中,一個嚴重的缺陷就是線程可能看到 final 域的值會改變。比如一個線程看到一個 int 類型 final 域的值為0,此時該值是還未初始化之前的零值,過一段時間之后該值被某線程初始化后這個線程再去讀這個 final 域的值會發現值變為1。

為了修復該漏洞,JSR-133 通過為 final 域增加寫和讀重排序規則,提供初始化安全保證:只要對象是正確構造的(被構造對象的引用在構造方法中沒有逸出),那么不需要使用同步就可以保證任意線程都能看到這個final域在構造方法中被初始化之后的值。

寫 final 域重排序規則

禁止把 final 域的寫重排序到構造方法之外,編譯器會在final域的寫之后,構造方法的 return之前,插入一個Store Store屏障。該規則可以確保在對象引用為任意線程可見之前,對象的 final 域已經被正確初始化過了,而普通域不具有這個保障。

對于引用類型,增加了約束:在構造方法內對一個 final 引用的對象的成員域的寫入,與隨后在構造方法外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

讀 final 域重排序規則

在一個線程中,初次讀對象引用和初次讀該對象包含的final域,JMM 禁止處理器重排序這兩個操作。編譯器會在讀final域操作的前面插入一個Load Load 屏障,該規則可以確保在讀一個對象的 final 域之前,一定會先讀包含這個 final 域的對象的引用。


P7:synchronized 關鍵字

每個 Java 對象都有一個關聯的 monitor 監視器,使用 synchronized 時,JVM 會根據 synchronized 的使用環境找到對應對象的 monitor,再根據 monitor 的狀態進行加、解鎖的判斷。如果成功加鎖就成為該 monitor 的唯一持有者,monitor 在被釋放前不能再被其他線程獲取。

方法元信息中會使用 ACC_SYNCHRONIZED 標識該方法是一個同步方法,同步代碼塊中會使用 monitorenter 和 monitorexit 這兩個字節碼指令獲取和釋放 monitor。這兩個字節碼指令都需要一個引用類型的參數來指明要鎖定和解鎖的對象,對于同步普通方法,鎖是當前實例對象;對于靜態同步方法,鎖是當前類的 Class 對象;對于同步方法塊,鎖是 synchronized 括號里的對象。

在執行 monitorenter 指令時,首先要去嘗試獲取對象的鎖。如果這個對象沒有被鎖定,或者當前線程已經持有了那個對象的鎖,那么就把鎖的計數器的值增加 1,而在執行 monitorexit 指令時會將鎖計數器的值減 1。一旦計數器的值為 0,鎖隨即就被釋放了。如果獲取鎖對象失敗,那當前線程就應該被阻塞等待,直到請求鎖定的對象被持有它的線程釋放為止。

例如有兩個線程 A 和 B 競爭 monitor,當線程 A 競爭到鎖時,會將 monitor 中的 owner 設置為 A,把線程 B 阻塞并放到等待競爭資源的 ContentionList 隊列。ContentionList 中的部分線程會進入 EntryList,EntryList 中的線程會被指定為 OnDeck 競爭候選者線程,如果獲得了鎖資源將進入 Owner 狀態,釋放鎖資源后進入 !Owner 狀態。被阻塞的線程會進入 WaitSet。

被 synchronized 修飾的同步塊對一條線程來說是可重入的,并且同步塊在持有鎖的線程執行完畢并釋放鎖之前,會無條件地阻塞后面其他線程的進入。從執行成本的角度看,持有鎖是一個重量級的操作。在主流 JVM 實現中,Java 的線程是映射到操作系統的原生內核線程之上的,如果要阻塞或喚醒一條線程,則需要操作系統幫忙完成,這就不可避免陷入用戶態到核心態的轉換中,進行這些狀態轉換需要耗費很多的處理器時間。

不公平的原因

所有收到鎖請求的線程首先自旋,如果通過自旋也沒有獲取鎖資源將被放入 ContentionList 隊列,該做法對于已經進入隊列的線程是不公平的。

為了防止 ContentionList 尾部的元素被大量線程進行 CAS 訪問影響性能,Owner 線程會在釋放鎖時將 ContentionList 的部分線程移動到 EntryList 并指定某個線程為 OnDeck 線程,Owner 并沒有將鎖直接傳遞給 OnDeck 線程而是把鎖競爭的權利交給它,該行為叫做競爭切換,犧牲了公平性但提高了性能。


P8:鎖優化

JDK 6 對 synchronized 做了很多優化,引入了適應自旋、鎖消除、鎖粗化、偏向鎖和輕量級鎖等提高鎖的效率,鎖一共有 4 個狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,這種只能升級不能降級的鎖策略是為了提高獲得鎖和釋放鎖的效率。

自旋鎖與自適應自旋

互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給 JVM 的并發性能帶來了很大壓力。同時虛擬機開發團隊也注意到了在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間去掛機和恢復線程并不值得。現在絕大多數的個人電腦和服務器都是多核心處理器系統,如果物理機器有一個以上的處理器或者處理器核心,能讓兩個或以上的線程同時并行執行,我們就可以讓后面請求鎖的那個線程稍等一會,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙循環,這項技術就是所謂的自旋鎖。

自旋鎖在 JDK 1.4中就已經引入,只不過默認是關閉的,在 JDK 6中就已經改為默認開啟了。自旋等待不能代替阻塞,自旋等待本身雖然避免了線程切換的開銷,但它要占用處理器時間,所以如果鎖被占用的時間很短,自旋的效果就會非常好,反之只會白白消耗處理器資源。因此自旋的時間必須有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程。自旋次數的默認次數是 10 次。

在 JDK 6 中對自旋鎖的優化,引入了自適應自旋。自旋的時間不再是固定的了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而允許自旋等待持續相對更長的時間。如果對于某個鎖,自旋很少成功獲得過鎖,那在以后要獲取這個鎖時將有可能之間省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程序運行時間的增長以及性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越精準。

鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼要求同步,但是對被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定依據來源于逃逸分析的數據支持,如果判斷到一段代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當作棧上的數據對待,認為它們是線程私有的,同步加鎖自然就無須再進行。

鎖粗化

原則上我們在編寫代碼時,總是推薦將同步塊的作用范圍限制得盡量小,只在共享數據得實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變少,即使存在鎖競爭,等待鎖得線程也能盡可能快拿到鎖。

大多數情況下這種原則是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體之外的,那么即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能消耗。

如果虛擬機探測到有一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部。

偏向鎖

偏向鎖的目的是為了在資源沒有被多線程競爭的情況下盡量減少鎖帶來的性能開銷。輕量級鎖是在無競爭的情況下使用 CAS 操作消除同步互斥量,偏向鎖是在無競爭的情況下把整個同步都去掉,連 CAS 操作都不做了。

偏向鎖的意思就是這個鎖會偏向于第一個獲得它的線程,如果在接下來的執行過程中,該鎖一直沒有被其他線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。

當一個線程訪問同步代碼塊并獲取鎖時,會在對象頭和幀棧中的鎖記錄里存儲鎖偏向的線程 ID,以后該線程再進入和退出同步代碼塊不需要進行 CAS 操作來加鎖和解鎖,只需要簡單地測試一下對象頭的"Mark Word"里是否存儲著指向當前線程的偏向鎖。如果測試成功表示線程已經獲得了鎖,如果失敗則需要再測試一下"Mark Word"中偏向鎖的標識是否設置成了 1 即表示當前使用偏向鎖,如果設置了就嘗試使用 CAS 將對象頭的偏向鎖指向當前線程,否則使用 CAS 方式競爭鎖。

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷需要等待全局安全點即此時沒有正在執行的字節碼,它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態則將對象頭設為無鎖狀態。如果線程還活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的"Mark Word"要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。

輕量級鎖

輕量級是相對于操作系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就被稱為重量級鎖。輕量級鎖并不是用來代替重量級鎖的,它設計的初衷是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

在代碼即將進入同步塊的時候,如果此同步對象沒有被鎖定,虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄的空間,用于存儲鎖對象目前的Mark Word的拷貝。然后虛擬機將使用 CAS 操作嘗試把對象的 Mark Word 更新為指向鎖記錄的指針,如果這個更新操作成功了,即代表該線程擁有了這個對象的鎖,并且鎖標志位將轉變為"00",表示此對象處于輕量級鎖定狀態。

如果這個更新操作失敗了,那就意味著至少存在一條線程與當前線程競爭獲取該對象的鎖。虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是則說明當前線程以及擁有了這個對象的鎖,直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶占了。如果出現兩條以上的線程爭用同一個鎖的情況,那輕量級鎖就不再有效,必須要膨脹為重量級鎖,鎖標志的狀態變為"10",此時Mark Word中存儲的就是指向重量級鎖的指針,后面等待鎖的線程也必須進入阻塞狀態。

解鎖操作也同樣是通過 CAS 操作來進行,如果對象的 Mark Word 仍然指向線程的鎖記錄,那就用 CAS 操作把對象當前的 Mark Word 和線程復制的 Mark Word 替換回來。假如能夠替換成功,那整個同步過程就順利完成了,如果替換失敗,則說明有其他線程嘗試過獲取該鎖,就要在釋放鎖的同時喚醒被掛起的線程。

偏向鎖、輕量級鎖和重量級鎖的區別

偏向鎖的優點是加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距,缺點是如果線程間存在鎖競爭會帶來額外鎖撤銷的消耗,適用于只有一個線程訪問同步代碼塊的場景。

輕量級鎖的優點是競爭的線程不會阻塞,提高了程序的響應速度,缺點是如果線程始終得不到鎖會自旋消耗CPU,適用于追求響應時間和同步代碼塊執行非常快的場景。

重量級鎖的優點是線程競爭不使用自旋不會消耗CPU,缺點是線程會被阻塞,響應時間很慢,適應于追求吞吐量、同步代碼塊執行較慢的場景。


P9:Lock 接口

自 JDK 5 起 Java 類庫提供了 juc 并發包,Lock 接口是 juc 包的頂層接口。基于Lock 接口,用戶能夠以非塊結構來實現互斥同步,從而擺脫了語言特性的束縛,改為在類庫層面去實現同步。Lock 并未用到 synchronized,而是利用了 volatile 的可見性。

重入鎖 ReentrantLock 是 Lock 接口最常見的一種實現,它與 synchronized 一樣是可重入的,在基本用法上也很相似,不過它增加了一些高級功能,主要包括以下三項:

  • 等待可中斷:是指持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待而處理其他事情。可中斷特性對處理執行時間非常長的同步塊很有幫助。
  • 公平鎖:是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖,而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock在默認情況下也是非公平的,但可以通過帶有布爾值的構造方法要求使用公平鎖。不過一旦使用了公平鎖,將會導致性能急劇下降,明顯影響吞吐量。
  • 鎖綁定多個條件:是指一個 ReentrantLock 對象可以同時綁定多個 Condition 對象。在 synchronized中,鎖對象的 wait 跟它的notify/notifyAll 方法配合可以實現一個隱含的條件,如果要和多于一個的條件關聯時就不得不額外添加一個鎖,而 ReentrantLock 可以多次調用 newCondition 方法。

一般優先考慮使用synchronized:① synchronized 是 Java 語法層面的同步,足夠清晰和簡單。② Lock 應該確保在 finally 中釋放鎖,否則一旦受同步保護的代碼塊中拋出異常,則有可能永遠不會釋放持有的鎖。這一點必須由程序員自己來保證,而使用 synchronized 可以由 JVM 來確保即使出現異常鎖也能被正常釋放。③ 盡管在 JDK 5 時ReentrantLock 的性能領先于 synchronized,但在 JDK 6 進行鎖優化之后,二者的性能基本能夠持平。從長遠來看 JVM 更容易針對synchronized進行優化,因為 JVM 可以在線程和對象的元數據中記錄 synchronized 中鎖的相關信息,而使用Lock的話 JVM 很難得知具體哪些鎖對象是由特定線程持有的。

ReentrantLock 的可重入實現

以非公平鎖為例,通過 nonfairTryAcquire 方法獲取鎖,該方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否為獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求則將同步狀態值進行增加并返回 true,表示獲取同步狀態成功。

成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,這就要求 ReentrantLock 在釋放同步狀態時減少同步狀態值。如果該鎖被獲取了 n 次,那么前 n-1 次 tryRelease 方法必須都返回fasle,只有同步狀態完全釋放了才能返回 true,該方法將同步狀態是否為 0 作為最終釋放的條件,當同步狀態為 0 時,將占有線程設置為null,并返回 true 表示釋放成功。

對于非公平鎖只要 CAS 設置同步狀態成功則表示當前線程獲取了鎖,而公平鎖則不同。公平鎖使用 tryAcquire 方法,該方法與nonfairTryAcquire 的唯一區別就是判斷條件中多了對同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回 true 表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取并釋放鎖之后才能繼續獲取鎖。、


P10:讀寫鎖

ReentrantLock 是排他鎖,在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一個讀鎖和一個寫鎖,通過分離讀寫鎖使并發性相比一般的排他鎖有了很大提升。

除了保證寫操作對讀操作的可見性以及并發性的提升之外,讀寫鎖能夠簡化讀寫交互場景的編程方式。只需要在讀操作時獲取讀鎖,寫操作時獲取寫鎖即可,當寫鎖被獲取時后續的讀寫操作都會被阻塞,寫鎖釋放之后所有操作繼續執行,編程方式相對于使用等待/通知機制的實現方式變得簡單。

讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。讀寫鎖的自定義同步器需要在同步狀態即一個整形變量上維護多個讀線程和一個寫線程的狀態。如果在一個 int 型變量上維護多種狀態,就一定要按位切割使用這個變量,讀寫鎖將變量切分成了兩個部分,高 16 位表示讀,低 16 位表示寫。

寫鎖是一個支持重入的排他鎖,如果當前線程已經獲得了寫鎖則增加寫狀態,如果當前線程在獲取寫鎖時,讀鎖已經被獲取或者該線程不是已經獲得寫鎖的線程則當前線程進入等待狀態。寫鎖的釋放與 ReentrantLock 的釋放過程類似,每次釋放均減少寫狀態,當寫狀態為 0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續訪問讀寫鎖,同時前次寫線程的修改對后續讀寫線程可見。

讀鎖是一個支持重入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問時,讀鎖總會被成功地獲取,而所做的只是線程安全地增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取則進入等待狀態。讀鎖的每次釋放均會減少讀狀態,減少的值是(1<<16),讀鎖的每次釋放是線程安全的。

鎖降級指的是寫鎖降級成為讀鎖,如果當前線程擁有寫鎖,然后將其釋放,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級指的是把持住當前擁有的寫鎖,再獲取到讀鎖,隨后釋放先前擁有的寫鎖的過程。

鎖降級中讀鎖的獲取是必要的,主要是為了保證數據的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程 A 獲取了寫鎖修改了數據,那么當前線程是無法感知線程 A 的數據更新的。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,線程 A 將會被阻塞,直到當前線程使用數據并釋放讀鎖之后,線程 A 才能獲取寫鎖進行數據更新。


P11:AQS 隊列同步器

隊列同步器是用來構建鎖或者其他同步組件的基礎框架,它使用了一個 volatile int state 變量作為共享資源,如果線程獲取資源失敗,則進入同步 FIFO 隊列中等待;如果獲取成功就執行臨界區代碼,執行完釋放資源時,會通知同步隊列中的等待線程來獲取資源后出隊并執行。

使用方式

同步器的主要使用方式是繼承,子類通過繼承同步器并實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法 getState、setState 和 compareAndSetState 來進行操作,因為它們能夠保證狀態的改變是安全的。子類推薦被定義為自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨占式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型地同步組件。

和鎖的關系

同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所關注的領域。

同步隊列

AQS 中每當有新的線程請求資源時,該線程都會進入一個等待隊列,只有當持有鎖的線程釋放鎖資源后該線程才能持有資源。等待隊列通過雙向鏈表實現,線程會被封裝在鏈表的 Node 節點中,Node 的等待狀態包括:CANCELLED 表示線程已取消、SIGNAL 表示線程需要喚醒、CONDITION 表示線程正在等待、PROPAGATE 表示后繼節點會傳播喚醒操作,只會在共享模式下起作用。

兩種模式

獨占模式表示鎖會被一個線程占用,其他線程必須等到持有鎖的線程釋放鎖后才能獲取到鎖繼續執行,在同一時間內只能有一個線程獲取到這個鎖,ReentrantLock 就采用的是獨占模式。

共享模式表示多個線程獲取同一個鎖的時候有可能會成功,ReadLock 就采用的是共享模式。

獨占模式通過 acquire 和 release 方法獲取和釋放鎖,共享模式通過 acquireShared 和 releaseShared 方法獲取和釋放鎖。

獨占式的獲取和釋放流程

在獲取同步狀態時,同步器調用 acquire 方法,維護一個同步隊列,使用 tryAcquire 方法安全地獲取線程同步狀態,獲取狀態失敗的線程會構造同步節點并通過 addWaiter 方法被加入到同步隊列的尾部,并在隊列中進行自旋。之后會調用 acquireQueued 方法使得該節點以死循環的方式獲取同步狀態,如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞節點被中斷實現,移出隊列或停止自旋的條件是前驅節點是頭結點并且成功獲取了同步狀態。

在釋放同步狀態時,同步器調用 tryRelease 方法釋放同步狀態,然后調用 unparkSuccessor 方法(該方法使用 LockSupport 喚醒處于等待狀態的線程)喚醒頭節點的后繼節點,進而使后繼節點重新嘗試獲取同步狀態。

只有當前驅節點是頭節點時才能夠嘗試獲取同步狀態原因

頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放同步狀態之后,將會喚醒其后繼節點,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點。

維護同步隊列的FIFO原則,節點和節點在循環檢查的過程中基本不相互通信,而是簡單地判斷自己的前驅是否為頭節點,這樣就使得節點的釋放規則符合FIFO,并且也便于對過早通知的處理(過早通知是指前驅節點不是頭結點的線程由于中斷而被喚醒)。

共享式的獲取和釋放流程

在獲取同步狀態時,同步器調用 acquireShared 方法,該方法調用 tryAcquireShared 方法嘗試獲取同步狀態,返回值為 int 類型,當返回值大于等于 0 時表示能夠獲取到同步狀態。因此在共享式獲取鎖的自旋過程中,成功獲取到同步狀態并退出自旋的條件就是該方法的返回值大于等于0。

釋放同步狀態時,調用 releaseShared 方法,釋放同步狀態后會喚醒后續處于等待狀態的節點。對于能夠支持多線程同時訪問的并發組件,它和獨占式的主要區別在于 tryReleaseShared 方法必須確保同步狀態安全釋放,一般通過循環和 CAS 來保證,因為釋放同步狀態的操作會同時來自多個線程。


P12:線程

現代操作系統在運行一個程序時會為其創建一個進程,而操作系統調度的最小單位是線程,線程也叫輕量級進程。在一個進程中可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,并且能夠訪問共享的內存變量。處理器在這些線程上告訴切換,讓使用者感覺到這些線程在同時執行。

生命周期

① NEW:新建狀態,是線程被創建且未啟動的狀態,此時還未調用 start 方法。

② RUNNABLE:Java 線程將操作系統中的就緒和運行兩種狀態統稱為 RUNNABLE,此時線程有可能正在等待操作系統分配CPU時間片,也有可能正在執行。

③ BLOCKED:阻塞狀態,可能由于鎖被其他線程占用、調用了 sleep 或 join 方法、執行了 wait 方法等。

④ WAITING:等待狀態,處于該狀態的線程不會被分配CPU時間片,當前線程需要等待其他線程通知或中斷。導致線程進入該狀態的方法:無參數的 wait 和 join 方法、LockSupport 的 park 方法。

⑤ TIME_WAITING:限期等待狀態,可以在指定時間內自行返回。導致線程進入該狀態的方法:有參數的 wait 和 join 方法、LockSupport 的 parkNanos 和 parkUntil 方法。

⑥ TERMINATED:終止狀態,表示當前線程已經執行完畢或異常退出。

實現方式

① 繼承 Thread 類并重寫 run 方法。優點是實現簡單,缺點是不符合里氏替換原則,不可以繼承其他類。② 實現 Runnable 接口并重寫 run 方法。優點是避免了單繼承的局限性,使編程更加靈活,實現解耦操作,對外暴露細節少。③實現 Callable 接口并重寫 call 方法。優點是可以獲取線程執行結果的返回值,并且可以拋出異常。

方法

① wait 是Object類的方法,調用 wait 方法的線程會進入 WAITING 狀態,只有等待其他線程的通知或被中斷后才會解除阻塞,調用wait方法會釋放鎖資源。② sleep 是 Thread 類的方法,調用 sleep 方法會導致當前線程進入休眠狀態,與 wait 不同的是該方法不會釋放鎖資源,進入的是 TIMED-WAITING 狀態。③ yiled 方法會使當前線程讓出 CPU 時間片給優先級相同或更高的線程,回到 RUNNABLE 狀態,與其他線程一起重新競爭CPU時間片。④ join 方法用于等待其他線程運行終止,如果當前線程調用了另一個線程的 join 方法,則當前線程進入阻塞狀態,當另一個線程結束時當前線程才能從阻塞狀態轉為就緒態,等待獲取CPU時間片。底層使用的是wait,也會釋放鎖。

守護線程

守護線程是一種支持型線程,因為它主要被用作程序中后臺調度以及支持性工作,當 JVM 中不存在非守護線程時,JVM 將會退出,可以通過 setDaemon(true) 將線程設置為daemon線程,但必須在線程啟動之前設置。守護線程被用于完成支持性工作,但是在 JVM 退出時守護線程中的 finally 塊并不一定會被執行,因為當 JVM 中沒有非守護線程時需要立即退出,所有守護線程都將立即終止,因此不能依靠 finally 確保執行關閉或清理資源的邏輯。


P13:線程間通信

通信是指線程之間以何種機制來交換信息,在命令式編程中線程之間的通信機制有兩種,共享內存和消息傳遞。在共享內存的并發模型里線程之間共享程序的公共狀態,通過寫-讀內存中的公共狀態進行隱式通信。在消息傳遞的并發模型里線程之間沒有公共狀態,線程之間必須通過發送消息來顯示通信。Java 并發采用共享內存模型,線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。

volatile 和 synchronized 關鍵字

volatile 可以修飾字段,告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回主內存,它能保證所有線程對變量訪問的可見性。

synchronized 可以修飾方法或以同步塊的形式使用,它主要確保多個線程在同一個時刻只能有一個線程處于方法或同步塊中,保證了線程對變量訪問的可見性和排他性。

等待/通知機制

等待通知機制是指一個線程 A 調用了對象 O 的 wait 方法進入等待狀態,而另一個線程 B 調用了對象 O 的 notify 或 notifyAll 方法,線程 A 收到通知后從 wait 方法返回,進而執行后序操作。兩個線程通過對象 O 完成交互,對象上的 wait 和 notify/notifyAll 就如同開關信號,用來完成等待方和通知方之間的交互工作。

Thread.join

如果一個線程執行了某個線程的 join 方法,這個線程就會阻塞等待執行了 join 方法的線程終止之后才返回,這里涉及了等待/通知機制。join 方法的底層是通過 wait 方法實現的,當線程終止時會調用自身的 notifyAll 方法,通知所有等待在該線程對象上的線程。

管道 IO 流

管道 IO 流主要用于線程之間的數據傳輸,傳輸的媒介為內存。PipedOutputStream 和 PipedWriter 是管道輸出流,相當于生產者,PipedInputStream 和 PipedReader 是輸入流,相當于消費者。管道流使用一個循環緩沖數組來實現,默認大小為 1024B。輸入流從這個緩沖數組中讀數據,輸出流往這個緩沖數組中寫入數據。當數組已滿時,輸出流所在的線程將阻塞;當數組首次為空時,輸入流所在的線程將阻塞。

ThreadLocal

ThreadLocal 是共享變量,但它可以為每個線程創建單獨的副本,副本值是線程私有的,互相之間不會影響。


P14:ConcurrentHashMap

JDK 8 之前

ConcurrentHashMap 用于解決 HashMap 的線程不安全和 HashTable 的并發效率低下問題,HashTable 之所以效率低下是因為所有線程都必須競爭同一把鎖,假如容器里有多把鎖,每一把鎖用于鎖容器一部分數據,那么多線程訪問容器不同數據段的數據時線程間就不會存在鎖競爭,從而有效提高并發效率,這就是 ConcurrentHashMap 的鎖分段技術。首先將數據分成 Segment 數據段,然后給每一個數據段配一把鎖,當一個線程占用鎖訪問其中一個段的數據時,其他段的數據也能被其他線程訪問。

get 操作實現簡單高效,先經過一次再散列,然后使用這個散列值通過散列運算定位到 Segment,再通過散列算法定位到元素。get 的高效在于這個過程不需要加鎖,除非讀到空值才會加鎖重讀。get 方法里將要使用的共享變量都定義為 volatile 類型,volatile 保證了多線程的可見性,可以多線程讀,但是只能保證單線程寫,在 get 操作里只需要讀所以不用加鎖。

put 操作必須加鎖,put 方法首先定位到 Segment,然后進行插入操作,第一步判斷是否需要對 Segment 里的 HashEntry 數組進行擴容,第二步定位添加元素的位置,然后將其放入數組。

size 操作用于統計元素的數量,必須統計每個 Segment 的大小然后求和,在統計結果累加的過程中,之前累加過的 count 變化的幾率很小,因此 ConcurrentHashMap 的做法是先嘗試兩次通過不加鎖的方式統計結果,如果統計過程中容器大小發生了變化則再通過加鎖的方式統計所有 Segment 的大小。判斷容器是否發生變化是根據 modCount 變量確定的。

JDK 8 開始

主要對 JDK 7 的版本做了三點改造:① 取消分段鎖機制,進一步降低沖突概率。② 引入紅黑樹結構,同一個哈希槽上的元素個數超過一定閾值后,單向鏈表改為紅黑樹結構。③ 使用了更加優化的方式統計集合內的元素數量。具體優化表現在:在 put、resize 和 size 方法中設計元素總數的更新和計算都避免了鎖,使用 CAS 操作代替。Map 原有的 size 方法最大只能表示到 231-1,ConcurrentHashMap 提供了 mappingCount 方法用來返回集合內元素的數量,最大可用表示到 263-1。

get 操作同樣不需要同步控制,put 操作時如果沒有出現哈希沖突,就使用 CAS 方式來添加元素,如果出現了哈希沖突就使用 synchronized 加鎖的方式添加元素。

當某個槽內的元素個數增加到超過 8 個且 table 容量大于等于 64 時,由鏈表轉為紅黑樹。當某個槽內的元素減少到 6 個時,由紅黑樹重新轉為鏈表。鏈表轉紅黑樹的過程,就是把給定順序的元素構造成一棵紅黑樹的過程,需要注意的是,當 table 的容量小于 64 時,只會擴容,并不會把鏈表轉為紅黑樹。在轉化過程中,使用同步塊鎖住當前槽的首元素,防止其他線程對當前槽進行增刪改操作,轉化完成后利用 CAS 替換原有鏈表。由于 TreeNode 節點也存儲了 next 引用,因此紅黑樹轉為鏈表很簡單,只需從 TreeBin 的 first 元素開始遍歷所有節點,并把節點從 TreeNode 類型轉為 Node 類型即可,當構造好新鏈表后同樣會用 CAS 替換紅黑樹。


P15:CAS 操作

CAS 表示 Compare And Swap,比較并交換,CAS 需要三個操作數,分別是內存位置 V、舊的預期值 A 和準備設置的新值 B。CAS 指令執行時,當且僅當 V 符合 A 時,處理器才會用 B 更新 V 的值,否則它就不執行更新。但不管是否更新都會返回 V 的舊值,這些處理過程是原子操作,執行期間不會被其他線程打斷。

在 JDK 5 后,Java 類庫中才開始使用 CAS 操作,該操作由 Unsafe 類里的 compareAndSwapInt 等幾個方法包裝提供。HotSpot 在內部對這些方法做了特殊處理,即時編譯的結果是一條平臺相關的處理器 CAS 指令。Unsafe 類不是給用戶程序調用的類,因此在 JDK 9 之前只有 Java 類庫可以使用 CAS,譬如 juc 包里的 AtomicInteger類中 compareAndSet 等方法都使用了Unsafe 類的 CAS 操作來實現。

盡管 CAS 既簡單又高效,但這種操作無法涵蓋互斥同步的所有場景,并且 CAS 從語義上來說存在一個邏輯漏洞:如果 V 初次讀取的時候是 A,并且在準備賦值的時候檢查到它的值仍為 A,這依舊不能說明它的值沒有被其他線程更改過,因為這段時間內假設它的值先改為了 B 又改回 A,那么 CAS 操作就會誤認為它從來沒有被改變過。這個漏洞稱為 ABA 問題,juc 包提供了一個 AtomicStampedReference,原子更新帶有版本號的引用類型,它可以通過控制變量值的版本來解決 ABA 問題。這個類并不常用,大部分情況下 ABA 問題不會影響程序并發的正確性,如果需要解決該問題,改用傳統的互斥同步可能會比原子類更高效。


P16:原子操作類

Java 從 JDK 5 開始提供了 java.util.concurrent.atomic 包,這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。到 JDK 8 該包共有17個類,依據作用分為四種:原子更新基本類型類、原子更新數組類、原子更新引用類以及原子更新字段類,atomic 包里的類基本都是使用 Unsafe 實現的包裝類。

原子更新基本類型

AtomicInteger 原子更新整形、 AtomicLong 原子更新長整型、AtomicBoolean 原子更新布爾類型。

getAndIncrement 以原子方式將當前的值加 1,首先在 for 死循環中取得 AtomicInteger 里存儲的數值,第二步對 AtomicInteger 當前的值進行加 1 操作,第三步調用 compareAndSet 方法進行原子更新,該操作先檢查當前數值是否等于 expect,如果等于則說明當前值沒有被其他線程修改,則將值更新為 next,否則會更新失敗返回 false,程序會進入 for 循環重新進行 compareAndSet 操作。

atomic 包中只提供了 三種基本類型的原子更新,atomic 包里的類基本都是使用 Unsafe 實現的,Unsafe 只提供三種 CAS 方法:compareAndSwapInt、compareAndSwapLong 和 compareAndSwapObject,例如原子更新 Boolean 時是先轉成整形再使用 compareAndSwapInt 進行 CAS,所以原子更新 char、float、double 也可以用類似思路實現。

原子更新數組

AtomicIntegerArray,原子更新整形數組里的元素、 AtomicLongArray 原子更新長整型數組里的元素、 AtomicReferenceArray 原子更新引用類型數組里的元素。

原子更新引用

AtomicReference 原子更新引用類型、AtomicMarkableReference 原子更新帶有標記位的引用類型,可以綁定一個 boolean 類型的標記位、 AtomicStampedReference 原子更新帶有版本號的引用類型,關聯一個整數值用于原子更新數據和數據的版本號,可以解決 ABA 問題。

原子更新字段

AtomicIntegerFieldUpdater 原子更新整形字段的更新器、 AtomicLongFieldUpdater 原子更新長整形字段的更新器AtomicReferenceFieldUpdater 原子更新引用類型字段的更新器。

由于原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法 newUpdater 創建一個更新器,并且需要設置想要更新的類和字段。并且更新類的字段必須使用 public volatile 修飾。

JDK 8 更新的類

DoubleAccumulator 、 LongAccumulator、DoubleAdder、LongAdder、Striped64。


P17:并發工具類

等待多線程完成的 CountDownLatch

CountDownLatch 是基于執行時間的同步類,允許一個或多個線程等待其他線程完成操作,構造方法接收一個 int 類型的參數作為計數器,如果要等待 n 個點就傳入 n。每次調用 countDown 方法時計數器減 1,await 方法會阻塞當前線程直到計數器變為0,由于 countDown方法可用在任何地方,所以 n 個點既可以是 n 個線程也可以是一個線程里的 n 個執行步驟。

循環屏障 CyclicBarrier

循環屏障是基于同步到達某個點的信號量觸發機制,作用是讓一組線程到達一個屏障時被阻塞,直到最后一個線程到達屏障時,屏障才會解除,所有被攔截的線程才會繼續運行。構造方法中的參數表示屏障攔截的線程數量,每個線程調用 await 方法告訴 CyclicBarrier 自己已到達屏障,然后當前線程被阻塞。還支持在構造方法中傳入一個 Runable 類型的任務,當線程到達屏障時會優先執行該任務。適用于多線程計算數據,最后合并計算結果的應用場景。

CountDownLacth 的計數器只能用一次,而 CyclicBarrier 的計數器可使用 reset 方法重置,所以 CyclicBarrier 能處理更為復雜的業務場景,例如計算錯誤時可用重置計數器重新計算。

控制并發線程數的 Semaphore

信號量用來控制同時訪問特定資源的線程數量,它通過協調各個線程以保證合理使用公共資源。信號量可以用于流量控制,特別是公共資源有限的應用場景,比如數據庫連接。Semaphore 的構造方法參數接收一個 int 值,表示可用的許可數量即最大并發數。使用acquire 方法獲得一個許可證,使用 release 方法歸還許可,還可以用 tryAcquire 嘗試獲得許可。

線程間交換數據的 Exchanger

交換者是用于線程間協作的工具類,用于進行線程間的數據交換。它提供一個同步點,在這個同步點兩個線程可以交換彼此的數據。這兩個線程通過 exchange 方法交換數據,如果第一個線程先執行exchange方法它會阻塞等待第二個線程執行exchange方法,當兩個線程都到達同步點時這兩個線程就可以交換數據,將本線程生產出的數據傳遞給對方。應用場景包括遺傳算法、校對工作等。


P18:線程池

作用

① 降低資源消耗,復用已創建的線程降低開銷、控制最大并發數。

② 隔離線程環境,可以配置獨立線程池,將較慢的線程與較快的隔離開,避免相互影響。

③ 實現任務線程隊列緩沖策略和拒絕機制。

④ 實現某些與時間相關的功能,如定時執行、周期執行等。

當提交一個新任務到線程池時的處理流程

① 核心線程池未滿,創建一個新的線程執行任務,此時 workCount < corePoolSize,這一步需要獲取全局鎖。

② 如果核心線程池已滿,工作隊列未滿,將線程存儲在工作隊列,此時 workCount >= corePoolSize。

③ 如果工作隊列已滿,線程數小于最大線程數就創建一個新線程處理任務,此時 workCount < maximumPoolSize,這一步也需要獲取全局鎖。

④ 如果超過大小線程數,按照拒絕策略來處理任務,此時 workCount > maximumPoolSize。

線程池采取這種設計思路是為了在執行 execute 方法時盡可能地避免獲取全局鎖,在線程池完成預熱之后,即當前運行的線程數大于等于corePoolSize 時,幾乎所有的 execute 方法都是執行步驟 2,不需要獲取全局鎖。

線程池創建線程時,會將線程封裝成工作線程 Worker,Worker 在執行完任務后還會循環獲取工作隊列中的任務來執行。線程池中的線程執行任務分為兩種情況:①在 execute 方法中創建一個線程時會讓這個線程執行當前任務。②這個線程執行完任務之后,就會反復從工作隊列中獲取任務并執行。

可以使用 execute 和 submit 方法向線程池提交任務。execute 用于提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功了。submit 用于提交需要返回值的任務,線程池會返回一個 Future 類型的對象,通過該對象可以判斷任務是否執行成功,并且可以通過該對象的 get 方法獲取返回值,get 會阻塞當前線程直到任務完成,帶超時參數的 get 方法會在阻塞當前線程一段時間后立即返回,這時任務可能還沒有完成。

創建線程池

可以通過 Executors 的靜態工廠方法創建線程池,其核心方法有五個:

① newFixedThreadPool,固定大小的線程池,輸入的參數既是核心線程數也是最大線程數,不存在空閑線程,因此 keepAliveTime 等于 0。該線程池使用的工作隊列是無界阻塞隊列 LinkedBlockingQueue,適用于為了滿足資源管理的需求,而需要限制當前線程數量的應用場景,適用于負載比較重的服務器。

② newSingleThreadExecutor,使用單線程的線程池,相當于單線程串行執行所有任務,適用于需要保證順序執行任務的應用場景。

③ newCachedThreadPool,maximumPoolSize 設置為 Integer 最大值,是高度可伸縮的線程池。該線程池使用的工作隊列是沒有容量的 SynchronousQueue,如果主線程提交任務的速度高于線程處理的速度,線程池會不斷創建新線程,極端情況下會創建過多線程而耗盡CPU 和內存資源。適用于執行很多短期異步任務的小程序或者負載較輕的服務器。

④ newScheduledThreadPool:線程數最大至 Integer 最大值,存在 OOM 風險。支持定期及周期性任務執行,適用于需要多個后臺線程執行周期任務,同時需要限制后臺線程數量的應用場景。相比 Timer 更加安全,功能更強大,與 newCachedThreadPool 的區別是不回收工作線程。

⑤ newWorkStealingPool:JDK 8 引入,創建持有足夠線程的線程池支持給定的并行度,并通過使用多個隊列減少競爭。

線程池的參數

① corePoolSize:常駐核心線程數,如果為 0,當執行完任務沒有任何請求時會消耗線程池;如果大于 0,即使本地任務執行完,核心線程也不會被銷毀。該值設置過大會浪費資源,過小會導致線程的頻繁創建與銷毀。

② maximumPoolSize:線程池能夠容納同時執行的線程最大數,必須大于等于 1,如果與核心線程數設置相同代表固定大小線程池。

③ keepAliveTime:線程空閑時間,線程空閑時間達到該值后會被銷毀,直到只剩下 corePoolSize 個線程為止,避免浪費內存資源。

④ unit:keepAliveTime 的時間單位。

⑤ workQueue:工作隊列,當線程請求數大于等于 corePoolSize 時線程會進入阻塞隊列。

⑥ threadFactory:線程工廠,用來生產一組相同任務的線程。可以給線程命名,有利于分析錯誤。

⑦ handler:拒絕策略,默認策略下使用 AbortPolicy 丟棄任務并拋出異常,CallerRunsPolicy 表示重新嘗試提交該任務,DiscardOldestPolicy 表示拋棄隊列里等待最久的任務并把當前任務加入隊列,DiscardPolicy 表示直接拋棄當前任務但不拋出異常。

線程池的狀態

① RUNNING:接受新的任務,處理等待隊列中的任務。線程池被一旦被創建,就處于RUNNING狀態。

② SHUTDOWN:不接受新的任務提交,但是會繼續處理等待隊列中的任務。對應線程池的 shutdown方法。

③ STOP:不接受新的任務提交,不再處理等待隊列中的任務,中斷正在執行任務的線程。對應 shutdownNow 方法。

④ TIDYING:當線程池在 SHUTDOWN 狀態下,阻塞隊列為空并且線程池中執行的任務也為空時,就會變為 TIDYING。當線程池在STOP 狀態下,線程池中執行的任務為空時,也會變為 TIDYING。

⑤ TERMINATED:線程池處于 TIDYING 狀態時,執行完 terminated 方法后就會進入終止狀態。

關閉線程池

可以通過調用 shutdown 或 shutdownNow 方法關閉線程池,原理是遍歷線程池中的工作線程,然后逐個調用線程的 interrupt 方法中斷線程,所以無法響應中斷的任務可能永遠無法終止。區別是 shutdownNow 首先將線程池的狀態設為 STOP,然后嘗試停止所有正在執行或暫停任務的線程,并返回等待執行任務的列表,而 shutdown 只是將線程池的狀態設為 SHUTDOWN,然后中斷所有沒有正在執行任務的線程。通常調用 shutdown 來關閉線程池,如果任務不一定要執行完則可以調用 shutdownNow。

合理設置線程池

首先可以從以下角度分析:①任務的性質:CPU密集型任務、IO密集型任務和混合型任務。②任務的優先級:高、中和低。③任務的執行時間:長、中和短。④任務的依賴性:是否依賴其他系統資源,如數據庫連接。

性質不同的任務可以用不同規模的線程池分開處理,CPU密集型任務應配置盡可能小的線程,如配置 Ncpu+1 個線程的線程池。由于IO密集型任務線程并不是一直在執行任務,則應配置盡可能多的線程,如 2 * Ncpu。混合型的任務,如果可以拆分,將其拆分為一個 CPU 密集型任務和一個 IO 密集型任務,只要這兩個任務執行的時間相差不是太大那么分解后的吞吐量將高于串行執行的吞吐量,如果相差太大則沒必要分解。

優先級不同的任務可以使用優先級隊列 PriorityBlockingQueue 處理。

執行時間不同的任務可以交給不同規模的線程池處理,或者使用優先級隊列讓執行時間短的任務先執行。

依賴數據庫連接池的任務,由于線程提交 SQL 后需要等待數據庫返回的結果,等待的時間越長 CPU 空閑的時間就越長,因此線程數應該盡可能地設置大一些提高CPU的利用率。

建議使用有界隊列,能增加系統的穩定性和預警能力,可以根據需要設置的稍微大一些。


P19:阻塞隊列

阻塞隊列支持阻塞的插入和移除,當隊列滿時,阻塞插入元素的線程直到隊列不滿。當隊列為空時,獲取元素的線程會被阻塞直到隊列非空。阻塞隊列常用于生產者和消費者的場景,阻塞隊列就是生產者用來存放元素,消費者用來獲取元素的容器。

Java 中的阻塞隊列

ArrayBlockingQueue,由數組組成的有界阻塞隊列,默認情況下不保證線程公平,有可能先阻塞的線程最后才訪問隊列。

LinkedBlockingQueue,由鏈表結構組成的有界阻塞隊列,隊列的默認和最大長度為 Integer 的最大值。

PriorityBlockingQueue,支持優先級排序的無界阻塞隊列,默認情況下元素按照順序升序排序。可以自定義 compareTo 方法指定元素排序規則,或者初始化時指定 Comparator 對元素排序,不能保證同優先級元素的順序。

DelayQueue,支持延時獲取元素的無界阻塞隊列,使用優先級隊列實現。創建元素時可以指定多久才能從隊列中獲取當前元素,只有延遲期滿時才能從隊列中獲取元素,適用于緩存系統和定時任務調度。

SynchronousQueue,不存儲元素的阻塞隊列,每一個 put 操作必須等待一個 take 操作。默認使用非公平策略,也支持公平策略,適用于傳遞性場景,吞吐量高于 ArrayBlockingQueue 和 LinkedBlockingQueue。

LinkedTransferQueue,由鏈表組成的無界阻塞隊列,相對于其他阻塞隊列多了 tryTransfer 和 transfer 方法。transfe方法:如果當前有消費者正在等待接收元素,可以把生產者傳入的元素立刻傳輸給消費者,如果沒有消費者等待接收元素,會將元素放在隊列的尾節點并等到該元素被消費者消費了才返回。tryTransfer 方法用來試探生產者傳入的元素能否直接傳給消費者,如果沒有消費者等待接收元素則返回 false,和transfer 的區別是無論消費者是否消費都會立即返回。

LinkedBlockingDeque,由鏈表組成的雙向阻塞隊列,可以從隊列的兩端插入和移出元素,在多線程同時入隊時減少了競爭。

實現原理

使用通知模式實現,當生產者往滿的隊列里添加元素時會阻塞生產者,當消費者消費了一個隊列中的元素后,會通知生產者當前隊列可用。例如 JDK 中的 ArrayBlockingQueue 使用了 Condition 實現。當往隊列里插入一個元素,如果隊列不可用,那么阻塞生產者主要通過LockSupport 的 park 方法實現,park 在不同的操作系統中使用不同的方式實現,在 Linux 下使用的是系統方法 pthread_cond_wait 實現。


P20:ThreadLoacl

ThreadLoacl 是線程變量,主要用于一個線程內,跨類、方法傳遞數據。ThreadLoacl 有一個靜態的內部類 ThreadLocalMap,ThreadLocalMap 的 Key 是 ThreadLocal 對象,值是 Entry 類型的元素,Entry 中只有一個 Object 類型的 vaule 值。ThreadLocal 是線程共享的,但是 ThreadLocalMap 是每個線程私有的。ThreadLocal 主要有 set、get 和 remove 三個方法。

set 方法

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}

首先獲取當前線程,然后再獲取當前線程對應的 ThreadLocalMap 類型的對象 map。如果 map 存在就直接設置值,key 是當前的 ThreadLocal 對象,value 就是傳入的參數。如果 map 不存在就通過 createMap 方法為當前線程創建一個 ThreadLocalMap 對象再設置值。

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();}

首先獲取當前線程,然后再獲取當前線程對應的 ThreadLocalMap 類型的對象 map。如果 map 存在就以當前 ThreadLocal 對象作為 key 獲取 Entry 類型的對象 e,如果 e 存在就返回它的 value 屬性。如果 e 不存在或者 map 不存在,就調用 setInitialValue 方法先為當前線程創建一個 ThreadLocalMap 對象然后返回默認的初始值 null。

remove 方法

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

首先還是通過當前線程獲取其對應的 ThreadLocalMap 類型的對象 m,如果 m 不為空,就解除 ThreadLocal 這個 key 及其對應的 value 值的聯系。

存在的問題

線程復用會產生臟數據,由于線程池會重用 Thread 對象,因此與 Thread 綁定的 ThreadLocal 也會被重用。如果沒有調用 remove 清理與線程相關的 ThreadLocal 信息,那么假如下一個線程沒有調用 set 設置初始值就可能 get 到重用的線程信息。

ThreadLocal 還存在內存泄漏的問題,由于 ThreadLocal 是弱引用,但 Entry 的 value 是強引用,因此當 ThreadLocal 被垃圾回收后,value 依舊不會被釋放。因此需要及時調用 remove 方法進行清理操作。


Spring 10

P1:Spring 框架

Spring 是分層的企業級應用輕量級開源框架,以 IoC 和 AOP為內核。Spring 可以降低企業級應用開發的復雜性,對此主要采取了四個關鍵策略:基于 POJO 的輕量級和最小侵入性編程、通過依賴注入和面向接口實現松耦合、基于切面和慣性進行聲明式編程、通過切面和模板減少樣板式代碼。

好處

降低代碼耦合度、簡化開發。通過 Spring 提供的 IoC 容器可以將對象間的依賴關系交由 Spring 進行控制,避免硬編碼所造成的過度程序耦合。用戶也不必再為單例模式類、屬性文件解析等這些底層的需求編寫代碼,可以更專注于上層的應用。

AOP 編程以及聲明式事務的支持。通過 Spring 的 AOP 功能可以方便進行面向切面的編程,通過聲明式事務可以靈活進行事務管理,提高開發效率和質量。

方便程序的測試和集成各種框架。可以用非容器依賴的編程方式進行幾乎所有的測試工作,可以降低各種框架的使用難度,提供了對 Mybatis 和 Hibernate 等框架的直接支持。

降低了 JavaEE API 的使用難度。Spring 對 JDBC、JavaMail、遠程調用等 API 進行了封裝,使這些 API 的使用難度大幅降低。

核心容器

核心容器由 spring-beans、spring-core、spring-context 和 spring-expression 四個模塊組成。

spring-beans 和 spring-core 模塊是 Spring 的核心模塊,包括了控制反轉和依賴注入。BeanFactory 使用控制反轉對應用程序的配置和依賴性規范與實際的應用代碼進行分離,BeanFactory 實例化后并不會自動實例化 Bean,只有當 Bean 被使用時才會對其進行實例化與依賴關系的裝配。

spring-context 模塊構架于核心模塊之上,擴展了 BeanFactory,為它添加了 Bean 的生命周期控制、框架事件體系及資源透明化加載等功能。ApplicationConext 是該模塊的核心接口,它是 BeanFactory 的子接口,它實例化后會自動對所有單例 Bean 進行實例化與依賴關系的裝配,使之處于待用狀態。

spring-expression 是 EL 語言的擴展模塊,可以查詢、管理運行中的對象,同時也可以方便地調用對象方法,以及操作數組、集合等。


P2:IoC 控制反轉

IoC 即控制反轉,是一種給予應用程序中目標組件更多控制的設計范式,簡單來說就是把原來代碼里需要實現的對象創建、依賴反轉給容器來幫忙實現,需要創建一個容器并且需要一種描述來讓容器知道要創建的對象之間的關系,在 Spring 框架中管理對象及其依賴關系是通過 Spring 的 IoC 容器實現的,IoC 的作用是降低代碼耦合度。

IoC 的實現方式有依賴注入和依賴查找,由于依賴查找使用的很少,因此 IoC 也叫做依賴注入。依賴注入指對象被動地接受依賴類而不用自己主動去找,對象不是從容器中查找它依賴的類,而是在容器實例化對象時主動將它依賴的類注入給它。假設一個 Car 類需要一個 Engine 的對象,那么一般需要需要手動 new 一個 Engine,利用 IoC 就只需要定義一個私有的 Engine 類型的成員變量,容器會在運行時自動創建一個 Engine 的實例對象并將引用自動注入給成員變量。

基于 XML 的容器初始化

當創建一個 ClassPathXmlApplicationContext 時,構造方法做了兩件事:首先調用父容器的構造方法為容器設置好 Bean 資源加載器,然后調用父類的 setConfigLocations 方法設置 Bean 配置信息的定位路徑。

ClassPathXmlApplicationContext 通過調用父類 AbstractApplicationContext 的 refresh 方法啟動整個 IoC 容器對 Bean 定義的載入過程,refresh 是一個模板方法,規定了 IoC 容器的啟動流程。refresh 方法的主要作用是:在創建 IoC 容器之前如果已有容器存在,需要把已有的容器銷毀和關閉,以保證在 refresh 方法之后使用的是新創建的 IoC 容器。

容器創建后通過 loadBeanDefinitions 方法加載 Bean 配置資源,該方法會做兩件事:首先調用資源加載器的方法獲取要加載的資源,其次真正執行加載功能,由子類 XmlBeanDefinitionReader 實現。在加載資源時,首先會解析配置文件路徑,讀取配置文件的內容,然后通過 XML 解析器將 Bean 配置信息轉換成文檔對象,之后再按照 Spring Bean 的定義規則對文檔對象進行解析。

Spring IoC 容器中注冊解析的 Bean 信息存放在一個 HashMap 集合中,key 是 String 字符串,值是 BeanDefinition,在注冊過程中需要使用 synchronized 同步塊保證線程安全。當 Bean 配置信息中配置的 Bean 被解析后且被注冊到 IoC 容器中,初始化就算真正完成了,Bean 定義信息已經可以使用,并且可以被檢索。Spring IoC 容器的作用就是對這些注冊的 Bean 定義信息進行處理和維護,注冊的 Bean 定義信息是控制反轉和依賴注入的基礎。

基于注解的容器初始化

Spring 對注解的處理分為兩種方式:① 直接將注解 Bean 注冊到容器中,可以在初始化容器時注冊,也可以在容器創建之后手動注冊,然后刷新容器使其對注冊的注解 Bean 進行處理。② 通過掃描指定的包及其子包的所有類處理,在初始化注解容器時指定要自動掃描的路徑。


P3:DI 依賴注入

可注入的數據類型

基本數據類型和 String、集合類型、Bean 類型。

實現方式

構造方法注入:IoC Service Provider 會檢查被注入對象的構造方法,取得它所需要的依賴對象列表,進而為其注入相應的對象。這種方法的優點是在對象構造完成后就處于就緒狀態,可以馬上使用。缺點是當依賴對象較多時,構造方法的參數列表會比較長,構造方法無法被繼承,無法設置默認值。對于非必需的依賴處理可能需要引入多個構造方法,參數數量的變動可能會造成維護的困難。

setter 方法注入:當前對象只需要為其依賴對象對應的屬性添加 setter 方法,就可以通過 setter 方法將依賴對象注入到被依賴對象中。setter 方法注入在描述性上要比構造方法注入強,并且可以被繼承,允許設置默認值。缺點是無法在對象構造完成后馬上進入就緒狀態。

接口注入:必須實現某個接口,這個接口提供一個方法來為其注入依賴對象。使用較少,因為它強制要求被注入對象實現不必要的接口,侵入性強。

相關注解

@Autowired:自動按類型注入,如果有多個匹配則按照指定 Bean 的 id 查找,查找不到會報錯。

@Qualifier:在自動按照類型注入的基礎上再按照 Bean 的 id 注入,給變量注入時必須搭配 @Autowired,給方法注入時可單獨使用。

@Resource :直接按照 Bean 的 id 注入,只能注入 Bean 類型。

@Value :用于注入基本數據類型和 String 類型。

依賴注入的過程

getBean 方法是獲取 Bean 實例的方法,該方法會調用 doGetBean 方法,doGetBean 真正實現向 IoC 容器獲取 Bean 的功能,也是觸發依賴注入的地方。如果 Bean 定義為單例模式,容器在創建之前先從緩存中查找以確保整個容器中只存在一個實例對象。如果 Bean 定義為原型模式,則容器每次都會創建一個新的實例。

具體創建 Bean 實例對象的過程由 ObjectFactory 的 createBean 方法完成,該方法主要通過 createBeanInstance 方法生成 Bean 包含的 Java 對象實例和 populateBean 方法對 Bean 屬性的依賴注入進行處理。

在 createBeanInstance 方法中根據指定的初始化策略,通過簡單工廠、工廠方法或容器的自動裝配特性生成 Java 實例對象,對工廠方法和自動裝配特性的 Bean,調用相應的工廠方法或參數匹配的構造方法即可完成實例化對象的工作,但最常用的默認無參構造方法需要使用 JDK 的反射或 CGLib 來進行初始化。

在 populateBean 方法中,注入過程主要分為兩種情況:① 屬性值類型不需要強制轉換時,不需要解析屬性值,直接進行依賴注入。② 屬性值類型需要強制轉換時,首先需要解析屬性值,然后對解析后的屬性值進行依賴注入。依賴注入的過程就是將 Bean 對象實例設置到它所依賴的 Bean 對象屬性上,真正的依賴注入是通過 setPropertyValues 方法實現的,該方法使用了委派模式。

BeanWrapperImpl 類負責對容器完成初始化的 Bean 實例對象進行屬性的依賴注入,對于非集合類型的屬性,大量使用 JDK 的反射機制,通過屬性的 getter 方法獲取指定屬性注入前的值,同時調用屬性的 setter 方法為屬性設置注入后的值。對于集合類型的屬性,將屬性值解析為目標類型的集合后直接賦值給屬性。

當 Spring IoC 容器對 Bean 定義資源的定位、載入、解析和依賴注入全部完成后,就不再需要我們手動創建所需的對象,Spring IoC 容器會自動為我們創建對象并且注入好相關依賴。


P4:Bean 對象

生命周期

在 IoC 容器的初始化過程中會對 Bean 定義完成資源定位,加載讀取配置并解析,最后將解析的 Bean 信息放在一個 HashMap 集合中。當 IoC 容器初始化完成后,會進行對 Bean 實例的創建和依賴注入過程,注入對象依賴的各種屬性值,在初始化時可以指定自定義的初始化方法。經過這一系列初始化操作后 Bean 達到可用狀態,接下來就可以使用 Bean 了,當使用完成后會調用 destroy 方法進行銷毀,此時也可以指定自定義的銷毀方法,最終 Bean 被銷毀且從容器中移除。

指定 Bean 初始化和銷毀的方法:

XML 方式通過配置 bean 標簽中的 init-Method 和 destory-Method 指定自定義初始化和銷毀方法。

注解方式通過 @PreConstruct 和 @PostConstruct 注解指定自定義初始化和銷毀方法。

作用范圍

通過 scope 屬性指定 bean 的作用范圍,包括:① singleton:單例模式,是默認作用域,不管收到多少 Bean 請求每個容器中只有一個唯一的 Bean 實例。② prototype:原型模式,和 singleton 相反,每次 Bean 請求都會創建一個新的實例。③ request:每次 HTTP 請求都會創建一個新的 Bean 并把它放到 request 域中,在請求完成后 Bean 會失效并被垃圾收集器回收。④ session:和 request 類似,確保每個 session 中有一個 Bean 實例,session 過期后 bean 會隨之失效。⑤ global session:當應用部署在 Portlet 容器中時,如果想讓所有 Portlet 共用全局存儲變量,那么這個變量需要存儲在 global session 中。

創建方式

XML

通過默認無參構造方法,只需要指明 bean 標簽中的 id 和 class 屬性,如果沒有無參構造方法會報錯。

使用靜態工廠方法,通過 bean 標簽中的 class 屬性指明靜態工廠,factory-method 屬性指明靜態工廠方法。

使用實例工廠方法,通過 bean 標簽中的 factory-bean 屬性指明實例工廠,factory-method 屬性指明實例工廠方法。

注解

@Component 把當前類對象存入 Spring 容器中,相當于在 xml 中配置一個 bean 標簽。value 屬性指定 bean 的 id,默認使用當前類的首字母小寫的類名。

@Controller,@Service,@Repository 三個注解都是 @Component 的衍生注解,作用及屬性都是一模一樣的。只是提供了更加明確語義,@Controller 用于表現層,@Service用于業務層,@Repository用于持久層。如果注解中有且只有一個 value 屬性要賦值時可以省略 value。

如果想將第三方的類變成組件又沒有源代碼,也就沒辦法使用 @Component 進行自動配置,這種時候就要使用 @Bean 注解。被 @Bean 注解的方法返回值是一個對象,將會實例化,配置和初始化一個新對象并返回,這個對象由 Spring 的 IoC 容器管理。name 屬性用于給當前 @Bean 注解方法創建的對象指定一個名稱,即 bean 的 id。當使用注解配置方法時,如果方法有參數,Spring 會去容器查找是否有可用 bean對象,查找方式和 @Autowired 一樣。

@Configuration 用于指定當前類是一個 spring 配置類,當創建容器時會從該類上加載注解,value 屬性用于指定配置類的字節碼。

@ComponentScan 用于指定 Spring 在初始化容器時要掃描的包。basePackages 屬性用于指定要掃描的包。

@PropertySource 用于加載 .properties 文件中的配置。value 屬性用于指定文件位置,如果是在類路徑下需要加上 classpath。

@Import 用于導入其他配置類,在引入其他配置類時可以不用再寫 @Configuration 注解。有 @Import 的是父配置類,引入的是子配置類。value 屬性用于指定其他配置類的字節碼。

BeanFactory、FactoryBean 和 ApplicationContext 的區別

BeanFactory 是一個 Bean 工廠,使用了簡單工廠模式,是 Spring IoC 容器最頂級的接口,可以理解為含有 Bean 集合的工廠類,它的作用是管理 Bean,包括實例化、定位、配置應用程序中的對象及建立這些對象之間的依賴。BeanFactory 實例化后并不會自動實例化 Bean,只有當 Bean 被使用時才會對其進行實例化與依賴關系的裝配,屬于延遲加載,適合多例模式。

FactoryBean 是一個工廠 Bean,使用了工廠方法模式,作用是生產其他 Bean 實例,可以通過實現該接口,提供一個工廠方法來自定義實例化 Bean 的邏輯。FactoryBean 接口由 BeanFactory 中配置的對象實現,這些對象本身就是用于創建對象的工廠,如果一個 Bean 實現了這個接口,那么它就是創建對象的工廠 Bean,而不是 Bean 實例本身。

ApplicationConext 是 BeanFactory 的子接口,擴展了 BeanFactory 的功能,提供了支持國際化的文本消息,統一的資源文件讀取方式,事件傳播以及應用層的特別配置等。容器會在初始化時對配置的 Bean 進行預實例化,Bean 的依賴注入在容器初始化時就已經完成,屬于立即加載,適合單例模式,一般推薦使用 ApplicationContext。


P5:AOP 面向切面編程

概念和原理

AOP 即面向切面編程,簡單地說就是將代碼中重復的部分抽取出來,在需要執行的時候使用動態代理的技術,在不修改源碼的基礎上對方法進行增強。優點是可以減少代碼的冗余,提高開發效率,維護方便。

Spring 會根據類是否實現了接口來判斷動態代理的方式,如果實現了接口會使用 JDK 的動態代理,核心是 InvocationHandler 接口和 Proxy 類,如果沒有實現接口會使用 CGLib 動態代理,CGLib 是在運行時動態生成某個類的子類,如果某一個類被標記為 final,是不能使用 CGLib 動態代理的。

JDK 動態代理主要通過重組字節碼實現,首先獲得被代理對象的引用和所有接口,生成新的類必須實現被代理類的所有接口,動態生成Java 代碼后編譯新生成的 .class 文件并重新加載到 JVM 運行。JDK 代理直接寫 Class 字節碼,CGLib 是采用 ASM 框架寫字節碼,生成代理類的效率低。但是 CGLib 調用方法的效率高,因為 JDK 使用反射調用方法,CGLib 使用 FastClass 機制為代理類和被代理類各生成一個類,這個類會為代理類或被代理類的方法生成一個 index,這個 index 可以作為參數直接定位要調用的方法。

常用場景包括權限認證、自動緩存、錯誤處理、日志、調試和事務等。

相關注解

@Aspect:聲明被注解的類是一個切面 Bean。

@Before:前置通知,指在某個連接點之前執行的通知。

@After:后置通知,指某個連接點退出時執行的通知(不論正常返回還是異常退出)。

@AfterReturning:返回后通知,指某連接點正常完成之后執行的通知,返回值使用returning屬性接收。

@AfterThrowing:異常通知,指方法拋出異常導致退出時執行的通知,和@AfterReturning只會有一個執行,異常使用throwing屬性接收。

相關術語

Aspect:切面,一個關注點的模塊化,這個關注點可能會橫切多個對象。

Joinpoint:連接點,程序執行過程中的某一行為,即業務層中的所有方法。。

Advice:通知,指切面對于某個連接點所產生的動作,包括前置通知、后置通知、返回后通知、異常通知和環繞通知。

Pointcut:切入點,指被攔截的連接點,切入點一定是連接點,但連接點不一定是切入點。

Proxy:代理,Spring AOP 中有 JDK 動態代理和 CGLib 代理,目標對象實現了接口時采用 JDK 動態代理,反之采用 CGLib 代理。

Target:代理的目標對象,指一個或多個切面所通知的對象。

Weaving :織入,指把增強應用到目標對象來創建代理對象的過程。

AOP 的過程

Spring AOP 是由 BeanPostProcessor 后置處理器開始的,這個后置處理器是一個監聽器,可以監聽容器觸發的 Bean 生命周期事件,向容器注冊后置處理器以后,容器中管理的 Bean 就具備了接收 IoC 容器回調事件的能力。BeanPostProcessor 的調用發生在 Spring IoC 容器完成 Bean 實例對象的創建和屬性的依賴注入之后,為 Bean 對象添加后置處理器的入口是 initializeBean 方法。

Spring 中 JDK 動態代理生通過 JdkDynamicAopProxy 調用 Proxy 的 newInstance 方法來生成代理類,JdkDynamicAopProxy 也實現了 InvocationHandler 接口,invoke 方法的具體邏輯是先獲取應用到此方法上的攔截器鏈,如果有攔截器則創建 MethodInvocation 并調用其 proceed 方法,否則直接反射調用目標方法。因此 Spring AOP 對目標對象的增強是通過攔截器實現的。


P6:Spring MVC 核心組件

DispatcherServlet:SpringMVC 中的前端控制器,是整個流程控制的核心,負責接收請求并轉發給對應的處理組件。

Handler:處理器,完成具體業務邏輯,相當于 Servlet 或 Action。

HandlerMapping:完成URL 到 Controller映射的組件,DispatcherServlet 接收到請求之后,通過 HandlerMapping 將不同的請求映射到不同的 Handler。

HandlerInterceptor:處理器攔截器,是一個接口,如果需要完成一些攔截處理,可以實現該接口。

HandlerExecutionChain:處理器執行鏈,包括兩部分內容:Handler 和 HandlerInterceptor。

HandlerAdapter:處理器適配器,Handler執行業務方法前需要進行一系列操作,包括表單數據驗證、數據類型轉換、將表單數據封裝到JavaBean等,這些操作都由 HandlerAdapter 完成。DispatcherServlet 通過 HandlerAdapter 來執行不同的 Handler。

ModelAndView:裝載了模型數據和視圖信息,作為 Handler 的處理結果返回給 DispatcherServlet。

ViewResolver:視圖解析器,DispatcherServlet 通過它將邏輯視圖解析為物理視圖,最終將渲染的結果響應給客戶端。


P7:Spring MVC 處理流程

Web 容器啟動時會通知 Spring 初始化容器,加載 Bean 的定義信息并初始化所有單例 Bean,然后遍歷容器中的 Bean,獲取每一個 Controller 中的所有方法訪問的 URL,將 URL 和對應的 Controller 保存到一個 Map 集合中。

所有的請求會轉發給 DispatcherServlet 前端處理器處理,DispatcherServlet 會請求 HandlerMapping 找出容器中被 @Controler 注解修飾的 Bean 以及被 @RequestMapping 修飾的方法和類,生成 Handler 和 HandlerInterceptor 并以一個 HandlerExcutionChain 處理器執行鏈的形式返回。

之后 DispatcherServlet 使用 Handler 找到對應的 HandlerApapter,通過 HandlerApapter 調用 Handler 的方法,將請求參數綁定到方法的形參上,執行方法處理請求并得到 ModelAndView。

最后 DispatcherServlet 根據使用 ViewResolver 試圖解析器對得到的 ModelAndView 邏輯視圖進行解析得到 View 物理視圖,然后對視圖渲染,將數據填充到視圖中并返回給客戶端。

注解

@Controller:在類定義處添加,將類交給IoC容器管理。

@RequtestMapping:將URL請求和業務方法映射起來,在類和方法定義上都可以添加該注解。value 屬性指定URL請求的實際地址,是默認值。method 屬性限制請求的方法類型,包括GET、POST、PUT、DELETE等。如果沒有使用指定的請求方法請求URL,會報405 Method Not Allowed 錯誤。params 屬性限制必須提供的參數,如果沒有會報錯。

@RequestParam:如果 Controller 方法的形參和 URL 參數名一致可以不添加注解,如果不一致可以使用該注解綁定。value 屬性表示HTTP請求中的參數名。required 屬性設置參數是否必要,默認false。defaultValue 屬性指定沒有給參數賦值時的默認值。

@PathVariable:Spring MVC 也支持 RESTful 風格的 URL,通過 @PathVariable 完成請求參數與形參的綁定。


P8:Spring Data JPA 框架

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 規范的基礎上封裝的一套 JPA 應用框架,可使開發者用極簡的代碼實現對數據庫的訪問和操作。它提供了包括增刪改查等在內的常用功能,且易于擴展,可以極大提高開發效率。

ORM 即 Object-Relational Mapping ,表示對象關系映射,映射的不只是對象的值還有對象之間的關系,通過 ORM 就可以把對象映射到關系型數據庫中。操作實體類就相當于操作數據庫表,可以不再重點關注 SQL 語句。

使用時只需要持久層接口繼承 JpaRepository 即可,泛型參數列表中第一個參數是實體類類型,第二個參數是主鍵類型。運行時通過 JdkDynamicAopProxy 的 invoke 方法創建了一個動態代理對象 SimpleJpaRepository,SimpleJpaRepository 中封裝了 JPA 的操作,通過 hibernate(封裝了JDBC)完成數據庫操作。

注解

@Entity:表明當前類是一個實體類。

@Table :關聯實體類和數據庫表。

@Column :關聯實體類屬性和數據庫表中字段。

@Id :聲明當前屬性為數據庫表主鍵對應的屬性。

@GeneratedValue: 配置主鍵生成策略。

@OneToMany :配置一對多關系,mappedBy 屬性值為主表實體類在從表實體類中對應的屬性名。

@ManyToOne :配置多對一關系,targetEntity 屬性值為主表對應實體類的字節碼。

@JoinColumn:配置外鍵關系,name 屬性值為外鍵名稱,referencedColumnName 屬性值為主表主鍵名稱。

對象導航查詢

通過 get 方法查詢一個對象的同時,通過此對象可以查詢它的關聯對象。

對象導航查詢一到多默認使用延遲加載的形式, 關聯對象是集合,因此使用立即加載可能浪費資源。

對象導航查詢多到一默認使用立即加載的形式, 關聯對象是一個對象,因此使用立即加載。

如果要改變加載方式,在實體類注解配置加上 fetch 屬性即可,LAZY 表示延遲加載,EAGER 表示立即加載。


P9:Mybatis 框架

Mybatis 是一個實現了數據持久化的 ORM 框架,簡單理解就是對 JDBC 進行了封裝。

優點

相比 JDBC 減少了大量代碼量,減少冗余代碼。

使用靈活,SQL 語句寫在 XML 里,從程序代碼中徹底分離,降低了耦合度,便于管理。

提供 XML 標簽,支持編寫動態 SQL 語句。

提供映射標簽,支持對象與數據庫的 ORM 字段映射關系。

缺點

SQL 語句編寫工作量較大,尤其是字段和關聯表多時。

SQL 語句依賴于數據庫,導致數據庫移植性差,不能隨意更換數據庫。

映射文件標簽

select、insert、update、delete 標簽分別對應查詢、添加、更新、刪除操作。

parameterType 屬性表示參數的數據類型,包括基本數據類型和對應的包裝類型、String 和 Java Bean 類型,當有多個參數時可以使用 #{argn} 的形式表示第 n 個參數。除了基本數據類型都要以全限定類名的形式指定參數類型。

resultType 表示返回的結果類型,包括基本數據類型和對應的包裝類型、String 和 Java Bean 類型。還可以使用把返回結果封裝為復雜類型的 resultMap 。

緩存

使用緩存可以減少程序和數據庫交互的次數,從而提高程序的運行效率。第一次查詢后會自動將結果保存到緩存中,下一次查詢時直接從緩存中返回結果無需再次查詢數據庫。

  • 一級緩存
    SqlSession 級別,默認開啟且不能關閉。
    操作數據庫時需要創建 SqlSession 對象,對象中有一個 HashMap 存儲緩存數據,不同 SqlSession 之間緩存數據區域互不影響。
    一級緩存的作用域是 SqlSession 范圍的,在同一個 SqlSession 中執行兩次相同的 SQL 語句時,第一次執行完畢會將結果保存在緩存中,第二次查詢直接從緩存中獲取。
    如果 SqlSession 執行了 DML 操作(insert、update、delete),Mybatis 必須將緩存清空以保證數據的有效性。
  • 二級緩存
    Mapper 級別,默認關閉。
    使用二級緩存時多個 SqlSession 使用同一個 Mapper 的 SQL 語句操作數據庫,得到的數據會存在二級緩存區,同樣使用 HashMap 進行數據存儲,相比于一級緩存,二級緩存范圍更大,多個 SqlSession 可以共用二級緩存,作用域是 Mapper 的同一個 namespace,不同 SqlSession 兩次執行相同的 namespace 下的 SQL 語句,參數也相等,則第一次執行成功后會將數據保存在二級緩存中,第二次可直接從二級緩存中取出數據。
    要使用二級緩存,先在在全局配置文件中配置: <!-- 開啟二級緩存 --><setting name="cacheEnabled" value="true"/>再在對應的映射文件中配置一個 cache 標簽即可。<cache/>

P10:Spring Cloud 框架

單體應用存在的問題

隨著業務發展,開發越來越復雜。

修改、新增某個功能,需要對整個系統進行測試、重新部署。

一個模塊出現問題,可能導致整個系統崩潰。

多個開發團隊同時對數據進行管理,容易產生安全漏洞。

各個模塊使用同一種技術開發,各個模塊很難根據實際情況選擇更合適的技術框架,局限性很大。

分布式和集群的區別

集群:一臺服務器無法負荷高并發的數據訪問量,就設置多臺服務器一起分擔壓力,是在物理層面解決問題。

分布式:將一個復雜的問題拆分成若干簡單的小問題,將一個大型的項目架構拆分成若干個微服務來協同完成,在軟件設計層面解決問題。

微服務的優點

各個服務的開發、測試、部署都相互獨立,用戶服務可以拆分為獨立服務,如果用戶量很大,可以很容易對其實現負載。

當新需求出現時,使用微服務不再需要考慮各方面的問題,例如兼容性、影響度等。

使用微服務拆分項目后,各個服務之間消除了很多限制,只需要保證對外提供的接口正常可用,而不限制語言和框架等選擇。

服務治理 Eureka

服務治理的核心由三部分組成:服務提供者、服務消費者、注冊中心。

服務注冊:在分布式系統架構中,每個微服務在啟動時,將自己的信息存儲在注冊中心。

服務發現:服務消費者從注冊中心獲取服務提供者的網絡信息,通過該信息調用服務。

Spring Cloud 的服務治理使用 Eureka 實現,Eureka 是 Netflix 開源的基于 REST 的服務治理解決方案,Spring Cloud 集成了 Eureka,提供服務注冊和服務發現的功能,可以和基于 Spring Boot 搭建的微服務應用輕松完成整合,將 Eureka 二次封裝為 Spring Cloud Eureka。Eureka Server 是注冊中心,所有要進行注冊的微服務通過 Eureka Client 連接到 Eureka Server 完成注冊。

服務網關 Zuul

Spring Cloud 集成了 Zuul 組件,實現服務網關。Zuul 是 Netflix 提供的一個開源的 API 網關服務器,是客戶端和網站后端所有請求的中間層,對外開放一個 API,將所有請求導入統一的入口,屏蔽了服務端的具體實現邏輯,可以實現方向代理功能,在網關內部實現動態路由、身份認證、IP過濾、數據監控等。

負載均衡 Ribbon

Spring Cloud Ribbon 是一個負載均衡的解決方案,Ribbon 是 Netflix 發布的均衡負載器,Spring Cloud Ribbon是基于 Netflix Ribbon 實現的,是一個用于對 HTTP 請求進行控制的負載均衡客戶端。

在注冊中心對 Ribbon 進行注冊之后,Ribbon 就可以基于某種負載均衡算法(輪循、隨機、加權輪詢、加權隨機等)自動幫助服務消費者調用接口,開發者也可以根據具體需求自定義 Ribbon 負載均衡算法。實際開發中 Spring Clooud Ribbon 需要結合 Spring Cloud Eureka 使用,Eureka 提供所有可以調用的服務提供者列表,Ribbon 基于特定的負載均衡算法從這些服務提供者中選擇要調用的具體實例。

聲明式接口調用 Feign

Feign 與 Ribbon 一樣也是 Netflix 提供的,Feign 是一個聲明式、模板化的 Web Service 客戶端,簡化了開發者編寫 Web 服務客戶端的操作,開發者可以通過簡單的接口和注解來調用 HTTP API,Spring Cloud Feign 整合了 Ribbon 和 Hystrix,具有可插拔、基于注解、負載均衡、服務熔斷等一系列功能。

相比于 Ribbon + RestTemplate 的方式,Feign 可以大大簡化代碼開發,支持多種注解,包括 Feign 注解、JAX-RS 注解、Spring MVC 注解等。RestTemplate 是 Spring 框架提供的基于 REST 的服務組件,底層是對 HTTP 請求及響應進行了封裝,提供了很多訪問 REST 服務的方法,可以簡化代碼開發。

服務熔斷 Hystrix

熔斷器的作用是在不改變各個微服務調用關系的前提下,針對錯誤情況進行預先處理。

設計原則:服務隔離機制、服務降級機制、熔斷機制、提供實時監控和報警功能和提供實時配置修改功能

Hystrix 數據監控需要結合 Spring Boot Actuator 使用,Actuator 提供了對服務的數據監控、數據統計,可以通過 hystirx-stream 節點獲取監控的請求數據,同時提供了可視化監控界面。

服務配置 Config

Spring Cloud Config 通過服務端可以為多個客戶端提供配置服務,既可以將配置文件存儲在本地,也可以將配置文件存儲在遠程的 Git 倉庫,創建 Config Server,通過它管理所有的配置文件。

服務跟蹤 Zipkin

Spring Cloud Zipkin 是一個可以采集并跟蹤分布式系統中請求數據的組件,讓開發者更直觀地監控到請求在各個微服務耗費的時間,Zipkin 包括兩部分 Zipkin Server 和 Zipkin Client。


MySQL 15

P1:邏輯架構

第一層是服務器層,主要提供連接處理、授權認證、安全等功能,該層的服務不是 MySQL 獨有的,大多數基于網絡的 C/S 服務都有類似架構。

第二層實現了 MySQL 核心服務功能,包括查詢解析、分析、優化、緩存以及日期和時間等所有內置函數,所有跨存儲引擎的功能都在這一層實現,例如存儲過程、觸發器、視圖等。

第三層是存儲引擎層,存儲引擎負責 MySQL 中數據的存儲和提取。服務器通過 API 與存儲引擎通信,這些接口屏蔽了不同存儲引擎的差異,使得差異對上層查詢過程透明。除了會解析外鍵定義的 InnoDB 外,存儲引擎不會解析 SQL,不同存儲引擎之間也不會相互通信,只是簡單響應上層服務器請求。


P2:鎖

當有多個查詢需要在同一時刻修改數據時就會產生并發控制的問題,MySQL 在兩個層面進行并發控制:服務器層與存儲引擎層。

讀寫鎖

在處理并發讀或寫時,可以通過實現一個由兩種類型組成的鎖系統來解決問題。這兩種類型的鎖通常被稱為共享鎖和排它鎖,也叫讀鎖和寫鎖。讀鎖是共享的,相互不阻塞,多個客戶在同一時刻可以同時讀取同一個資源而不相互干擾。寫鎖則是排他的,也就是說一個寫鎖會阻塞其他的寫鎖和讀鎖,確保在給定時間內只有一個用戶能執行寫入并防止其他用戶讀取正在寫入的同一資源。

在實際的數據庫系統中,每時每刻都在發生鎖定,當某個用戶在修改某一部分數據時,MySQL 會通過鎖定防止其他用戶讀取同一數據。寫鎖比讀鎖有更高的優先級,一個寫鎖請求可能會被插入到讀鎖隊列的前面,但是讀鎖不能插入到寫鎖前面。

鎖策略

一種提高共享資源并發性的方法就是讓鎖定對象更有選擇性,盡量只鎖定需要修改的部分數據而不是所有資源,更理想的方式是只對會修改的數據進行精確鎖定。任何時刻在給定的資源上,鎖定的數據量越少,系統的并發程度就越高,只要不發生沖突即可。

鎖策略就是在鎖的開銷和數據安全性之間尋求平衡,這種平衡也會影響性能。大多數商業數據庫系統沒有提供更多選擇,一般都是在表上加行鎖,而 MySQL 提供了多種選擇,每種MySQL存儲引擎都可以實現自己的鎖策略和鎖粒度。MySQL 最重要的兩種鎖策略:

  • 表鎖是MySQL中最基本的鎖策略,并且是開銷最小的策略。表鎖會鎖定整張表,一個用戶在對表進行寫操作前需要先獲得寫鎖,這會阻塞其他用戶對該表的所有讀寫操作。只有沒有寫鎖時,其他讀取的用戶才能獲取讀鎖,讀鎖之間不相互阻塞。
  • 行鎖可以最大程度地支持并發處理,同時也帶來了最大的鎖開銷。InnoDB 和 XtraDB 以及一些其他存儲引擎實現了行鎖。行鎖只在存儲引擎層實現,而服務器層沒有實現。

死鎖

死鎖是指兩個或者多個事務在同一資源上相互占用并請求鎖定對方占用的資源,從而導致惡性循環的現象。當多個事務試圖以不同順序鎖定資源時就可能會產生死鎖,多個事務同時鎖定同一個資源時也會產生死鎖。

為了解決死鎖問題,數據庫系統實現了各種死鎖檢測和死鎖超時機制。越復雜的系統,例如InnoDB 存儲引擎,越能檢測到死鎖的循環依賴,并立即返回一個錯誤。這種解決方式很有效,否則死鎖會導致出現非常慢的查詢。還有一種解決方法,就是當查詢的時間達到鎖等待超時的設定后放棄鎖請求,這種方式通常來說不太好。InnoDB 目前處理死鎖的方法是將持有最少行級排它鎖的事務進行回滾。

鎖的行為與順序是和存儲引擎相關的,以同樣的順序執行語句,有些存儲引擎會產生死鎖有些則不會。死鎖的產生有雙重原因:有些是真正的數據沖突,這種情況很難避免,有些則完全是由于存儲引擎的實現方式導致的。

死鎖發生之后,只有部分或者完全回滾其中一個事務,才能打破死鎖。對于事務型系統這是無法避免的,所以應用程序在設計時必須考慮如何處理死鎖。大多數情況下只需要重新執行因死鎖回滾的事務即可。


P3:事務

事務就是一組原子性的 SQL 查詢,或者說一個獨立的工作單元。如果數據庫引擎能夠成功地對數據庫應用該組查詢的全部語句,那么就執行該組查詢。如果其中有任何一條語句因為崩潰或其他原因無法執行,那么所有的語句都不會執行。也就是說事務內的語句要么全部執行成功,要么全部執行失敗。

ACID 特性

一個運行良好的事務處理系統必須具備 ACID 特性,實現了 ACID 的數據庫需要更強的CPU處理能力、更大的內存和磁盤空間。

  • 原子性 atomicity
    一個事務在邏輯上是必須不可分割的最小工作單元,整個事務中的所有操作要么全部提交成功,要么全部失敗回滾,對于一個事務來說不可能只執行其中的一部分。
  • 一致性 consistency
    數據庫總是從一個一致性的狀態轉換到另一個一致性的狀態。
  • 隔離性 isolation
    針對并發事務而言,隔離性就是要隔離并發運行的多個事務之間的相互影響,一般來說一個事務所做的修改在最終提交以前,對其他事務是不可見的。
  • 持久性 durability
    一旦事務提交成功,其修改就會永久保存到數據庫中,此時即使系統崩潰,修改的數據也不會丟失。

隔離級別

在 SQL 標準中定義了四種隔離級別,每一種隔離級別都規定了一個事務中所做的修改,哪些在事務內和事務間是可見的,哪些是不可見的。較低級別的隔離通常可以執行更高的并發,系統的開銷也更低。

  • 未提交讀 READ UNCOMMITTED
    在該級別事務中的修改即使沒有被提交,對其他事務也是可見的。事務可以讀取其他事務修改完但未提交的數據,這種問題稱為臟讀。這個級別還會導致不可重復讀和幻讀,從性能上說也沒有比其他級別好很多,因此很少使用。
  • 提交讀 READ COMMITTED
    大多數數據庫系統默認的隔離級別就是提交讀,但 MySQL 不是。提交讀滿足了隔離性的簡單定義:一個事務開始時只能"看見"已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前的任何修改對其他事務都是不可見的。這個級別有時也叫不可重復讀,因為兩次執行同樣的查詢可能會得到不同結果。提交讀存在不可重復讀和幻讀的問題。
  • 可重復讀 REPEATABLE READ(MySQL默認的隔離級別)
    可重復讀解決了不可重復讀的問題,該級別保證了在同一個事務中多次讀取同樣的記錄結果是一致的。但可重復讀隔離級別還是無法解決幻讀的問題,所謂幻讀,指的是當某個事務在讀取某個范圍內的記錄時,會產生幻行。InnoDB 存儲引擎通過多版本并發控制MVCC 解決幻讀的問題。
  • 可串行化 SERIALIZABLE
    該級別是最高的隔離級別,通過強制事務串行執行,避免了幻讀的問題。可串行化會在讀取的每一行數據上都加鎖,可能導致大量的超時和鎖爭用的問題。實際應用中很少用到這個隔離級別,只有非常需要確保數據一致性且可以接受沒有并發的情況下才考慮該級別。

MySQL 中的事務

MySQL 提供了兩種事務型的存儲引擎:InnoDB 和 NDB Cluster。

MySQL 事務默認采用自動提交模式,如果不是顯式地開始一個事務,則每個查詢都將被當作一個事務執行提交操作。在當前連接中,可以通過設置 AUTOCOMMIT 變量來啟用或禁用自動提交模式。

1 或 ON 表示啟用,0 或 OFF 表示禁用,當禁用自動提交時,所有的查詢都是在一個事務中,直到顯式地執行 COMMIT 或 ROLLBACK 后該事務才會結束,同時又開始了一個新事務。修改 AUTOCOMMIT 對非事務型表,例如 MyISAM 或內存表不會有任何影響,對這類表來說沒有 COMMIT 或 ROLLBACK 的概念,也可以理解為一直處于啟用自動提交的模式

有一些命令在執行之前會強制執行提交當前的活動事務,例如ALTER TABLE和LOCK TABLES等。

MySQL能夠識別所有的 4個 ANSI 隔離級別,InnoDB 引擎也支持所有隔離級別。


P4:MVCC 多版本并發控制

可以認為 MVCC 是行級鎖的一個變種,但它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現機制有所不同,但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。

MVCC 的實現,是通過保存數據在某個時間點的快照來實現的。也就是說不管需要執行多長時間,每個事務看到的數據都是一致的。根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的數據可能是不一樣的。

不同的存儲引擎的 MVCC 實現是不同的,典型的有樂觀并發控制和悲觀并發控制。

InnoDB 的 MVCC 實現

InnoDB 的MVCC 通過在每行記錄后面保存兩個隱藏的列來實現,這兩個列一個保存了行的創建時間,一個保存行的過期時間間。不過存儲的不是實際的時間值而是系統版本號,每開始一個新的事務系統版本號都會自動遞增,事務開始時刻的系統版本號會作為事務的版本號,用來和查詢到的每行記錄的版本號進行比較。

REPEATABLE READ 級別下 MVCC 的具體實現

SELECT:InnoDB 會根據以下兩個條件檢查每行記錄:

  • 只查找版本早于當前事務版本的數據行,可以確保事務讀取的行要么是事務開始前已經存在的,要么是事物自身插入或修改過的。
  • 行的刪除版本要么未定義,要么大于當前事務版本號,可以確保事務讀取到的行在事務開始前未被刪除。

INSERT :為新插入的每一行保存當前系統版本號作為行版本號。

DELETE:為刪除的每一行保存當前系統版本號作為行刪除標識。

UPDATE:為插入的每一行新記錄保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識。

保存這兩個額外系統版本號使大多數讀操作都可以不用加鎖。這樣設計使讀數據操作簡單且高效,并且能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外存儲空間,需要做更多行檢查工作以及一些額外維護工作。

MVCC 只能在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作,因為 READ UNCOMMITTED 總是讀取最新的數據行,而不是符合當前事務版本的數據行,而 SERIALIZABLE 則會對所有讀取的行都加鎖。


P5:InnoDB 存儲引擎

InnoDB 是 MySQL 的默認事務型引擎,它被設計用來處理大量短期事務。InnoDB 的性能和自動崩潰恢復特性使得它在非事務型存儲需求中也很流行,除非有特別原因否則應該優先考慮 InnoDB 引擎。

InnoDB 的數據存儲在表空間中,表空間由一系列數據文件組成。MySQL4.1 后 InnoDB 可以將每個表的數據和索引放在單獨的文件中。

InnoDB 采用 MVCC 來支持高并發,并且實現了四個標準的隔離級別。其默認級別是 REPEATABLE READ,并且通過間隙鎖策略防止幻讀,間隙鎖使 InnoDB 不僅僅鎖定查詢涉及的行,還會對索引中的間隙進行鎖定防止幻行的插入。

InnoDB 表是基于聚簇索引建立的,InnoDB 的索引結構和其他存儲引擎有很大不同,聚簇索引對主鍵查詢有很高的性能,不過它的二級索引中必須包含主鍵列,所以如果主鍵很大的話其他所有索引都會很大,因此如果表上索引較多的話主鍵應當盡可能小。

InnoDB 的存儲格式是平臺獨立的,可以將數據和索引文件從一個平臺復制到另一個平臺。

InnoDB 內部做了很多優化,包括從磁盤讀取數據時采用的可預測性預讀,能夠自動在內存中創建加速讀操作的自適應哈希索引,以及能夠加速插入操作的插入緩沖區等。

選擇合適的存儲引擎

MySQL5.5 將 InnoDB 作為默認存儲引擎,除非需要用到某些 InnoDB 不具備的特性,并且沒有其他方法可以代替,否則都應該優先選用InnoDB。

如果應用需要事務支持,那么 InnoDB 是目前最穩定并且經過驗證的選擇。如果不需要事務并且主要是 SELECT 和 INSERT 操作,那么MyISAM 是不錯的選擇。相對而言,MyISAM 崩潰后發生損壞的概率要比 InnoDB 大很多而且恢復速度也要慢,因此即使不需要事務支持,也可以選擇InnoDB。

如果可以定期地關閉服務器來執行備份,那么備份的因素可以忽略。反之如果需要在線熱備份,那么 InnoDB 就是基本的要求。


P6:MyISAM 存儲引擎

在 MySQL5.1及之前,MyISAM 是默認的存儲引擎,MyISAM 提供了大量的特性,包括全文索引、壓縮、空間函數等,但不支持事務和行鎖,最大的缺陷就是崩潰后無法安全恢復。對于只讀的數據或者表比較小、可以忍受修復操作的情況仍然可以使用 MyISAM。

MyISAM 將表存儲在數據文件和索引文件中,分別以 .MYD 和 .MYI 作為擴展名。MyISAM 表可以包含動態或者靜態行,MySQL 會根據表的定義決定行格式。MyISAM 表可以存儲的行記錄數一般受限于可用磁盤空間或者操作系統中單個文件的最大尺寸。

MyISAM 對整張表進行加鎖,讀取時會對需要讀到的所有表加共享鎖,寫入時則對表加排它鎖。但是在表有讀取查詢的同時,也支持并發往表中插入新的記錄。

對于MyISAM 表,MySQL 可以手動或自動執行檢查和修復操作,這里的修復和事務恢復以及崩潰恢復的概念不同。執行表的修復可能導致一些數據丟失,而且修復操作很慢。

對于 MyISAM 表,即使是 BLOB 和 TEXT 等長字段,也可以基于其前 500 個字符創建索引。MyISAM 也支持全文索引,這是一種基于分詞創建的索引,可以支持復雜的查詢。

創建 MyISAM 表時如果指定了 DELAY_KEY_WRITE 選項,在每次修改執行完成時不會立刻將修改的索引數據寫入磁盤,而是會寫到內存中的鍵緩沖區,只有在清理緩沖區或關閉表的時候才會將對應的索引庫寫入磁盤。這種方式可以極大提升寫性能,但在數據庫或主機崩潰時會造成索引損壞,需要執行修復。延遲更新索引鍵的特性可以在全局設置也可以單個表設置。

MyISAM 設計簡單,數據以緊密格式存儲,所以在某些場景下性能很好。MyISAM 最典型的性能問題還是表鎖問題,如果所有的查詢長期處于 Locked 狀態,那么原因毫無疑問就是表鎖。


P7:Memory 存儲引擎

如果需要快速訪問數據,并且這些數據不會被修改,重啟以后丟失也沒有關系,那么使用 Memory 表是非常有用的。Memory 表至少要比 MyISAM 表快一個數量級,因為所有的數據都保存在內存中,不需要進行磁盤 IO,Memory 表的結構在重啟以后還會保留,但數據會丟失。

Memory 表適合的場景:查找或者映射表、緩存周期性聚合數據的結果、保存數據分析中產生的中間數據。

Memory 表支持哈希索引,因此查找速度極快。雖然速度很快但還是無法取代傳統的基于磁盤的表,Memory 表使用表級鎖,因此并發寫入的性能較低。它不支持 BLOB 和 TEXT 類型的列,并且每行的長度是固定的,所以即使指定了 VARCHAR 列,實際存儲時也會轉換成CHAR,這可能導致部分內存的浪費。

如果 MySQL 在執行查詢的過程中需要使用臨時表來保持中間結果,內部使用的臨時表就是 Memory 表。如果中間結果太大超出了Memory 表的限制,或者含有 BLOB 或 TEXT 字段,臨時表會轉換成 MyISAM 表。


P8:數據類型

整數類型

如果存儲整數可以使用這幾種整數類型:TINYINT、SMALLINT、MEDIUMINT、INT,BIGINT,它們分別使用8、16、24、32、64 位存儲空間。

整數類型有可選的 UNSIGNED 屬性,表示不允許負值,可以使整數的上限提高一倍。有符號和無符號類型使用相同的存儲空間并具有相同的性能,可以根據實際情況選擇合適的類型。

MySQL 可以為整數類型指定寬度,例如 INT(11),這對大多數應用沒有意義,不會限制值的范圍,只是規定了 MySQL 的交互工具顯示字符的個數,對于存儲和計算來說 INT(1) 和 INT(11) 是相同的。

實數類型

實數是帶有小數部分的數字,但它們不只是為了存儲小數,也可以使用 DECIMAL 存儲比 BIGINT 還大的整數。MySQL既支持精確類型,也支持不精確類型。

FLOAT 和 DOUBLE 支持使用標準的浮點運算進行近似運算,DECIMAL 用于存儲精確的小數。

浮點類型在存儲同樣范圍的值時,通常比 DECIMAL 使用更少的空間。FLOAT 使用 4 字節存儲,DOUBLE 占用8字節,MySQL 內部使用DOUBLE 作為內部浮點計算的類型。

因為需要額外空間和計算開銷,所以應當盡量只在對小數進行精確計算時才使用 DECIMAL。在數據量較大時可以考慮 BIGINT 代替DECIMAL,將需要存儲的貨幣單位根據小數的位數乘以相應的倍數即可。假設要存儲的數據精確到萬分之一分,則可以把所有金額乘以一百萬將結果存儲在 BIGINT 中,這樣可以同時避免浮點存儲計算不精確和 DECIMAL 精確計算代價高的問題。

VARCHAR

VARCHAR 用于存儲可變字符串,是最常見的字符串數據類型。它比定長字符串更節省空間,因為它僅使用必要的空間。VARCHAR 需要 1或 2 個額外字節記錄字符串長度,如果列的最大長度不大于 255 字節則只需要1 字節。VARCHAR 不會刪除末尾空格。

VARCHAR 節省了存儲空間,但由于行是變長的,在 UPDATE 時可能使行變得比原來更長,這就導致需要做額外的工作。如果一個行占用的空間增長并且頁內沒有更多的空間可以存儲,這種情況下不同存儲引擎處理不同,InnoDB 會分裂頁而 MyISAM 會將行拆分成不同片。

適用場景:字符串列的最大長度比平均長度大很多、列的更新很少、使用了 UTF8 這種復雜字符集,每個字符都使用不同的字節數存儲。

InnoDB 可以把過長的 VARCHAR 存儲為 BLOB。

CHAR

CHAR 是定長的,根據定義的字符串長度分配足夠的空間。CHAR 會刪除末尾空格。

CHAR 適合存儲很短的字符串,或所有值都接近同一個長度,例如存儲密碼的 MD5 值。對于經常變更的數據,CHAR 也比 VARCHAR更好,因為定長的 CHAR 不容易產生碎片。對于非常短的列,CHAR 在存儲空間上也更有效率,例如用 CHAR 來存儲只有 Y 和 N 的值只需要一個字節,但是 VARCHAR 需要兩個字節,因為還有一個記錄長度的額外字節。

BLOB 和 TEXT 類型

BLOB 和 TEXT 都是為了存儲大數據而設計的字符串數據類型,分別采用二進制和字符串方式存儲。MySQL會把每個 BLOB 和 TEXT 值當作一個獨立的對象處理,存儲引擎在存儲時通常會做特殊處理。當值太大時,InnoDB 會使用專門的外部存儲區來進行存儲。BLOB 和TEXT 僅有的不同是 BLOB 存儲的是二進制數據,沒有排序規則或字符集,而 TEXT 有字符集和排序規則。

MySQL 對 BLOB 和TEXT 列進行排序與其他類型不同:它只對每個列最前 max_sort_length 字節而不是整個字符串做排序,如果只需要排序前面一小部分字符,則可以減小 max_sort_length 的配置。MySQL 不能將 BLOB 和 TEXT 列全部長度的字符串進行索引,也不能使用這些索引消除排序。

DATETIME

能保存大范圍的值,從 1001 年到 9999 年,精度為秒。它把日期和時間封裝到了一個整數中,與時區無關,使用 8 字節的存儲空間。

TIMESTAMP

和 UNIX 時間戳相同,只使用 4 字節的存儲空間,因此范圍比 DATETIME 小得多,只能表示 1970 年到 2038 年,并且依賴于時區。


P9:索引的分類

索引在也叫做鍵,是存儲引擎用于快速找到記錄的一種數據結構。索引對于良好的性能很關鍵,尤其是當表中數據量越來越大時,索引對性能的影響愈發重要。在數據量較小且負載較低時,不恰當的索引對性能的影響可能還不明顯,但數據量逐漸增大時,性能會急劇下降。

索引大大減少了服務器需要掃描的數據量、可以幫助服務器避免排序和臨時表、可以將隨機 IO 變成順序 IO。但索引并不總是最好的工具,對于非常小的表,大部分情況下會采用全表掃描。對于中到大型的表,索引就非常有效。但對于特大型的表,建立和使用索引的代價也隨之增長,這種情況下應該使用分區技術。

在MySQL中,首先在索引中找到對應的值,然后根據匹配的索引記錄找到對應的數據行。索引可以包括一個或多個列的值,如果索引包含多個列,那么列的順序也十分重要,因為 MySQL 只能高效地使用索引的最左前綴列。

B-Tree 索引

大多數 MySQL 引擎都支持這種索引,不過底層的存儲引擎可能使用不同的存儲結構,例如 NDB 集群實際使用 T-Tree,而 InnoDB 則使用 B+Tree。

存儲引擎以不同方式使用 B-Tree 索引,性能也不同。例如 MyISAM 使用前綴壓縮技術使得索引更小,但 InnoDB 則按照原數據格式進行存儲。再例如 MyISAM 索引通過數據的物理位置引用被索引的行,而 InnoDB 則根據主鍵引用被索引的行。

B-Tree 通常意味著所有的值都是按順序存儲的,并且每個葉子頁到根的距離相同。B-Tree 索引能夠加快訪問數據的速度,因為存儲引擎不再需要進行全表掃描來獲取需要的數據,取而代之的是從索引的根節點開始進行搜索。根節點的槽中存放了指向子節點的指針,存儲引擎根據這些指針向下層查找。通過比較節點頁的值和要查找的值可以找到合適的指針進入下層子節點,這些指針實際上定義了子節點頁中值的上限和下限。最終存儲引擎要么找到對應的值,要么該記錄不存在。葉子節點的指針指向的是被索引的數據,而不是其他的節點頁。

B-Tree索引適用于全鍵值、鍵值范圍或鍵前綴查找,其中鍵前綴查找只適用于最左前綴查找。索引對如下類型的查詢有效:

  • 全值匹配:全值匹配指的是和索引中的所有列進行匹配。
  • 匹配最左前綴:只使用索引的第一列。
  • 匹配列前綴:只匹配某一列的值的開頭部分。
  • 匹配范圍值:查找某兩個值之間的范圍。
  • 精確匹配某一列并范圍匹配另一列:有一列全匹配而另一列范圍匹配。
  • 只訪問索引的查詢:B-Tree 通常可以支持只訪問索引的查詢,即查詢只需要訪問索引而無需訪問數據行。

因為索引樹中的節點有序,所以除了按值查找之外索引還可以用于查詢中的 ORDER BY 操作。一般如果 B-Tree 可以按照某種方式查找到值,那么也可以按照這種方式排序。

B-Tree索引的限制:

  • 如果不是按照索引的最左列開始查找,則無法使用索引。
  • 不能跳過索引中的列,例如索引為 (id,name,sex),不能只使用 id 和 sex 而跳過 name。
  • 如果查詢中有某個列的范圍查詢,則其右邊的所有列都無法使用索引。

B-Tree 和 B+Tree 的區別:

B-Tree 中每個節點同時存儲 key 和 data,而 B+Tree 中只有葉子節點才存儲 data,非葉子節點只存儲 key。InnoDB 對 B+Tree 進行了優化,在每個葉子節點上增加了一個指向相鄰葉子節點的鏈表指針,形成了帶有順序指針的 B+Tree,提高區間訪問的性能。

B+Tree 的優點在于:① 由于 B+Tree 在非葉子節點上不含數據信息,因此在內存頁中能夠存放更多的 key,數據存放得更加緊密,具有更好的空間利用率,訪問葉子節點上關聯的數據也具有更好的緩存命中率。② B+Tree 的葉子結點都是相連的,因此對整棵樹的遍歷只需要一次線性遍歷葉子節點即可。而 B-Tree 則需要進行每一層的遞歸遍歷,相鄰的元素可能在內存中不相鄰,所以緩存命中性沒有 B+Tree 好。但是 B-Tree 也有優點,由于每一個節點都包含 key 和 value,因此經常訪問的元素可能離根節點更近,訪問也更迅速。


哈希索引

哈希索引基于哈希表實現,只有精確匹配索引所有列的查詢才有效。對于每一行數據,存儲引擎都會對所有的索引列計算一個哈希碼,哈希碼是一個較小的值,并且不同鍵值的行計算出的哈希碼也不一樣。哈希索引將所有的哈希碼存儲在索引中,同時在哈希表中保存指向每個數據行的指針。

只有 Memory 引擎顯式支持哈希索引,這也是 Memory 引擎的默認索引類型。

因為索引自身只需存儲對應的哈希值,所以索引的結構十分緊湊,這讓哈希索引的速度非常快,但它也有一些限制:

  • 哈希索引只包含哈希值和行指針而不存儲字段值,所以不能使用索引中的值來避免讀取行。
  • 哈希索引數據并不是按照索引值順序存儲的,因此無法用于排序。
  • 哈希索引不支持部分索引列匹配查找,因為哈希索引始終是使用索引列的全部內容來計算哈希值的。例如在數據列(a,b)上建立哈希索引,如果查詢的列只有a就無法使用該索引。
  • 哈希索引只支持等值比較查詢,不支持任何范圍查詢。
  • 訪問哈希索引的數據非常快,除非有很多哈希沖突。當出現哈希沖突時,存儲引擎必須遍歷鏈表中所有的行指針,逐行進行比較直到找到所有符合條件的行。
  • 如果哈希沖突很高的話,索引維護的代價也會很高。

自適應哈希索引是 InnoDB 引擎的一個特殊功能,當它注意到某些索引值被使用的非常頻繁時,會在內存中基于 B-Tree 索引之上再創鍵一個哈希索引,這樣就讓 B-Tree 索引也具有哈希索引的一些優點,比如快速哈希查找。這是一個完全自動的內部行為,用戶無法控制或配置,但如果有必要可以關閉該功能。

如果存儲引擎不支持哈希索引,可以創建自定義哈希索引,在 B-Tree基礎 上創建一個偽哈希索引,它使用哈希值而不是鍵本身進行索引查找,需要在查詢的 WHERE 子句中手動指定哈希函數。當數據表非常大時,CRC32 會出現大量的哈希沖突,可以考慮自己實現 64 位哈希函數,或者使用 MD5 函數返回值的一部分作為自定義哈希函數。


空間索引

MyISAM 表支持空間索引,可以用作地理數據存儲。和 B-Tree 索引不同,這類索引無需前綴查詢。空間索引會從所有維度來索引數據,查詢時可以有效地使用任意維度來組合查詢。必須使用 MySQL 的 GIS 即地理信息系統的相關函數來維護數據,但 MySQL 對 GIS 的支持并不完善,因此大部分人都不會使用這個特性。


全文索引

通過數值比較、范圍過濾等就可以完成絕大多數需要的查詢,但如果希望通過關鍵字的匹配進行查詢過濾,那么就需要基于相似度的查詢,而不是精確的數值比較,全文索引就是為這種場景設計的。全文索引有自己獨特的語法,沒有索引也可以工作,如果有索引效率會更高。

全文索引可以支持各種字符內容的搜索,包括 CHAR、VARCHAR 和 TEXT 類型,也支持自然語言搜索和布爾搜索。在 MySQL 中全文索引有很多限制,例如表鎖對性能的影響、數據文件的崩潰恢復等,這使得 MyISAM 的全文索引對很多應用場景并不合適。MyISAM 的全文索引作用對象是一個"全文集合",可能是某個數據表的一列,也可能是多個列。具體的對某一條記錄,MySQL 會將需要索引的列全部拼接成一個字符串然后進行索引。

MyISAM 的全文索引是一種特殊的 B-Tree 索引,一共有兩層。第一層是所有關鍵字,然后對于每一個關鍵字的第二層,包含的是一組相關的"文檔指針"。全文索引不會索引文檔對象中的所有詞語,它會根據規則過濾掉一些詞語,例如停用詞列表中的詞都不會被索引。


聚簇索引

聚簇索引不是一種索引類型,而是一種數據存儲方式。InnoDB 的聚簇索引實際上在同一個結構中保存了 B-Tree 索引和數據行。當表有聚餐索引時,它的行數據實際上存放在索引的葉子頁中,因為無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。

優點:① 可以把相關數據保存在一起,例如實現電子郵箱時可以根據用戶 ID 聚集數據,這樣只需要從磁盤讀取少數數據頁就能獲取某個用戶的全部郵件,如果沒有使用聚簇索引,每封郵件可能都導致一次磁盤 IO。② 數據訪問更快,聚簇索引將索引和數據保存在同一個 B-Tree 中,因此獲取數據比非聚簇索引要更快。③ 使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值。

缺點:① 聚簇索引最大限度提高了 IO 密集型應用的性能,如果數據全部在內存中將會失去優勢。② 插入速度驗證依賴于插入順序,按照主鍵的順序插入是加載數據到 InnoDB 引擎最快的方式。③ 更新聚簇索引列的代價很高,因為會強制每個被更新的行移動到新位置。④ 基于聚簇索引的表插入新行或主鍵被更新導致行移動時,可能導致頁分裂,表會占用更多磁盤空間。④ 當行稀疏或由于頁分裂導致數據存儲不連續時,全表掃描可能很慢。


覆蓋索引

覆蓋索引指一個索引包含或覆蓋了所有需要查詢的字段的值,不再需要根據索引回表查詢數據。覆蓋索引必須要存儲索引列的值,因此 MySQL 只能使用 B-Tree 索引做覆蓋索引。

優點:① 索引條目通常遠小于數據行大小,可以極大減少數據訪問量。② 因為索引按照列值順序存儲,所以對于 IO 密集型防偽查詢回避隨機從磁盤讀取每一行數據的 IO 少得多。③ 由于 InnoDB 使用聚簇索引,覆蓋索引對 InnoDB 很有幫助。InnoDB 的二級索引在葉子節點保存了行的主鍵值,如果二級主鍵能覆蓋查詢那么可以避免對主鍵索引的二次查詢。


P10:索引使用原則

建立索引

對查詢頻次較高,且數據量比較大的表建立索引。索引字段的選擇,最佳候選列應當從 WHERE 子句的條件中提取,如果 WHERE 子句中的組合比較多,那么應當挑選最常用、過濾效果最好的列的組合。業務上具有唯一特性的字段,即使是多個字段的組合,也必須建成唯一索引。

使用前綴索引

索引列開始的部分字符,索引創建后也是使用硬盤來存儲的,因此短索引可以提升索引訪問的 IO 效率。對于 BLOB、TEXT 或很長的 VARCHAR 列必須使用前綴索引,MySQL 不允許索引這些列的完整長度。前綴索引是一種能使索引更小更快的有效方法,但缺點是 MySQL 無法使用前綴索引做 ORDER BY 和 GROUP BY,也無法使用前綴索引做覆蓋掃描。

選擇合適的索引順序

當不需要考慮排序和分組時,將選擇性最高的列放在前面。索引的選擇性是指不重復的索引值和數據表的記錄總數之比,索引的選擇性越高則查詢效率越高,唯一索引的選擇性是 1,因此也可以使用唯一索引提升查詢效率。

刪除無用索引

MySQL 允許在相同列上創建多個索引,重復的索引需要單獨維護,并且優化器在優化查詢時也需要逐個考慮,這會影響性能。重復索引是指在相同的列上按照相同的順序創建的相同類型的索引,應該避免創建重復索引。如果創建了索引 (A,B) 再創建索引 (A) 就是冗余索引,因為這只是前一個索引的前綴索引,對于 B-Tree 索引來說是冗余的。解決重復索引和冗余索引的方法就是刪除這些索引。除了重復索引和冗余索引,可能還會有一些服務器永遠不用的索引,也應該考慮刪除。

減少碎片

B-Tree 索引可能會碎片化,碎片化的索引可能會以很差或無序的方式存儲在磁盤上,這會降低查詢的效率。表的數據存儲也可能碎片化,包括行碎片、行間碎片、剩余空間碎片,對于 MyISAM 這三類碎片化都有可能發生,對于 InnoDB 不會出現短小的行碎片,它會移動短小的行重寫到一個片段中。可以通過執行 OPTIMIZE TABLE 或者導出再導入的方式重新整理數據,對于 MyISAM 可以通過排序重建索引消除碎片。InnoDB 可以通過先刪除再重新創建索引的方式消除索引碎片。

索引失效情況

如果索引列出現了隱式類型轉換,則 MySQL 不會使用索引。常見的情況是在 SQL 的 WHERE 條件中字段類型為字符串,其值為數值,如果沒有加引號那么 MySQL 不會使用索引。

如果 WHERE 條件中含有 OR,除非 OR 前使用了索引列而 OR 之后是非索引列,索引會失效。

MySQL 不能在索引中執行 LIKE 操作,這是底層存儲引擎 API 的限制,最左匹配的 LIKE 比較會被轉換為簡單的比較操作,但如果是以通配符開頭的 LIKE 查詢,存儲引擎就無法做筆記。這種情況下 MySQL 服務器只能提取數據行的值而不是索引值來做比較。

如果查詢中的列不是獨立的,則 MySQL 不會使用索引。獨立的列是指索引列不能是表達式的一部分,也不能是函數的參數。

對于多個范圍條件查詢,MySQL 無法使用第一個范圍列后面的其他索引列,對于多個等值查詢則沒有這種限制。

如果 MySQL 判斷全表掃描比使用索引查詢更快,則不會使用索引。

頁面搜索嚴禁左模糊或者全模糊,如果需要請走搜索引擎來解決。索引文件具有 B-Tree 的最左前綴匹配特性,如果左邊的值未確定,那么無法使用此索引


P11:優化數據類型

更小的通常更好

一般情況下盡量使用可以正確存儲數據的最小數據類型,更小的數據類型通常也更快,因為它們占用更少的磁盤、內存和 CPU 緩存。

盡可能簡單

簡單數據類型的操作通常需要更少的 CPU 周期,例如整數比字符操作代價更低,因為字符集和校對規則使字符相比整形更復雜。應該使用 MySQL 的內建類型 date、time 和 datetime 而不是字符串來存儲日期和時間,另一點是應該使用整形存儲 IP 地址。

盡量避免 NULL

通常情況下最好指定列為 NOT NULL,除非需要存儲 NULL值。因為如果查詢中包含可為 NULL 的列對 MySQL 來說更難優化,可為 NULL 的列使索引、索引統計和值比較都更復雜,并且會使用更多存儲空間。當可為 NULL 的列被索引時,每個索引記錄需要一個額外字節,在MyISAM 中還可能導致固定大小的索引變成可變大小的索引。

通常把可為 NULL 的列設置為 NOT NULL 帶來的性能提升較小,因此調優時沒必要首先查找并修改這種情況。但如果計劃在列上建索引,就應該盡量避免設計成可為 NULL 的列。

在為列選擇數據類型時,第一步需要確定合適的大類型:數字、字符串、時間等。下一步是選擇具體類型,很多 MySQL 數據類型可以存儲相同類型的數據,只是存儲的長度和范圍不一樣,允許的精度不同或需要的物理空間不同。


P12:優化查詢概述

優化數據訪問

如果把查詢看作一個任務,那么它由一系列子任務組成,每個子任務都會消耗一定時間。如果要優化查詢,要么消除一些子任務,要么減少子任務的執行次數。查詢性能低下最基本的原因是訪問的數據太多,大部分性能低下的查詢都可以通過減少訪問的數據量進行優化。可以通過以下兩個步驟分析。

是否向數據庫請求了不需要的數據:有些查詢會請求超過實際需要的數據,然后這些多余的數據會被應用程序丟棄,這會給 MySQL 服務器造成額外負擔并增加網絡開銷,另外也會消耗應用服務器的 CPU 和內存資源。例如多表關聯時返回全部列,取出全部列會讓優化器無法完成索引覆蓋掃描這類優化,還會為服務器帶來額外的 IO、內存和 CPU 的消耗,因此使用 SELECT * 時需要仔細考慮是否真的需要返回全部列。再例如總是重復查詢相同的數據,比較好的解決方案是初次查詢時將數據緩存起來,需要的時候從緩存中取出。

MySQL 是否在掃描額外的記錄:在確定查詢只返回需要的數據后,應該看看查詢為了返回結果是否掃描了過多的數據,最簡單的三個衡量指標時響應時間、掃描的行數和返回的行數。如果發現查詢需要掃描大量數據但只返回少數的行,可以使用以下手動優化:① 使用覆蓋索引掃描,把所有需要用的列都放到索引中,這樣存儲引擎無需回表查詢對應行就可以返回結果。② 改變庫表結構。 ③ 重寫這個復雜的查詢,讓 MySQL 優化器能夠以更優化的方式執行這個查詢。

重構查詢方式

在優化有問題的查詢時,目標應該是找到一個更優的方法獲取實際需要的結果,而不一定總是需要從 MySQL 獲取一模一樣的結果集。

切分查詢:有時候對于一個大查詢可以將其切分成小查詢,每個查詢功能完全一樣,只完成一小部分,每次只返回一小部分查詢結果。例如刪除舊數據,定期清除大量數據時,如果用一個大的語句一次性完成的話可能需要一次鎖住很多數據、占滿整個事務日志、耗盡系統資源、阻塞很多小的但重要的查詢。將一個大的 DELETE 語句切分成多個較小的查詢可以盡可能小地影響 MySQL 的性能,同時還可以減少MySQL 復制的延遲。

分解關聯查詢:很多高性能應用都會對關聯查詢進行分解,可以對每一個表進行單表查詢,然后將結果在應用程序中進行關聯。分解關聯查詢可以讓緩存的效率更高、減少鎖的競爭、提升查詢效率、還可以減少冗余記錄的查詢。


P13:查詢執行流程

簡單來說分為五步:① 客戶端發送一條查詢給服務器。② 服務器先檢查查詢緩存,如果命中了緩存則立刻返回存儲在緩存中的結果,否則進入下一階段。③ 服務器端進行 SQL 解析、預處理,再由優化器生成對應的執行計劃。④ MySQL 根據優化器生成的執行計劃,調用存儲引擎的 API 來執行查詢。⑤ 將結果返回給客戶端。

查詢緩存

在解析一個查詢語句之前,如果查詢緩存是打開的,那么 MySQL 會優先檢查這個查詢是否命中查詢緩存中的數據。這個檢查是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同,也不會匹配緩存結果,這種情況下會進行下一個階段的處理。如果當前的查詢恰好命中了查詢緩存,那么在返回查詢結果之前 MySQL 會檢查一次用戶權限。如果權限沒有問題,MySQL 會跳過其他階段,直接從緩沖中拿到結果并返回給客戶端,這種情況下查詢不會被解析,不用生成執行計劃,不會被執行。

查詢優化處理

該階段包括多個子階段:解析 SQL、預處理、優化 SQL 執行計劃。首先 MySQL 通過關鍵字將 SQL 語句進行解析,并生成一顆對應的解析樹,MySQL 解析器將使用 MySQL 語法規則驗證和解析查詢。例如它將驗證是否使用了錯誤的關鍵字,或者使用關鍵字的順序是否正確等。預處理器則根據一些 MySQL 規則進一步檢查解析樹是否合法,例如檢查數據表和數據列是否存在,還會解析名字和別名看它們是否有歧義。下一步預處理器會驗證權限,這一步通常很快,除非服務器上有非常多的權限配置。

語法樹被認為合法后,查詢優化器將其轉成執行計劃。一條查詢可以有多種查詢方式,最后都返回相同的結果,優化器的作用就是找到這其中最好的執行計劃。MySQL 使用基于成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本,并選擇其中成本最小的一個。優化策略可以簡單分為兩種,一種是靜態優化,可以直接對解析樹分析并完成優化,不依賴于特別的數值,可以認為是一種編譯時優化。另一種是動態優化,和查詢的上下文有關,每次查詢時都需要重新評估。

MySQL 可以處理的優化類型包括:重新定義表的關聯順序、將外連接轉化成內連接、使用等價變換規則、優化 COUNT() 和 MIN() 以及 MAX() 函數、預估并轉為常數表達式、覆蓋索引掃描、子查詢優化等。

查詢執行引擎

在解析和優化階段,MySQL 將生成查詢對應的執行計劃,MySQL 的查詢執行引擎則根據這個計劃來完成整個查詢。執行計劃是一個數據結構,而不是其他關系型數據庫那樣會生成對應的字節碼。查詢執行階段并不復雜,MySQL 只是簡單的根據執行計劃給出的指令逐步執行,再根據執行計劃執行的過程中,有大量操作需要通過調用存儲引擎實現的接口來完成。

返回結果給客戶端

查詢執行的最后一個階段是將結果返回給客戶端,即使查詢不需要返回結果集,MySQL 仍然會返回這個查詢的一些信息,如該查詢影響到的行數。如果查詢可以被緩存,那么 MySQL 會在這個階段將結果存放到查詢緩沖中。MySQL 將結果集返回客戶端是一個增量、逐步返回的過程,這樣做的好處是服務器無需存儲太多的結果,減少內存消耗,也可以讓客戶端第一時間獲得響應結果。結果集中的每一行給都會以一個滿足 MySQL 客戶端/服務器通信協議的包發送,再通過 TCP 協議進行傳輸,在 TCP 傳輸過程中可能對包進行緩存然后批量傳輸。


P14:優化 SQL

定位低效 SQL

可以通過兩種方式來定位執行效率較低的 SQL 語句。一種是通過慢查詢日志定位,可以通過慢查詢日志定位那些已經執行完畢的 SQL 語句。另一種是使用 SHOW PROCESSLIST 查詢,慢查詢日志在查詢結束以后才記錄,所以在應用反應執行效率出現問題的時候查詢慢查詢日志不能定位問題,此時可以使用 SHOW PROCESSLIST 命令查看當前 MySQL 正在進行的線程,包括線程的狀態、是否鎖表等,可以實時查看 SQL 的執行情況,同時對一些鎖表操作進行優化。找到執行效率低的 SQL 語句后,就可以通過 SHOW PROFILE、EXPLAIN 或 trace 等豐富來繼續優化語句。

SHOW PROFILE

通過 SHOW PROFILE 可以分析 SQL 語句性能消耗,例如查詢到 SQL 會執行多少時間,并顯示 CPU、內存使用量,執行過程中系統鎖及表鎖的花費時間等信息。例如 SHOW PROFILE CPU/MEMORY/BLOCK IO FOR QUERY N 分別查詢 id 為 N 的 SQL 語句的 CPU、內存以及 IO 的消耗情況。

TRACE

從 MySQL 5.6 版本開始,可以通過 trace 文件進一步獲取優化器是是如何選擇執行計劃的,在使用時需要先打開設置,然后執行一次 SQL,最后查看 information_schema.optimizer_trace 表而都內容,該表為聯合i表,只能在當前會話進行查詢,每次查詢后返回的都是最近一次執行的 SQL 語句。

EXPLAIN

執行計劃是 SQL 調優的一個重要依據,可以通過 EXPLAIN 命令查看 SQL 語句的執行計劃,如果作用在表上,那么該命令相當于 DESC。EXPLAIN 的指標及含義如下:

指標名 含義
id 表示 SELECT 子句或操作表的順序,執行順序從大到小執行,當 id 一樣時,執行順序從上往下。
select_type 表示查詢中每個 SELECT 子句的類型,例如 SIMPLE 表示不包含子查詢、表連接或其他復雜語法的簡單查詢,PRIMARY 表示復雜查詢的最外層查詢,SUBQUERY 表示在 SELECT 或 WHERE 列表中包含了子查詢。
type 表示訪問類型,性能由差到好為:ALL 全表掃描、index 索引全掃描、range 索引范圍掃描、ref 返回匹配某個單獨值得所有行,常見于使用非唯一索引或唯一索引的非唯一前綴進行的查找,也經常出現在 join 操作中、eq_ref 唯一性索引掃描,對于每個索引鍵只有一條記錄與之匹配、const 當 MySQL 對查詢某部分進行優化,并轉為一個常量時,使用這些訪問類型,例如將主鍵或唯一索引置于 WHERE 列表就能將該查詢轉為一個 const、system 表中只有一行數據或空表,只能用于 MyISAM 和 Memory 表、NULL 執行時不用訪問表或索引就能得到結果。SQL 性能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是consts 最好。
possible_keys 表示查詢時可能用到的索引,但不一定使用。列出大量可能索引時意味著備選索引數量太多了。
key 顯示 MySQL 在查詢時實際使用的索引,如果沒有使用則顯示為 NULL。
key_len 表示使用到索引字段的長度,可通過該列計算查詢中使用的索引的長度,對于確認索引有效性以及多列索引中用到的列數目很重要。
ref 表示上述表的連接匹配條件,即哪些列或常量被用于查找索引列上的值。
rows 表示 MySQL 根據表統計信息及索引選用情況,估算找到所需記錄所需要讀取的行數。
Extra 表示額外信息,例如 Using temporary 表示需要使用臨時表存儲結果集,常見于排序和分組查詢。Using filesort 表示無法利用索引完成的文件排序,這是 ORDER BY 的結果,可以通過合適的索引改進性能。Using index 表示只需要使用索引就可以滿足查詢表得要求,說明表正在使用覆蓋索引。

優化 COUNT 查詢

COUNT 是一個特殊的函數,它可以統計某個列值的數量,在統計列值時要求列值是非空的,不會統計 NULL 值。如果在 COUNT 中指定了列或列的表達式,則統計的就是這個表達式有值的結果數,而不是 NULL。

COUNT 的另一個作用是統計結果集的行數,當 MySQL 確定括號內的表達式不可能為 NULL 時,實際上就是在統計行數。當使用 COUNT() 時, 不會擴展成所有列,它會忽略所有的列而直接統計所有的行數。

某些業務場景并不要求完全精確的 COUNT 值,此時可以使用近似值來代替,EXPLAIN 出來的優化器估算的行數就是一個不錯的近似值,因為執行 EXPLAIN 并不需要真正地執行查詢。

通常來說 COUNT 都需要掃描大量的行才能獲取精確的結果,因此很難優化。在 MySQL 層還能做的就只有覆蓋掃描了,如果還不夠就需要修改應用的架構,可以增加匯總表或者外部緩存系統。

優化關聯查詢

確保 ON 或 USING 子句中的列上有索引,在創建索引時就要考慮到關聯的順序。

確保任何 GROUP BY 和 ORDER BY 的表達式只涉及到一個表中的列,這樣 MySQL 才有可能使用索引來優化這個過程。

在 MySQL 5.5 及以下版本盡量避免子查詢,可以用關聯查詢代替,因為執行器會先執行外部的 SQL 再執行內部的 SQL。

優化 GROUP BY

如果沒有通過 ORDER BY 子句顯式指定要排序的列,當查詢使用 GROUP BY 子句的時候,結果集會自動按照分組的字段進行排序,如果不關心結果集的順序,可以使用 ORDER BY NULL 禁止排序。

優化 LIMIT 分頁

在偏移量非常大的時候,需要查詢很多條數據再舍棄,這樣的代價非常高。要優化這種查詢,要么是在頁面中限制分頁的數量,要么是優化大偏移量的性能。最簡單的辦法是盡可能地使用覆蓋索引掃描,而不是查詢所有的列,然后根據需要做一次關聯操作再返回所需的列。

還有一種方法是從上一次取數據的位置開始掃描,這樣就可以避免使用 OFFSET。其他優化方法還包括使用預先計算的匯總表,或者關聯到一個冗余表,冗余表只包含主鍵列和需要做排序的數據列。

優化 UNION 查詢

MySQL 通過創建并填充臨時表的方式來執行 UNION 查詢,除非確實需要服務器消除重復的行,否則一定要使用 UNION ALL,如果沒有 ALL 關鍵字,MySQL 會給臨時表加上 DISTINCT 選項,這會導致對整個臨時表的數據做唯一性檢查,這樣做的代價非常高。

使用用戶自定義變量

在查詢中混合使用過程化和關系化邏輯的時候,自定義變量可能會非常有用。用戶自定義變量是一個用來存儲內容的臨時容器,在連接 MySQL 的整個過程中都存在,可以在任何可以使用表達式的地方使用自定義變量。例如可以使用變量來避免重復查詢剛剛更新過的數據、統計更新和插入的數量等。

優化 INSERT

需要對一張表插入很多行數據時,應該盡量使用一次性插入多個值的 INSERT 語句,這種方式將縮減客戶端與數據庫之間的連接、關閉等消耗,效率比多條插入單個值的 INSERT 語句高。也可以關閉事務的自動提交,在插入完數據后提交。當插入的數據是按主鍵的順序插入時,效率更高。


P15:復制

復制解決的基本問題是讓一臺服務器的數據與其他服務器保持同步,一臺主庫的數據可以同步到多臺備庫上,備庫本身也可以被配置成另外一臺服務器的主庫。主庫和備庫之間可以有多種不同的組合方式。

MySQL 支持兩種復制方式:基于行的復制和基于語句的復制,基于語句的復制也稱為邏輯復制,從 MySQL 3.23 版本就已存在,基于行的復制方式在 5.1 版本才被加進來。這兩種方式都是通過在主庫上記錄二進制日志、在備庫重放日志的方式來實現異步的數據復制。因此同一時刻備庫的數據可能與主庫存在不一致,并且無法包裝主備之間的延遲。

MySQL 復制大部分是向后兼容的,新版本的服務器可以作為老版本服務器的備庫,但是老版本不能作為新版本服務器的備庫,因為它可能無法解析新版本所用的新特性或語法,另外所使用的二進制文件格式也可能不同。

復制解決的問題:數據分布、負載均衡、備份、高可用性和故障切換、MySQL 升級測試。

復制步驟

概述:① 在主庫上把數據更改記錄到二進制日志中。② 備庫將主庫的日志復制到自己的中繼日志中。 ③ 備庫讀取中繼日志中的事件,將其重放到備庫數據之上。

第一步是在主庫上記錄二進制日志,每次準備提交事務完成數據更新前,主庫將數據更新的事件記錄到二進制日志中。MySQL 會按事務提交的順序而非每條語句的執行順序來記錄二進制日志,在記錄二進制日志后,主庫會告訴存儲引擎可以提交事務了。

下一步,備庫將主庫的二進制日志復制到其本地的中繼日志中。備庫首先會啟動一個工作的 IO 線程,IO 線程跟主庫建立一個普通的客戶端連接,然后在主庫上啟動一個特殊的二進制轉儲線程,這個線程會讀取主庫上二進制日志中的事件。它不會對事件進行輪詢。如果該線程追趕上了主庫將進入睡眠狀態,直到主庫發送信號量通知其有新的事件產生時才會被喚醒,備庫 IO 線程會將接收到的事件記錄到中繼日志中。

備庫的 SQL 線程執行最后一步,該線程從中繼日志中讀取事件并在備庫執行,從而實現備庫數據的更新。當 SQL 線程追趕上 IO 線程時,中繼日志通常已經在系統緩存中,所以中繼日志的開銷很低。SQL 線程執行的時間也可以通過配置選項來決定是否寫入其自己的二進制日志中。

這種復制架構實現了獲取事件和重放事件的解耦,允許這兩個過程異步進行,也就是說 IO 線程能夠獨立于 SQL 線程工作。但這種架構也限制了復制的過程,在主庫上并發允許的查詢在備庫只能串行化執行,因為只有一個 SQL 線程來重放中繼日志中的事件。


Redis 10

P1:特點

基于鍵值對的數據結構服務器

Redis 中的值不僅可以是字符串,還可以是具體的數據結構,這樣不僅能應用于多種場景開發,也可以提高開發效率。它主要提供五種數據結構:字符串、哈希、列表、集合、有序集合,同時在字符串的基礎上演變出了 Bitmaps 和 HyperLogLog 兩種數據結構,Redis 3.2 還加入了有關 GEO 地理信息定位的功能。

豐富的功能

① 提供了鍵過期功能,可以實現緩存。② 提供了發布訂閱功能,可以實現消息系統。③ 支持 Lua 腳本,可以創造新的 Redis 命令。④ 提供了簡單的事務功能,能在一定程度上保證事務特性。⑤ 提供了流水線功能,客戶端能將一批命令一次性傳到 Redis,減少網絡開銷。

簡單穩定

Redis 的簡單主要體現在三個方面:① 源碼很少,早期只有 2 萬行左右,在 3.0 版本由于添加了集群特性,增加到了 5 萬行左右,相對于很多 NoSQL 數據庫來說代碼量要少很多。② 采用單線程模型,使得服務端處理模型更簡單,也使客戶端開發更簡單。③ 不依賴底層操作系統的類庫,自己實現了事件處理的相關功能。雖然 Redis 比較簡單,但也很穩定。

客戶端語言多

Redis 提供了簡單的 TCP 通信協議,很多編程語言可以方便地接入 Redis,例如 Java、PHP、Python、C、C++ 等。

持久化

通常來說數據放在內存中是不安全的,一旦發生斷電或故障數據就可能丟失,因此 Redis 提供了兩種持久化方式 RDB 和 AOF 將內存的數據保存到硬盤中。

數據結構和內部編碼

可以使用 type 命令查看當前鍵的數據類型結構,它們分別是:string、hash、list、set、zset,但這些只是 Redis 對外的數據結構。實際上每種數據結構都有自己底層的內部編碼實現,這樣 Redis 會在合適的場景選擇合適的內部編碼,string 包括了 raw、int 和 embstr,hash 包括了 hashtable 和 ziplist,list 包括了 linkedlist 和 ziplist,set 包括了 hashtable 和 intset,zset 包括了 skiplist 和 ziplist。可以使用 object encoding 查看內部編碼。

Redis 這樣設計的好處是:① 可以改進內部編碼,而對外的數據結構和命令沒有影響。② 多種內部編碼實現可以在不同場景下發揮各自的優勢,例如 ziplist 比較節省內存,但在列表元素較多的情況下性能有所下降,這時 Redis 會根據配置選項將列表類型的內部實現轉換為 linkedlist。

高性能

Redis 使用了單線程架構和 IO 多路復用模型來實現高性能的內存數據庫服務。

每次客戶端調用都經歷了發送命令、執行命令、返回結果三個過程,因為 Redis 是單線程處理命令的,所以一條命令從客戶端到達服務器不會立即執行,所有命令都會進入一個隊列中,然后逐個被執行。客戶端的執行順序可能不確定,但是可以確定不會有兩條命令被同時執行,不存在并發問題。

通常來說單線程處理能力要比多線程差,Redis 快的原因:① 純內存訪問,Redis 將所有數據放在內存中。② 非阻塞 IO,Redis 使用 epoll 作為 IO 多路復用技術的實現,再加上 Redis 本身的事件處理模型將 epoll 中的連接、讀寫、關閉都轉換為時間,不在網絡 IO 上浪費過多的時間。③ 單線程避免了線程切換和競爭產生的消耗。單線程的一個問題是對于每個命令的執行時間是有要求的,如果某個命令執行時間過長會造成其他命令的阻塞,對于 Redis 這種高性能服務來說是致命的,因此 Redis 是面向快速執行場景的數據庫。


P2:字符串

字符串類型是 Redis 最基礎的數據結構,鍵都是字符串類型,而且其他幾種數據結構都是在字符串類型的基礎上構建的。字符串類型的值可以實際可以是字符串(簡單的字符串、復雜的字符串如 JSON、XML)、數字(整形、浮點數)、甚至二進制(圖片、音頻、視頻),但是值最大不能超過 512 MB。

設置值

set key value [ex seconds] [px millseconds] [nx|xx]

  • ex seconds:為鍵設置秒級過期時間,跟 setex 效果一樣
  • px millseconds:為鍵設置毫秒級過期時間
  • nx:鍵必須不存在才可以設置成功,用于添加,跟 setnx 效果一樣。由于 Redis 的單線程命令處理機制,如果多個客戶端同時執行,則只有一個客戶端能設置成功,可以用作分布式鎖的一種實現。
  • xx:鍵必須存在才可以設置成功,用于更新

獲取值

get key,如果不存在返回 nil

批量設置值

mset key value [key value…]

批量獲取值

mget key [key…]

批量操作命令可以有效提高開發效率,假如沒有 mget,執行 n 次 get 命令需要 n 次網絡時間 + n 次命令時間,使用 mget 只需要 1 次網絡時間 + n 次命令時間。Redis 可以支持每秒數萬的讀寫操作,但這指的是 Redis 服務端的處理能力,對于客戶端來說一次命令處理命令時間還有網絡時間。因為 Redis 的處理能力已足夠高,對于開發者來說,網絡可能會成為性能瓶頸。

計數

incr key

incr 命令用于對值做自增操作,返回結果分為三種:① 值不是整數返回錯誤。② 值是整數,返回自增后的結果。③ 值不存在,按照值為 0 自增,返回結果 1。除了 incr 命令,還有自減 decr、自增指定數字 incrby、自減指定數組 decrby、自增浮點數 incrbyfloat。

內部編碼

  • int:8 個字節的長整形
  • embstr:小于等于 39 個字節的字符串
  • raw:大于 39 個字節的字符串

典型使用場景

  • 緩存功能
    Redis 作為緩存層,MySQL 作為存儲層,首先從 Redis 獲取數據,如果失敗就從 MySQL 獲取并將結果寫回 Redis 并添加過期時間。
  • 計數
    Redis 可以實現快速計數功能,例如視頻每播放一次就用 incy 把播放數加 1。
  • 共享 Session
    一個分布式 Web 服務將用戶的 Session 信息保存在各自服務器,但會造成一個問題,出于負載均衡的考慮,分布式服務會將用戶的訪問負載到不同服務器上,用戶刷新一次可能會發現需要重新登陸。為解決該問題,可以使用 Redis 將用戶的 Session 進行集中管理,在這種模式下只要保證 Redis 是高可用和擴展性的,每次用戶更新或查詢登錄信息都直接從 Redis 集中獲取。
  • 限速
    例如為了短信接口不被頻繁訪問會限制用戶每分鐘獲取驗證碼的次數或者網站限制一個 IP 地址不能在一秒內訪問超過 n 次。可以使用鍵過期策略和自增計數實現。

P3:哈希

哈希類型是指鍵值本身又是一個鍵值對結構,哈希類型中的映射關系叫做 field-value,這里的 value 是指 field 對于的值而不是鍵對于的值。

設置值

hset key field value,如果設置成功會返回 1,反之會返回 0,此外還提供了 hsetnx 命令,作用和 setnx 類似,只是作用于由鍵變為 field。

獲取值

hget key field,如果不存在會返回 nil。

刪除 field

hdel key field [field…],會刪除一個或多個 field,返回結果為刪除成功 field 的個數。

計算 field 個數

hlen key

批量設置或獲取 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。

內部編碼

  • ziplist 壓縮列表:當哈希類型元素個數和值小于配置值(默認 512 個和 64 字節)時會使用 ziplist 作為內部實現,使用更緊湊的結構實現多個元素的連續存儲,在節省內存方面比 hashtable 更優秀。
  • hashtable 哈希表:當哈希類型無法滿足 ziplist 的條件時會使用 hashtable 作為哈希的內部實現,因為此時 ziplist 的讀寫效率會下降,而 hashtable 的讀寫時間復雜度都為 O(1)。

使用場景

緩存用戶信息,有三種實現:

  • 原生字符串類型:每個屬性一個鍵。
    優點:簡單直觀,每個屬性都支持更新操作。
    缺點:占用過多的鍵,內存占用量較大,用戶信息內聚性差,一般不會在生產環境使用。
  • 序列化字符串類型:將用戶信息序列化后用一個鍵保存。
    優點:編程簡單,如果合理使用序列化可以提高內存使用率。
    缺點:序列化和反序列化有一定開銷,同時每次更新屬性都需要把全部數據取出進行反序列化,更新后再序列化到 Redis。
  • 哈希類型:每個用戶屬性使用一對 field-value,但只用一個鍵保存。
    優點:簡單直觀,如果合理使用可以減少內存空間使用。
    缺點:要控制哈希在 ziplist 和 hashtable 兩種內部編碼的轉換,hashtable 會消耗更多內存。

P4:列表

列表類型是用來存儲多個有序的字符串,列表中的每個字符串稱為元素,一個列表最多可以存儲 232-1 個元素。可以對列表兩端插入(push)和彈出(pop),還可以獲取指定范圍的元素列表、獲取指定索引下標的元素等。列表是一種比較靈活的數據結構,它可以充當棧和隊列的角色,在實際開發中有很多應用場景。

列表類型有兩個特點:① 列表中的元素是有序的,可以通過索引下標獲取某個元素或者某個范圍內的元素列表。② 列表中的元素可以重復。

添加操作

從右邊插入元素:rpush key value [value…]

從左到右獲取列表的所有元素:lrange 0 -1

從左邊插入元素:lpush key value [value…]

向某個元素前或者后插入元素:linsert key before|after pivot value,會在列表中找到等于 pivot 的元素,在其前或后插入一個新的元素 value。

查找

獲取指定范圍內的元素列表:lrange key start end,索引從左到右的范圍是 0~N-1,從右到左是 -1~-N,lrange 中的 end 包含了自身。

獲取列表指定索引下標的元素:lindex key index,獲取最后一個元素可以使用 lindex key -1。

獲取列表長度:llen key

刪除

從列表左側彈出元素:lpop key

從列表右側彈出元素:rpop key

刪除指定元素:lrem key count value,如果 count 大于 0,從左到右刪除最多 count 個元素,如果 count 小于 0,從右到左刪除最多個 count 絕對值個元素,如果 count 等于 0,刪除所有。

按照索引范圍修剪列表:ltrim key start end,只會保留 start ~ end 范圍的元素。

修改

修改指定索引下標的元素:lset key index newValue。

阻塞操作

阻塞式彈出:blpop/brpop key [key…] timeout,timeout 表示阻塞時間。

當列表為空時,如果 timeout = 0,客戶端會一直阻塞,如果在此期間添加了元素,客戶端會立即返回。

如果是多個鍵,那么brpop會從左至右遍歷鍵,一旦有一個鍵能彈出元素,客戶端立即返回。

如果多個客戶端對同一個鍵執行 brpop,那么最先執行該命令的客戶端可以獲取彈出的值。

內部編碼

  • ziplist 壓縮列表:跟哈希的 zipilist 相同,元素個數和大小小于配置值(默認 512 個和 64 字節)時使用。
  • linkedlist 鏈表:當列表類型無法滿足 ziplist 的條件時會使用linkedlist。

Redis 3.2 提供了 quicklist 內部編碼,它是以一個 ziplist 為節點的 linkedlist,它結合了兩者的優勢,為列表類提供了一種更為優秀的內部編碼實現。

使用場景

  • 消息隊列
    Redis 的 lpush + brpop 即可實現阻塞隊列,生產者客戶端使用 lpush 從列表左側插入元素,多個消費者客戶端使用 brpop 命令阻塞式地搶列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。
  • 文章列表
    每個用戶有屬于自己的文章列表,現在需要分頁展示文章列表,就可以考慮使用列表。因為列表不但有序,同時支持按照索引范圍獲取元素。每篇文章使用哈希結構存儲。

lpush + lpop = 棧、lpush + rpop = 隊列、lpush + ltrim = 優先集合、lpush + brpop = 消息隊列


P5:集合

集合類型也是用來保存多個字符串元素,和列表不同的是集合不允許有重復元素,并且集合中的元素是無序的,不能通過索引下標獲取元素。一個集合最多可以存儲 232-1 個元素。Redis 除了支持集合內的增刪改查,還支持多個集合取交集、并集、差集。

添加元素

sadd key element [element…],返回結果為添加成功的元素個數。

刪除元素

srem key element [element…],返回結果為成功刪除的元素個數。

計算元素個數

scard key,時間復雜度為 O(1),會直接使用 Redis 內部的遍歷。

判斷元素是否在集合中

sismember key element,如果存在返回 1,否則返回 0。

隨機從集合返回指定個數個元素

srandmember key [count],如果不指定 count 默認為 1。

從集合隨機彈出元素

spop key,可以從集合中隨機彈出一個元素。

獲取所有元素

smembers key

求多個集合的交集/并集/差集

sinter key [key…]

sunion key [key…]

sdiff key [key…]

保存交集、并集、差集的結果

sinterstore/sunionstore/sdiffstore destination key [key…]

集合間運算在元素較多情況下比較耗時,Redis 提供這三個指令將集合間交集、并集、差集的結果保存在 destination key 中。

內部編碼

  • intset 整數集合:當集合中的元素個數小于配置值(默認 512 個時),使用 intset。
  • hashtable 哈希表:當集合類型無法滿足 intset 條件時使用 hashtable。當某個元素不為整數時,也會使用 hashtable。

使用場景

集合類型比較典型的使用場景是標簽,例如一個用戶可能與娛樂、體育比較感興趣,另一個用戶可能對例時、新聞比較感興趣,這些興趣點就是標簽。這些數據對于用戶體驗以及增強用戶黏度比較重要。

sadd = 標簽、spop/srandmember = 生成隨機數,比如抽獎、sadd + sinter = 社交需求


P6:有序集合

有序集合保留了集合不能有重復成員的特性,不同的是可以排序。但是它和列表使用索引下標作為排序依據不同的是,他給每個元素設置一個分數(score)作為排序的依據。有序集合提供了獲取指定分數和元素查詢范圍、計算成員排名等功能。

數據結構 是否允許元素重復 是否有序 有序實現方式 應用場景
列表 是 是 下標 時間軸,消息隊列
集合 否 否 / 標簽,社交
有序集合 否 是 分值 排行榜,社交

添加成員

zadd key score member [score member…],返回結果是成功添加成員的個數

Redis 3.2 為 zadd 命令添加了 nx、xx、ch、incr 四個選項:

  • nx:member 必須不存在才可以設置成功,用于添加。
  • xx:member 必須存在才能設置成功,用于更新。
  • ch:返回此次操作后,有序集合元素和分數變化的個數。
  • incr:對 score 做增加,相當于 zincrby。

zadd 的時間復雜度為 O(logn),sadd 的時間復雜度為 O(1)。

計算成員個數

zcard key,時間復雜度為 O(1)。

計算某個成員的分數

zscore key member ,如果不存在則返回 nil。

計算成員排名

zrank key member,從低到高返回排名。

zrevrank key member,從高到低返回排名。

刪除成員

zrem key member [member…],返回結果是成功刪除的個數。

增加成員的分數

zincrby key increment member

返回指定排名范圍的成員

zrange key start end [withscores],從低到高返回

zrevrange key start end [withscores], 從高到底返回

返回指定分數范圍的成員

zrangebyscore key min max [withscores] [limit offset count],從低到高返回

zrevrangebyscore key min max [withscores] [limit offset count], 從高到底返回

返回指定分數范圍成員個數

zcount key min max

刪除指定分數范圍內的成員

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:交集結果保存到這個鍵
  • numkeys:要做交集計算鍵的個數
  • key:需要做交集計算的鍵
  • weight:每個鍵的權重,默認 1
  • aggregate sum|min|max:計算交集后,分值可以按和、最小值、最大值匯總,默認 sum。

內部編碼

  • ziplist 壓縮列表:當有序集合元素個數和值小于配置值(默認128 個和 64 字節)時會使用 ziplist 作為內部實現。
  • skiplist 跳躍表:當 ziplist 不滿足條件時使用,因為此時 ziplist 的讀寫效率會下降。

使用場景

有序集合的典型使用場景就是排行榜系統,例如用戶上傳了一個視頻并獲得了贊,可以使用 zadd 和 zincrby。如果需要將用戶從榜單刪除,可以使用 zrem。如果要展示獲取贊數最多的十個用戶,可以使用 zrange。


P7:鍵和數據庫管理

鍵重命名

rename key newkey

如果 rename 前鍵已經存在,那么它的值也會被覆蓋。為了防止強行覆蓋,Redis 提供了 renamenx 命令,確保只有 newkey 不存在時才被覆蓋。由于重命名鍵期間會執行 del 命令刪除舊的鍵,如果鍵對應值比較大會存在阻塞的可能。

隨機返回一個鍵

random key

鍵過期

expire key seconds:鍵在 seconds 秒后過期。

如果過期時間為負值,鍵會被立即刪除,和 del 命令一樣。persist 命令可以將鍵的過期時間清除。

對于字符串類型鍵,執行 set 命令會去掉過期時間,set 命令對應的函數 setKey 最后執行了 removeExpire 函數去掉了過期時間。setex 命令作為 set + expire 的組合,不單是原子執行并且減少了一次網絡通信的時間。

鍵遷移

  • move
    move 命令用于在 Redis 內部進行數據遷移,move key db 把指定的鍵從源數據庫移動到目標數據庫中。
  • dump + restore
    可以實現在不同的 Redis 實例之間進行數據遷移,分為兩步:
    ① dump key ,在源 Redis 上,dump 命令會將鍵值序列化,格式采用 RDB 格式。
    ② restore key ttl value,在目標 Redis 上,restore 命令將序列化的值進行復原,ttl 代表過期時間, ttl = 0 則沒有過期時間。
    整個遷移并非原子性的,而是通過客戶端分步完成,并且需要兩個客戶端。
  • migrate
    實際上 migrate 命令就是將 dump、restore、del 三個命令進行組合,從而簡化操作流程。migrate 具有原子性,支持多個鍵的遷移,有效提高了遷移效率。實現過程和 dump + restore 類似,有三點不同:
    ① 整個過程是原子執行,不需要在多個 Redis 實例開啟客戶端。
    ② 數據傳輸直接在源 Redis 和目標 Redis 完成。
    ③ 目標 Redis 完成 restore 后會發送 OK 給源 Redis,源 Redis 接收后根據 migrate 對應選項來決定是否在源 Redis 上刪除對應鍵。

切換數據庫

select dbIndex,Redis 中默認配置有 16 個數據庫,例如 select 0 將切換到第一個數據庫,數據庫之間的數據是隔離的。

flushdb/flushall

用于清除數據庫,flushdb 只清除當前數據庫,flushall 會清除所有數據庫。如果當前數據庫鍵值數量比較多,flushdb/flushall 存在阻塞 Redis 的可能性。


P8:發布訂閱

Redis 提供了基于發布/訂閱模式的消息機制,該模式下消息發布者和訂閱者不進行直接通信,發布者客戶端向指定的頻道(channel)發送消息,訂閱該頻道的每個客戶端都可以收到該消息。

發布消息

publish channel message,返回結果為訂閱者的個數。

訂閱消息

subscribe channel [channel…],訂閱者可以訂閱一個或多個頻道。

客戶端在執行訂閱命令后會進入訂閱狀態,只能接收 subscribe、psubscribe、unsubscribe、punsubscribe 的四個命令。新開啟的訂閱客戶端,無法收到該頻道之前的消息,因為 Redis 不會對發布的消息進行持久化。

和很多專業的消息隊列系統如 Kafka、RocketMQ 相比,Redis 的發布訂閱略顯粗糙,例如無法實現消息堆積和回溯,但勝在足夠簡單,如果當前場景可以容忍這些缺點,也是一個不錯的選擇。

取消訂閱

unsubscribe [channel [channel…]]

客戶端可以通過 unsubscribe 命令取消對指定頻道的訂閱,取消成功后不會再收到該頻道的發布消息。

按照模式訂閱和取消訂閱

psubscribe/unsubscribe pattern [pattern…],例如訂閱所有以 it 開頭的頻道:psubscribe it*


P9:RDB 持久化

RDB 持久化是把當前進程數據生成快照保存到硬盤的過程,觸發 RDB 持久化過程分為手動觸發和自動觸發。

觸發機制

手動觸發分別對應 save 和 bgsave 命令:

  • save:阻塞當前 Redis 服務器,直到 RDB 過程完成為止,對于內存比較大的實例會造成長時間阻塞,線上環境不建議使用。
  • bgasve:Redis 進程執行 fork 操作創建子進程,RDB 持久化過程由子進程負責,完成后自動結束。阻塞只發生在 fork 階段,一般時間很短。bgsave 是針對 save 阻塞問題做的優化,因此 Redis 內部所有涉及 RDB 的操作都采用 bgsave 的方式,而 save 方式已經廢棄。

除了手動觸發外,Redis 內部還存在自動觸發 RDB 的持久化機制,例如:

  • 使用 save 相關配置,如 save m n,表示 m 秒內數據集存在 n 次修改時,自動觸發 bgsave。
  • 如果從節點執行全量復制操作,主節點自動執行 bgsave 生成 RDB 文件并發送給從節點。
  • 執行 debug reload 命令重新加載 Redis 時也會自動觸發 save 操作。
  • 默認情況下執行 shutdown 命令時,如果沒有開啟 AOF 持久化功能則自動執行 bgsave。

bgsave 是主流的觸發 RDB 持久化的方式,運作流程如下:

① 執行 bgsave 命令,Redis 父進程判斷當前是否存在正在執行的子進程,如 RDB/AOF 子進程,如果存在 bgsave 命令直接返回。

② 父進程執行 fork 操作創建子進程,fork 操作過程中父進程會阻塞。

③ 父進程 fork 完成后,bgsave 命令返回并不再阻塞父進程,可以繼續響應其他命令。

④ 子進程創建 RDB 文件,根據父進程內存生成臨時快照文件,完成后對原有文件進行原子替換。

⑤ 進程發送信號給父進程表示完成,父進程更新統計信息。

優點

RDB 是一個緊湊壓縮的二進制文件,代表 Redis 在某個時間點上的數據快照。非常適合于備份,全量復制等場景。例如每 6 個消時執行 bgsave 備份,并把 RDB 文件拷貝到遠程機器或者文件系統中,用于災難恢復。

Redis 加載 RDB 恢復數據遠遠快于 AOF 的方式。

缺點

RDB 方式數據無法做到實時持久化/秒級持久化,因為 bgsave 每次運行都要執行 fork 操作創建子進程,屬于重量級操作,頻繁執行成本過高。針對 RDB 不適合實時持久化的問題,Redis 提供了 AOF 持久化方式。

RDB 文件使用特定二進制格式保存,Redis 版本演進過程中有多個格式的 RDB 版本,存在老版本 Redis 服務無法兼容新版 RDB 格式的問題。


P10:AOF 持久化

AOF 持久化以獨立日志的方式記錄每次寫命令,重啟時再重新執行 AOF 文件中的命令達到恢復數據的目的。AOF 的主要作用是解決了數據持久化的實時性,目前是 Redis 持久化的主流方式。

開啟 AOF 功能需要設置:appendonly yes,默認不開啟。保存路徑同 RDB 方式一致,通過 dir 配置指定。

AOF 的工作流程操作:命令寫入 append、文件同步 sync、文件重寫 rewrite、重啟加載 load:

  • 所有的寫入命令會追加到 aof_buf 緩沖區中。
  • AOF 緩沖區根據對應的策略向硬盤做同步操作。
  • 隨著 AOF 文件越來越大,需要定期對 AOF 文件進行重寫,達到壓縮的目的。
  • 當服務器重啟時,可以加載 AOF 文件進行數據恢復。

命令寫入

AOF 命令寫入的內容直接是文本協議格式,采用文本協議格式的原因:

  • 文本協議具有很好的兼容性。
  • 開啟 AOF 后所有寫入命令都包含追加操作,直接采用協議格式避免了二次處理開銷。
  • 文本協議具有可讀性,方便直接修改和處理。

AOF 把命令追加到緩沖區的原因:

Redis 使用單線程響應命令,如果每次寫 AOF 文件命令都直接追加到硬盤,那么性能完全取決于當前硬盤負載。先寫入緩沖區中還有另一個好處,Redis 可以提供多種緩沖區同步硬盤策略,在性能和安全性方面做出平衡。


文件同步

Redis 提供了多種 AOF 緩沖區文件同步策略,由參數 appendfsync 控制,不同值的含義如下:

  • always:命令寫入緩沖區后調用系統 fsync 操作同步到 AOF 文件,fsync 完成后線程返回。每次寫入都要同步 AOF,性能較低,不建議配置。
  • everysec:命令寫入緩沖區后調用系統 write 操作,write 完成后線程返回。fsync 同步文件操作由專門線程每秒調用一次。是建議的策略,也是默認配置,兼顧性能和數據安全。
  • no:命令寫入緩沖區后調用系統 write 操作,不對 AOF 文件做 fsync 同步,同步硬盤操作由操作系統負責,周期通常最長 30 秒。由于操作系統每次同步 AOF 文件的周期不可控,而且會加大每次同步硬盤的數據量,雖然提升了性能,但安全性無法保證。

文件重寫

文件重寫是把 Redis 進程內的數據轉化為寫命令同步到新 AOF 文件的過程,可以降低文件占用空間,更小的文件可以更快地被 加載。

重寫后 AOF 文件變小的原因:

  • 進程內已經超時的數據不再寫入文件。
  • 舊的 AOF 文件含有無效命令,重寫使用進程內數據直接生成,這樣新的 AOF 文件只保留最終數據寫入命令。
  • 多條寫命令可以合并為一個,為了防止單條命令過大造成客戶端緩沖區溢出,對于 list、set、hash、zset 等類型操作,以 64 個元素為界拆分為多條。

AOF 重寫分為手動觸發和自動觸發,手動觸發直接調用 bgrewriteaof 命令,自動觸發根據 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 參數確定自動觸發時機。

重寫流程:

① 執行 AOF 重寫請求,如果當前進程正在執行 AOF 重寫,請求不執行并返回,如果當前進程正在執行 bgsave 操作,重寫命令延遲到 bgsave 完成之后再執行。

② 父進程執行 fork 創建子進程,開銷等同于 bgsave 過程。

③ 父進程 fork 操作完成后繼續響應其他命令,所有修改命令依然寫入 AOF 緩沖區并同步到硬盤,保證原有 AOF 機制正確性。

④ 子進程根據內存快照,按命令合并規則寫入到新的 AOF 文件。每次批量寫入數據量默認為 32 MB,防止單次刷盤數據過多造成阻塞。

⑤ 新 AOF 文件寫入完成后,子進程發送信號給父進程,父進程更新統計信息。

⑥ 父進程把 AOF 重寫緩沖區的數據寫入到新的 AOF 文件并替換舊文件,完成重寫。


重啟加載

AOF 和 RDB 文件都可以用于服務器重啟時的數據恢復。Redis 持久化文件的加載流程:

① AOF 持久化開啟且存在 AOF 文件時,優先加載 AOF 文件。

② AOF 關閉時且存在 RDB 文件時,記載 RDB 文件。

③ 加載 AOF/RDB 文件成功后,Redis 啟動成功。

④ AOF/RDB 文件存在錯誤導致加載失敗時,Redis 啟動失敗并打印錯誤信息。


總結

以上是生活随笔為你收集整理的【备战秋招系列-4】Java高频知识——并发、Spring、MySQL、redis的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

久久综合久久综合久久 | 综合激情网 | 最近日韩中文字幕中文 | 亚洲精品乱码久久久久久蜜桃动漫 | 久久精品中文字幕少妇 | 久久久久久国产精品免费 | 久久观看最新视频 | 五月开心婷婷网 | 欧美精品v国产精品v日韩精品 | 一区二区伦理 | 深爱综合网 | 国产一级特黄毛片在线毛片 | 狠狠躁日日躁狂躁夜夜躁 | 综合网伊人 | 国产精品系列在线播放 | 国产精品一区二区久久精品爱微奶 | 99久久久久久 | 亚洲国产精品va在线看黑人动漫 | 日韩视频免费 | 久久99精品久久久久久秒播蜜臀 | 亚洲自拍偷拍色图 | 日韩簧片在线观看 | 国产在线观看99 | 成人久久视频 | 免费看一级一片 | 久久精品亚洲精品国产欧美 | 人人玩人人添人人澡97 | 久久天堂影院 | 国产精品久久久av久久久 | 国产精品美女久久久免费 | 国产精品国产毛片 | 国产一级不卡毛片 | 亚洲国产美女精品久久久久∴ | 久久er99热精品一区二区三区 | 亚洲无吗av| 国产精品永久久久久久久久久 | 日韩视频一区二区三区 | 不卡电影免费在线播放一区 | 久久国产高清 | 国产精品嫩草影院9 | 91麻豆精品一区二区三区 | 超碰免费在线公开 | 西西444www大胆无视频 | 亚洲黄色片 | 黄色软件在线观看视频 | 国产黄色理论片 | 国产精品福利久久久 | 免费黄色a网站 | 在线精品亚洲一区二区 | www日韩高清| 91免费在线 | 久久免费精品 | 操天天操 | 中文有码在线 | 久久99久久99精品免费看小说 | av福利在线导航 | 免费网站观看www在线观看 | 欧美精品一区二区三区一线天视频 | 丰满少妇在线观看 | 五月婷婷六月丁香 | 久久视| 久久天天躁狠狠躁夜夜不卡公司 | 亚洲精选视频免费看 | 久久久久久久久久久久国产精品 | 免费一级片在线观看 | 99久久精品国 | 欧美一级片在线播放 | 国产视频一区二区在线播放 | 欧美一级性视频 | www.色午夜 | 欧美91精品国产自产 | 成人小视频在线播放 | 日韩理论在线 | 亚洲成免费 | 天天干夜夜| 香蕉97视频观看在线观看 | 91精品免费在线观看 | 久久视| av在线永久免费观看 | 狠狠干婷婷 | 亚洲人成综合 | 久草视频中文在线 | 国产一级免费观看 | 久久99深爱久久99精品 | 色婷婷a| 日韩三级中文字幕 | a久久免费视频 | 毛片二区 | 在线免费观看黄色小说 | 久久久久久麻豆 | 国产精品久久久久久高潮 | 日韩高清av在线 | 国产伦理一区 | 精品一区二区在线免费观看 | 亚洲国产精品久久久久婷婷884 | 国产一区二区三区视频在线 | 黄色在线看网站 | 91九色网站| 人人插人人澡 | 日韩精品免费在线播放 | 欧美精品久久久久久久久久 | 久久久久久久久久电影 | 成人综合婷婷国产精品久久免费 | 992tv人人草 黄色国产区 | 精品99在线观看 | 色综合婷婷久久 | 911在线| 一级黄色片在线观看 | 久久综合福利 | 日本精品久久 | 黄色资源在线观看 | 成年人免费av网站 | 久久,天天综合 | 日本中文字幕系列 | 久久精品一二三区白丝高潮 | 少妇自拍av | 99久久久久久 | 国产高清av免费在线观看 | 狠狠干干 | 91丝袜美腿 | 免费瑟瑟网站 | 久久不色| 中文字幕免费高清av | 精品国产伦一区二区三区 | 婷婷丁香激情五月 | 人人射人人 | 日韩欧美一区二区在线观看 | 一区二区三区在线视频观看58 | 13日本xxxxxⅹxxx20 | 日韩色综合| 91av在线看 | 午夜精品久久久久久久99 | 日韩在线国产 | 日韩精品一区二区电影 | 亚洲精品三级 | 色婷婷成人 | 99这里只有精品视频 | 日日摸日日添夜夜爽97 | 亚洲午夜av电影 | 天堂av在线免费 | 91激情视频在线播放 | 免费视频一二三区 | av色图天堂网 | 日本久久久精品视频 | 伊人色综合网 | 在线日韩亚洲 | 亚洲精品视 | 91久久精品一区二区三区 | 天天操天天操天天操天天操 | 久久综合9988久久爱 | 国产黄色在线看 | 国产高清第一页 | 婷婷av在线 | av中文字幕日韩 | 中文一区在线 | 99c视频高清免费观看 | 国产精品成人免费精品自在线观看 | 色噜噜在线观看视频 | 日本一区二区不卡高清 | 九月婷婷人人澡人人添人人爽 | 日韩免费在线观看网站 | 五月婷婷国产 | 日日干美女 | 日韩精品一区二区在线观看 | 国产视频 亚洲精品 | 热久在线| 91成人在线网站 | 国产91在线观看 | 91爱爱视频 | 中文字幕资源在线 | 97超碰人人爱 | 国内精自线一二区永久 | 狠狠插天天干 | 国产亚洲一区二区在线观看 | 国产一区二区在线免费 | 中文综合在线 | 国产精品久久久久久久久久久久 | 国产亚洲视频在线观看 | 人人爽人人 | 色诱亚洲精品久久久久久 | 欧美一区二区三区四区夜夜大片 | 国产成免费视频 | 插综合网 | 在线观看日本高清mv视频 | 美腿丝袜一区二区三区 | 久久久免费 | 亚洲精品777| 欧美日一级片 | 国产成人精品在线观看 | 日韩精品国产一区 | 五月婷婷激情网 | 成人欧美一区二区三区黑人麻豆 | 香蕉影院在线 | 欧美一进一出抽搐大尺度视频 | 日韩理论电影在线 | 亚洲人成精品久久久久 | 亚洲精品中文字幕在线 | 国产又粗又猛又爽又黄的视频免费 | av丁香| 在线观看免费日韩 | 黄色看片 | 久久草av | 日韩三级视频在线观看 | 国产 欧美 日产久久 | 在线观看中文字幕dvd播放 | 精品国产一区二区三区久久久蜜臀 | 国产最新视频在线观看 | www免费视频com━ | 日韩大片免费在线观看 | 国产一区电影在线观看 | 一区二区不卡视频在线观看 | 国产字幕av | 丁香五香天综合情 | 九九热在线视频免费观看 | 91亚洲国产成人久久精品网站 | 国产一性一爱一乱一交 | 国产美女在线精品免费观看 | 免费在线成人av电影 | 日本三级全黄少妇三2023 | 深夜免费福利在线 | 97超碰.com | 久久精品综合 | 精品日韩在线 | 狠狠干网址| 欧美久久99 | 又色又爽又黄 | 国产亚洲免费观看 | 久久久久久免费 | 久久热亚洲 | 97人人添人澡人人爽超碰动图 | 黄av资源| 久久九九影院 | 视频一区二区精品 | 久久综合婷婷综合 | 狠狠色伊人亚洲综合成人 | 久久99操 | 亚洲一区二区高潮无套美女 | 亚洲无吗视频在线 | 久久综合免费视频影院 | 天天操偷偷干 | 99re国产 | 精品国产伦一区二区三区免费 | 国产在线第三页 | 日韩免费观看高清 | av电影免费看 | 成人在线免费视频 | 精品一区 精品二区 | 久久永久免费视频 | 最近免费观看的电影完整版 | 成人精品视频久久久久 | 免费网站看av片 | 日本天天色| 久久精品视频中文字幕 | 在线а√天堂中文官网 | 国产免费视频一区二区裸体 | 日本性动态图 | 一区二区精品在线观看 | 91手机视频 | 在线观看成年人 | 在线看一区二区 | 久久国产二区 | 色干干 | 五月综合| 亚洲激情在线观看 | 免费观看一区二区 | 日韩中文字幕国产 | 国产亚洲欧美在线视频 | 一级黄色免费网站 | 操久久免费视频 | 国产涩图 | 亚洲综合在线一区二区三区 | 99视频在线免费看 | 午夜手机电影 | 91最新网址 | 五月婷婷操 | 国产在线精品国自产拍影院 | 日韩伦理一区二区三区av在线 | 麻豆影视在线免费观看 | 日本黄色免费网站 | 日韩av专区| 国产精品嫩草影视久久久 | 国产精品成人一区二区三区 | 国产 日韩 在线 亚洲 字幕 中文 | 免费精品在线视频 | 精品国产乱码 | 爱色av.com | 99re国产视频 | 午夜av在线播放 | 日韩av免费大片 | 亚洲午夜精 | 精品视频久久久 | 黄色大全视频 | 欧美精品一区在线 | 天天躁天天操 | 国产精品永久在线 | 精品亚洲网 | 亚洲国产欧美在线人成大黄瓜 | 91av在线播放 | 国产高清视频在线播放 | 国产91九色视频 | a在线观看视频 | 在线看片91 | 五月婷婷毛片 | 欧美做受高潮 | 中文字幕在线观看视频一区 | 久久1区| 国际精品网 | 久久视频在线免费观看 | 国产在线观看中文字幕 | 欧美一级欧美一级 | 国产美女精品久久久 | 久久99精品久久久久婷婷 | .国产精品成人自产拍在线观看6 | 中文字幕在线观看网 | 国内精品免费 | 日韩激情在线 | 精品一二三四在线 | 色亚洲激情 | 人人干人人做 | 免费在线观看一区 | 欧美天天综合网 | 中文亚洲欧美日韩 | 在线亚洲小视频 | 日本精品在线视频 | 国产美女视频网站 | 911香蕉视频 | 欧美黑人xxxx猛性大交 | 欧美日韩伦理一区 | 成人av日韩 | 日韩免费网站 | 四虎天堂 | 超碰在线免费福利 | 国产精品精品国产婷婷这里av | 亚洲一二区视频 | 天天天天射 | 激情小说久久 | av在线短片| 美女视频黄免费网站 | 在线看v片成人 | 少妇av网 | 日日天天干 | 黄色网大全 | 精品资源在线 | 韩国一区二区在线观看 | 国产免费观看久久 | 久久www免费人成看片高清 | 亚洲福利精品 | 99国产精品 | 五月天亚洲精品 | 国产福利不卡视频 | 久久夜色网 | 欧美激情第一区 | 久久亚洲精品国产亚洲老地址 | 久久艹在线 | 992tv在线观看 | 午夜精品久久久久久中宇69 | 日韩免费成人av | 亚洲国产中文字幕在线观看 | 午夜久久网 | 日本久久中文 | 麻豆视频免费播放 | 日韩网站在线免费观看 | 在线免费观看视频一区 | 日本论理电影 | 黄色免费网| 国产在线一线 | 91高清在线看 | 在线观看一级 | 久久久国内精品 | 国产亚洲精品久久19p | 婷婷色网站 | 午夜在线资源 | 国产高清av | 日韩动态视频 | 免费在线国产黄色 | 视频在线观看一区 | 97视频免费观看2区 亚洲视屏 | 999久久久久久 | 成年人在线观看网站 | 国产打女人屁股调教97 | 国产麻豆成人传媒免费观看 | 久久夜色精品国产欧美乱极品 | 三级在线播放视频 | 精品国产一区二区三区噜噜噜 | 欧美激情综合五月 | 激情综合网色播五月 | 国产亚洲久一区二区 | 在线免费视频一区 | 高清精品久久 | 日本久久高清视频 | av免费观看高清 | 在线免费黄网站 | 日韩av不卡在线播放 | 亚洲精品国产日韩 | 久久久久在线观看 | 国产精久久久久久妇女av | 欧美日韩网站 | 日韩视频免费 | 国产亚洲精品电影 | av激情五月 | 久99久在线 | 五月天激情在线 | 国产精品高清av | 天天做夜夜做 | 国产精品久久久区三区天天噜 | 涩涩爱夜夜爱 | 久久精彩 | 人人视频网站 | 玖玖在线精品 | 91av免费在线观看 | 精品国产91亚洲一区二区三区www | www.久草视频 | 欧美精品久久久久久久久老牛影院 | www.福利| 操操操人人人 | 视色网站 | 国产美女主播精品一区二区三区 | 日韩欧美电影网 | 国产亚洲久一区二区 | 天天摸夜夜添 | 免费在线成人av电影 | 国产黄色免费 | 国产精品 日韩 欧美 | 99视频导航 | 天天色 天天 | 成人在线免费观看视视频 | 欧美日韩网址 | 色五月成人 | 97夜夜澡人人爽人人免费 | 超碰午夜 | 欧美一级免费高清 | 久久久精品国产免费观看同学 | 国产成人福利 | 亚洲第一成网站 | 日韩精品一区二区在线视频 | 欧美一级电影在线观看 | av中文在线观看 | 99久久er热在这里只有精品66 | 五月婷婷激情六月 | 97国产在线 | 91午夜精品 | 国产成a人亚洲精v品在线观看 | 国产一区在线视频 | av黄色免费在线观看 | 国产一区二区三精品久久久无广告 | 91成人免费看片 | 香蕉日日| 涩涩网站在线看 | 久久成人资源 | 一级黄毛片 | 91探花在线视频 | 国产视频丨精品|在线观看 国产精品久久久久久久久久久久午夜 | 久久99深爱久久99精品 | 91成人在线观看喷潮 | 黄色日批网站 | 97精品国产一二三产区 | 欧美日韩性生活 | 麻豆成人网 | 国产韩国日本高清视频 | 久久久国产精品成人免费 | 国产永久免费高清在线观看视频 | 一区二区视频播放 | www.天堂av| 另类老妇性bbwbbw高清 | 激情综合色综合久久综合 | 久久精品99国产精品 | 国内综合精品午夜久久资源 | 国产精品嫩草影视久久久 | 日韩高清在线一区二区三区 | 国产成人一区二区三区在线观看 | 在线中文字幕一区二区 | 久草免费看 | 成年免费在线视频 | 国内精品在线看 | 黄色软件在线看 | 欧美a视频在线观看 | 久久亚洲福利 | 69热国产视频 | 在线va视频 | 国产只有精品 | 91亚洲精品国产 | 亚洲综合色视频在线观看 | 久久超| 91大神电影| 成人动漫一区二区 | 欧美亚洲久久 | 久久久久久久久久久网站 | 精品福利视频在线观看 | 一区中文字幕在线观看 | 日本精品久久久一区二区三区 | 日韩aa视频 | 91精品办公室少妇高潮对白 | 国产91精品久久久久 | 最新av网站在线观看 | 99精品视频免费观看 | 国产精品视频线看 | 在线一级片| 1024久久 | 免费精品视频在线 | 麻豆视频在线 | av一区二区三区在线播放 | 久草在线视频首页 | 亚洲黄a | 久久不卡电影 | 久久成年人视频 | 丁香婷婷综合色啪 | 国产精品一区二区久久久 | 超碰在线人人艹 | 久久黄色小说视频 | 天天操狠狠操夜夜操 | 国产一区在线观看免费 | 在线激情网 | 亚洲人成综合 | 国产精品久久久久影院日本 | 欧美激情另类文学 | av在观看| 欧美国产精品久久久久久免费 | 午夜婷婷在线播放 | 丝袜制服综合网 | 国产精品一区二区久久精品爱涩 | 国产色a在线观看 | av青草 | 国产一级二级在线播放 | 中文字幕影片免费在线观看 | 精品久久久久久久久久 | 久久精品亚洲国产 | 国产精品免费不 | 国产青草视频在线观看 | 亚洲精品视频在线免费播放 | 日韩久久精品一区二区 | 中文字幕欲求不满 | 亚洲欧美国产精品 | 超碰精品在线 | 亚洲成a人片在线观看网站口工 | 欧美日韩亚洲国产一区 | 婷婷www| 免费看片亚洲 | 国产精品9区 | 免费av网站观看 | 亚洲日本精品视频 | 免费高清在线视频一区· | 国产一区二区影院 | 伊人小视频 | 91黄色在线视频 | 欧美日韩免费观看一区二区三区 | 国产麻豆剧传媒免费观看 | www五月| 免费高清在线观看电视网站 | 国产日韩欧美在线影视 | 91av成人| 欧美美女激情18p | 国产色道 | bbbbb女女女女女bbbbb国产 | 日韩在线视频一区 | 精品国产乱码 | 欧美永久视频 | 久久久久美女 | 日韩视频1 | 午夜精品久久久久99热app | 69精品在线观看 | 天堂av免费在线 | 制服丝袜天堂 | 中国老女人日b | 波多野结依在线观看 | 91精品国产成 | 激情网在线视频 | 日韩黄在线观看 | 久草在线视频精品 | 五月综合久久 | 日韩久久在线 | 国产成人av电影 | 这里只有精品视频在线观看 | 亚洲精品视频在线免费 | 亚洲一区视频免费观看 | 久久中文字幕视频 | 成人在线观看影院 | 五月天亚洲综合 | 在线亚州| a在线免费 | 亚洲在线观看av | 色婷婷激情综合 | 一区二区三区在线播放 | 天天干干 | 国产一级电影网 | 精品资源在线 | 99视频这里只有 | 久久草在线精品 | 免费看三片 | 成人免费网站视频 | 午夜成人免费影院 | 国产私拍在线 | 亚洲成a人片在线观看中文 中文字幕在线视频第一页 狠狠色丁香婷婷综合 | 99 精品 在线 | 激情六月婷婷久久 | 国产精品二区在线 | 日日夜夜噜噜噜 | 综合精品久久久 | 天天综合狠狠精品 | 中文字幕成人在线 | 伊人成人久久 | 五月婷婷六月丁香激情 | 草在线视频 | 五月天国产精品 | 久久久www成人免费毛片 | 久久综合九色综合欧美就去吻 | 91精品推荐 | 91精品办公室少妇高潮对白 | 国产不卡av在线 | 一级片免费观看视频 | 91av看片 | 午夜三级毛片 | 日本99干网 | 黄网站app在线观看免费视频 | 亚洲码国产日韩欧美高潮在线播放 | 亚洲a成人v | 久久在现视频 | 久久综合成人 | 人人草在线观看 | 91亚洲精品久久久蜜桃 | 日韩一级电影在线观看 | 国产成人av免费在线观看 | 国产精品久久久久一区二区国产 | 激情小说 五月 | 一区二区视频在线观看免费 | 亚洲国产成人精品久久 | 夜夜躁狠狠躁日日躁 | 国产精品麻豆91 | 国产色资源 | 不卡av在线 | www.亚洲精品视频 | 97精品国自产拍在线观看 | 国内精品久久久久影院优 | 五月天婷亚洲天综合网精品偷 | 亚洲另类xxxx | 国产中文字幕精品 | 又爽又黄又刺激的视频 | 综合网婷婷 | 综合色在线观看 | 精品在线视频一区 | 国产在线观看午夜 | 欧美视频在线观看免费网址 | 国产经典三级 | 色婷av | 一区 在线 影院 | 久久久综合色 | 久久久五月天 | 亚洲精品字幕 | 婷婷丁香七月 | 欧美日比视频 | 久久网站免费 | 久草免费在线视频观看 | 91视视频在线直接观看在线看网页在线看 | av免费观看在线 | 四虎永久免费网站 | 亚洲一区欧美精品 | 青青河边草免费 | 91在线资源 | 美女视频黄是免费的 | 久热国产视频 | 97视频免费在线看 | 精品美女国产在线 | 在线免费观看视频 | 久精品视频在线观看 | 国产中文字幕av | 欧美日本不卡视频 | 国产一级免费观看视频 | 亚洲精品免费在线 | 欧美性性网| 黄色网在线播放 | 成人黄在线 | 人成在线免费视频 | 天天草天天干天天 | 日韩午夜高清 | 久久久三级视频 | 一本一道久久a久久精品蜜桃 | 免费精品在线视频 | 日日躁你夜夜躁你av蜜 | 福利一区在线视频 | 亚洲精品影视 | 啪啪精品 | 久草电影免费在线观看 | 久久丁香网 | 深爱激情站 | 国产小视频免费在线观看 | 国产性天天综合网 | 国产精品资源在线 | 欧美在线视频一区二区三区 | 国产剧情久久 | 久久精品爱爱视频 | 欧美日韩1区 | 国产亚洲精品久久久久久大师 | 激情综合网婷婷 | 99国产一区二区三精品乱码 | 一区二区欧美日韩 | 天天操天天操天天操天天操天天操天天操 | 久久久久久久久久久精 | 91丨九色丨蝌蚪丨老版 | 国产这里只有精品 | 日韩在线免费视频 | 97在线观看视频免费 | 精品国产伦一区二区三区 | 91刺激视频| 久久精品亚洲 | 五月天激情开心 | 最近中文字幕免费视频 | 亚洲精品88欧美一区二区 | 欧美日韩啪啪 | 亚洲欧美日本国产 | 国产日韩欧美在线播放 | 欧美va天堂va视频va在线 | 天天干天天上 | 国产精品免费不 | 麻豆视频免费播放 | 在线天堂中文在线资源网 | 亚洲三级性片 | 精品专区一区二区 | 国产精品久久9 | 天天干人人干 | 国产伦理一区二区三区 | 久草电影免费在线观看 | 国产免费激情久久 | 欧美精品国产综合久久 | 国内三级在线观看 | 日韩欧美在线不卡 | 久久久蜜桃一区二区 | 福利在线看片 | 狠狠干.com | 国产精品视频全国免费观看 | 人人盈棋牌 | 99在线观看视频 | 91精品国产欧美一区二区 | 91中文字幕在线播放 | 日韩av中文在线观看 | 欧美福利视频一区 | 91精品国产福利 | 狠狠色婷婷丁香六月 | 国产一区二区在线免费播放 | 男女免费av | 免费观看成人 | 中文字幕资源在线 | 超薄丝袜一二三区 | 久久精品视频3 | 国产精品一区在线播放 | 久久一区国产 | 久久久国产网站 | 国产a视频免费观看 | 精品国产123 | 中文字幕中文字幕在线一区 | 高清av中文在线字幕观看1 | 日韩中文字幕在线不卡 | 久久久久女人精品毛片 | 久久国产精品免费看 | 免费观看日韩 | 丝袜网站在线观看 | 久久歪歪 | 国产精品igao视频网网址 | 国产在线观看中文字幕 | 豆豆色资源网xfplay | 国产精品一区二区免费看 | 国产精品v a免费视频 | 久久久久成人免费 | 欧美成人按摩 | 久久久久国产精品一区 | 正在播放国产一区二区 | 国产区在线视频 | 国产精品剧情在线亚洲 | 中文字幕在线观看一区 | 97超碰成人在线 | 成年人免费av网站 | 91精品国产九九九久久久亚洲 | 免费在线观看av | 成年人在线电影 | 久久激情日本aⅴ | 久久天天综合网 | 激情五月综合网 | 亚洲精品在线观看中文字幕 | av7777777| 狠狠干天天操 | 在线视频99| 欧美日韩高清一区二区三区 | 97中文字幕 | 国产精品av免费在线观看 | 国产精品综合在线观看 | 天天色天天操天天爽 | 欧洲精品一区二区 | 欧美欧美| 中文字幕在线观看一区 | 婷婷色狠狠 | 国产一级特黄毛片在线毛片 | 在线看中文字幕 | 日韩久久影院 | 久日精品 | 日韩激情视频 | 亚洲黄色影院 | 免费在线观看成人av | 在线观看深夜视频 | 国产精品va在线播放 | 亚洲专区中文字幕 | 狠狠色香婷婷久久亚洲精品 | av黄色成人| 天堂中文在线视频 | 久久精品一区八戒影视 | 丁香六月婷 | 亚洲综合最新在线 | 九九久久国产精品 | 国产高清久久久久 | 午夜精品久久久久99热app | 99亚洲精品 | 欧美性色综合 | 精品国产一区二区三区蜜臀 | 玖玖精品视频 | 亚洲国产网址 | 久久99精品波多结衣一区 | 国产精品久久久久久久久久99 | 在线日韩精品视频 | 一区二区精品视频 | 狠狠婷婷| 日韩一区二区久久 | 福利视频区 | 国产一区二区不卡在线 | 99久高清在线观看视频99精品热在线观看视频 | 久久久久成人精品免费播放动漫 | 午夜日b视频 | 亚洲专区中文字幕 | 黄色成人在线 | av高清影院 | www欧美xxxx | 国产欧美日韩精品一区二区免费 | 亚洲一区在线看 | 最近日韩中文字幕中文 | 精品久久久久国产免费第一页 | 国产免费视频一区二区裸体 | a视频免费看 | av电影在线观看完整版一区二区 | 国产精品久久久久久久久久久久午 | 奇米影视8888在线观看大全免费 | 依人成人综合网 | 韩国在线视频一区 | 久久国产精品99久久久久久老狼 | 麻豆国产露脸在线观看 | 在线观看日本高清mv视频 | 久章草在线 | 精品国产一区二区三区久久久蜜臀 | 免费观看一级 | 亚洲国产午夜 | 免费网站v| 69av在线播放| a精品视频| 狠狠五月婷婷 | 麻花豆传媒mv在线观看网站 | av在线电影网站 | 国产免费a| 国产精品视频大全 | av丁香花 | 免费黄色网址大全 | 人人插人人射 | 欧美极品xxxxx| 日产乱码一二三区别在线 | 五月网婷婷 | 中文字幕视频一区 | 婷婷午夜激情 | 国产视频资源在线观看 | 黄色大全免费观看 | 激情网五月 | 视频一区视频二区在线观看 | 国产精品99久久99久久久二8 | av黄色免费在线观看 | 欧美一区二区在线 | 亚洲精品1区2区3区 超碰成人网 | 高清视频一区 | 日日麻批40分钟视频免费观看 | 99热精品在线观看 | 美女网站色 | 午夜精品久久久久99热app | 久久久久国产精品免费免费搜索 | 久久男人视频 | 国产韩国日本高清视频 | 国产资源免费 | 六月婷婷久香在线视频 | 亚洲午夜在线视频 | 久久激情片 | 欧美日韩成人一区 | 国产精品第2页 | 狠狠狠的干 | 成人午夜在线观看 | 久久综合狠狠综合久久综合88 | 精品国模一区二区 | 国产精品一区二区三区电影 | 国产第一页在线播放 | 国产日产亚洲精华av | 国产女人18毛片水真多18精品 | 久久精品中文视频 | 日韩电影在线观看一区 | 91视频在线 | 91免费试看 | 亚洲一区二区三区四区精品 | 丁香资源影视免费观看 | 可以免费观看的av片 | 狠狠操狠狠插 | 亚洲欧美日韩国产精品一区午夜 | www.日日日.com | 欧美日韩不卡在线 | 波多野结衣网址 | 在线免费日韩 | 天天爽天天碰狠狠添 | 在线精品亚洲 | 久草在线视频在线观看 | 九九99视频 | 91中文字幕视频 | 日韩精品视频在线观看网址 | 亚洲天堂网在线视频观看 | 狠狠干婷婷 | 欧美精品久久久久久久久老牛影院 | 欧美美女一级片 | 成 人 黄 色 视频 免费观看 | 色九九在线 | 亚洲国产成人高清精品 | 色综合久久久久久久 | av电影在线免费观看 | 免费午夜网站 | 久久精品人人做人人综合老师 | 五月婷丁香 | 国产自偷自拍 | 四虎影视成人永久免费观看亚洲欧美 | 久久不卡免费视频 | 日韩天天干| 久久国产一区二区三区 | 日本中文在线播放 | 亚洲激情综合 | 国产精品毛片完整版 | 91热视频在线观看 | 国产又粗又硬又爽视频 | 久久久久国产精品厨房 | 久草在线免费资源站 | 中文字幕影片免费在线观看 | 在线观看视频你懂 | 欧美人交a欧美精品 | 日韩国产精品毛片 | 成人在线免费小视频 | 天天爽夜夜爽人人爽一区二区 | bbbb操bbbb| 色婷婷视频| 97视频成人| 亚洲精品乱码久久久久v最新版 | 久久黄色网| 91精品国产电影 | 欧美一区二区三区在线观看 | 在线观看91视频 | 日本视频精品 | 日韩欧美一级二级 | 亚洲一区动漫 | 欧美激情va永久在线播放 | 99在线观看视频网站 | 亚洲精品国偷拍自产在线观看蜜桃 | 久久精品网站视频 | 亚洲综合在线五月天 | 久久影视一区二区 | 天天综合狠狠精品 | 奇米影视999| 九九免费精品视频 | 久久久久99精品国产片 | 激情五月开心 | 色视频在线免费 | 丁香激情五月婷婷 | 不卡的av电影在线观看 | 黄色网免费| 国产九九九精品视频 | av福利资源 | 中文字幕乱码电影 | 精品99在线 | 正在播放亚洲精品 | 国产精品欧美久久久久天天影视 | 在线观看视频三级 | 国产99久久久久 | avhd高清在线谜片 | 男女全黄一级一级高潮免费看 | 国产一区二区精品久久 | 久久一本综合 | 在线观看电影av | 日韩 在线 | 久久久久久久免费观看 | 国产 色 | 在线免费黄网站 | 国产精品一区二区三区在线看 | 亚洲精品免费观看视频 | 亚洲日本三级 | 丁香六月天婷婷 | 香蕉视频网站在线观看 | 中文不卡视频在线 | 亚洲在线视频观看 | 国产色就色 | 超碰伊人网| 在线观看av大片 | 91视频久久久久 | 亚洲精品乱码久久久一二三 | 日日摸日日 | 91九色蝌蚪在线 | 97香蕉久久国产在线观看 |