久久精品国产精品国产精品污,男人扒开添女人下部免费视频,一级国产69式性姿势免费视频,夜鲁夜鲁很鲁在线视频 视频,欧美丰满少妇一区二区三区,国产偷国产偷亚洲高清人乐享,中文 在线 日韩 亚洲 欧美,熟妇人妻无乱码中文字幕真矢织江,一区二区三区人妻制服国产

歡迎訪問 生活随笔!

生活随笔

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

数据库

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

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

并發(fā) 20

P1:Java 內(nèi)存模型

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

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

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

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

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


P2:as-if-serial 和 happens-before 規(guī)則

as-if-serial

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

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

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

happens-before

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

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

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

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

區(qū)別

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


P3:指令重排序

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

  • 編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語義的前提下可以重排語句的執(zhí)行順序。
  • 指令級并行的重排序:如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機器指令的執(zhí)行順序。
  • 內(nèi)存系統(tǒng)的重排序:由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作操作看上去可能是亂序執(zhí)行。

從 Java 源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷編譯器優(yōu)化重排序、指令級并行重排序和內(nèi)存系統(tǒng)重排序,這些重排序可能會導致多線程程序出現(xiàn)內(nèi)存可見性問題。

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


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

原子性

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

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

可見性

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

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

有序性

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

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


P5:volatile 關(guān)鍵字

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

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

靜態(tài)變量 i 執(zhí)行多線程 i++ 的不安全問題

通過反編譯會發(fā)現(xiàn)一個自增語句是由 4 條字節(jié)碼指令構(gòu)成的,依次為getstatic、iconst_1、iadd、putstatic,當getstatic把 i 的值取到操作棧頂時,volatile保證了 i 的值在此刻是正確的,但是在執(zhí)行iconst_1、iadd這些指令時,其他線程可能已經(jīng)改變了i的值,而操作棧頂?shù)闹稻妥兂闪诉^期的數(shù)據(jù),所以 putstatic 指令執(zhí)行后就可能把較小的 i 值同步回了主內(nèi)存。

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

適用場景

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

變量不需要與其他狀態(tài)變量共同參與不變約束。

volatile的內(nèi)存語義

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

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

volatile指令重排序的特點

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

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

當?shù)谝粋€操作是volatile 寫,第二個操作是 volatile 讀時不能重排序。

JSR-133 增強 volatile 語義的原因

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


P6:final 關(guān)鍵字

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

JSR-133 增強 final語義的原因

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

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

寫 final 域重排序規(guī)則

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

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

讀 final 域重排序規(guī)則

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


P7:synchronized 關(guān)鍵字

每個 Java 對象都有一個關(guān)聯(lián)的 monitor 監(jiān)視器,使用 synchronized 時,JVM 會根據(jù) synchronized 的使用環(huán)境找到對應(yīng)對象的 monitor,再根據(jù) monitor 的狀態(tài)進行加、解鎖的判斷。如果成功加鎖就成為該 monitor 的唯一持有者,monitor 在被釋放前不能再被其他線程獲取。

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

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

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

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

不公平的原因

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

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


P8:鎖優(yōu)化

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

自旋鎖與自適應(yīng)自旋

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

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

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

鎖消除

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

鎖粗化

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

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

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

偏向鎖

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

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

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

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

輕量級鎖

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

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

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

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

偏向鎖、輕量級鎖和重量級鎖的區(qū)別

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

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

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


P9:Lock 接口

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

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

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

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

ReentrantLock 的可重入實現(xiàn)

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

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

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


P10:讀寫鎖

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

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

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

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

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

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

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


P11:AQS 隊列同步器

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

使用方式

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

和鎖的關(guān)系

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

同步隊列

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

兩種模式

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

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

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

獨占式的獲取和釋放流程

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

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

只有當前驅(qū)節(jié)點是頭節(jié)點時才能夠嘗試獲取同步狀態(tài)原因

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

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

共享式的獲取和釋放流程

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

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


P12:線程

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

生命周期

① NEW:新建狀態(tài),是線程被創(chuàng)建且未啟動的狀態(tài),此時還未調(diào)用 start 方法。

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

③ BLOCKED:阻塞狀態(tài),可能由于鎖被其他線程占用、調(diào)用了 sleep 或 join 方法、執(zhí)行了 wait 方法等。

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

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

⑥ TERMINATED:終止狀態(tài),表示當前線程已經(jīng)執(zhí)行完畢或異常退出。

實現(xiàn)方式

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

方法

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

守護線程

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


P13:線程間通信

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

volatile 和 synchronized 關(guān)鍵字

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

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

等待/通知機制

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

Thread.join

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

管道 IO 流

管道 IO 流主要用于線程之間的數(shù)據(jù)傳輸,傳輸?shù)拿浇闉閮?nèi)存。PipedOutputStream 和 PipedWriter 是管道輸出流,相當于生產(chǎn)者,PipedInputStream 和 PipedReader 是輸入流,相當于消費者。管道流使用一個循環(huán)緩沖數(shù)組來實現(xiàn),默認大小為 1024B。輸入流從這個緩沖數(shù)組中讀數(shù)據(jù),輸出流往這個緩沖數(shù)組中寫入數(shù)據(jù)。當數(shù)組已滿時,輸出流所在的線程將阻塞;當數(shù)組首次為空時,輸入流所在的線程將阻塞。

ThreadLocal

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


P14:ConcurrentHashMap

JDK 8 之前

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

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

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

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

JDK 8 開始

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

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

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


P15:CAS 操作

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

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

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


P16:原子操作類

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

原子更新基本類型

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

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

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

原子更新數(shù)組

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

原子更新引用

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

原子更新字段

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

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

JDK 8 更新的類

DoubleAccumulator 、 LongAccumulator、DoubleAdder、LongAdder、Striped64。


P17:并發(fā)工具類

等待多線程完成的 CountDownLatch

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

循環(huán)屏障 CyclicBarrier

循環(huán)屏障是基于同步到達某個點的信號量觸發(fā)機制,作用是讓一組線程到達一個屏障時被阻塞,直到最后一個線程到達屏障時,屏障才會解除,所有被攔截的線程才會繼續(xù)運行。構(gòu)造方法中的參數(shù)表示屏障攔截的線程數(shù)量,每個線程調(diào)用 await 方法告訴 CyclicBarrier 自己已到達屏障,然后當前線程被阻塞。還支持在構(gòu)造方法中傳入一個 Runable 類型的任務(wù),當線程到達屏障時會優(yōu)先執(zhí)行該任務(wù)。適用于多線程計算數(shù)據(jù),最后合并計算結(jié)果的應(yīng)用場景。

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

控制并發(fā)線程數(shù)的 Semaphore

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

線程間交換數(shù)據(jù)的 Exchanger

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


P18:線程池

作用

① 降低資源消耗,復(fù)用已創(chuàng)建的線程降低開銷、控制最大并發(fā)數(shù)。

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

③ 實現(xiàn)任務(wù)線程隊列緩沖策略和拒絕機制。

④ 實現(xiàn)某些與時間相關(guān)的功能,如定時執(zhí)行、周期執(zhí)行等。

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

① 核心線程池未滿,創(chuàng)建一個新的線程執(zhí)行任務(wù),此時 workCount < corePoolSize,這一步需要獲取全局鎖。

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

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

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

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

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

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

創(chuàng)建線程池

可以通過 Executors 的靜態(tài)工廠方法創(chuàng)建線程池,其核心方法有五個:

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

② newSingleThreadExecutor,使用單線程的線程池,相當于單線程串行執(zhí)行所有任務(wù),適用于需要保證順序執(zhí)行任務(wù)的應(yīng)用場景。

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

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

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

線程池的參數(shù)

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

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

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

④ unit:keepAliveTime 的時間單位。

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

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

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

線程池的狀態(tài)

① RUNNING:接受新的任務(wù),處理等待隊列中的任務(wù)。線程池被一旦被創(chuàng)建,就處于RUNNING狀態(tài)。

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

③ STOP:不接受新的任務(wù)提交,不再處理等待隊列中的任務(wù),中斷正在執(zhí)行任務(wù)的線程。對應(yīng) shutdownNow 方法。

④ TIDYING:當線程池在 SHUTDOWN 狀態(tài)下,阻塞隊列為空并且線程池中執(zhí)行的任務(wù)也為空時,就會變?yōu)?TIDYING。當線程池在STOP 狀態(tài)下,線程池中執(zhí)行的任務(wù)為空時,也會變?yōu)?TIDYING。

⑤ TERMINATED:線程池處于 TIDYING 狀態(tài)時,執(zhí)行完 terminated 方法后就會進入終止狀態(tài)。

關(guān)閉線程池

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

合理設(shè)置線程池

首先可以從以下角度分析:①任務(wù)的性質(zhì):CPU密集型任務(wù)、IO密集型任務(wù)和混合型任務(wù)。②任務(wù)的優(yōu)先級:高、中和低。③任務(wù)的執(zhí)行時間:長、中和短。④任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接。

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

優(yōu)先級不同的任務(wù)可以使用優(yōu)先級隊列 PriorityBlockingQueue 處理。

執(zhí)行時間不同的任務(wù)可以交給不同規(guī)模的線程池處理,或者使用優(yōu)先級隊列讓執(zhí)行時間短的任務(wù)先執(zhí)行。

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

建議使用有界隊列,能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力,可以根據(jù)需要設(shè)置的稍微大一些。


P19:阻塞隊列

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

Java 中的阻塞隊列

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

LinkedBlockingQueue,由鏈表結(jié)構(gòu)組成的有界阻塞隊列,隊列的默認和最大長度為 Integer 的最大值。

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

DelayQueue,支持延時獲取元素的無界阻塞隊列,使用優(yōu)先級隊列實現(xiàn)。創(chuàng)建元素時可以指定多久才能從隊列中獲取當前元素,只有延遲期滿時才能從隊列中獲取元素,適用于緩存系統(tǒng)和定時任務(wù)調(diào)度。

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

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

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

實現(xiàn)原理

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


P20:ThreadLoacl

ThreadLoacl 是線程變量,主要用于一個線程內(nèi),跨類、方法傳遞數(shù)據(jù)。ThreadLoacl 有一個靜態(tài)的內(nèi)部類 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);}

首先獲取當前線程,然后再獲取當前線程對應(yīng)的 ThreadLocalMap 類型的對象 map。如果 map 存在就直接設(shè)置值,key 是當前的 ThreadLocal 對象,value 就是傳入的參數(shù)。如果 map 不存在就通過 createMap 方法為當前線程創(chuàng)建一個 ThreadLocalMap 對象再設(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();}

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

remove 方法

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

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

存在的問題

線程復(fù)用會產(chǎn)生臟數(shù)據(jù),由于線程池會重用 Thread 對象,因此與 Thread 綁定的 ThreadLocal 也會被重用。如果沒有調(diào)用 remove 清理與線程相關(guān)的 ThreadLocal 信息,那么假如下一個線程沒有調(diào)用 set 設(shè)置初始值就可能 get 到重用的線程信息。

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


Spring 10

P1:Spring 框架

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

好處

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

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

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

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

核心容器

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

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

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

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


P2:IoC 控制反轉(zhuǎn)

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

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

基于 XML 的容器初始化

當創(chuàng)建一個 ClassPathXmlApplicationContext 時,構(gòu)造方法做了兩件事:首先調(diào)用父容器的構(gòu)造方法為容器設(shè)置好 Bean 資源加載器,然后調(diào)用父類的 setConfigLocations 方法設(shè)置 Bean 配置信息的定位路徑。

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

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

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

基于注解的容器初始化

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


P3:DI 依賴注入

可注入的數(shù)據(jù)類型

基本數(shù)據(jù)類型和 String、集合類型、Bean 類型。

實現(xiàn)方式

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

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

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

相關(guān)注解

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

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

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

@Value :用于注入基本數(shù)據(jù)類型和 String 類型。

依賴注入的過程

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

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

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

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

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

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


P4:Bean 對象

生命周期

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

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

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

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

作用范圍

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

創(chuàng)建方式

XML

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

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

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

注解

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

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

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

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

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

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

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

BeanFactory、FactoryBean 和 ApplicationContext 的區(qū)別

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

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

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


P5:AOP 面向切面編程

概念和原理

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

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

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

常用場景包括權(quán)限認證、自動緩存、錯誤處理、日志、調(diào)試和事務(wù)等。

相關(guān)注解

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

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

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

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

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

相關(guān)術(shù)語

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

Joinpoint:連接點,程序執(zhí)行過程中的某一行為,即業(yè)務(wù)層中的所有方法。。

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

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

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

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

Weaving :織入,指把增強應(yīng)用到目標對象來創(chuàng)建代理對象的過程。

AOP 的過程

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

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


P6:Spring MVC 核心組件

DispatcherServlet:SpringMVC 中的前端控制器,是整個流程控制的核心,負責接收請求并轉(zhuǎn)發(fā)給對應(yīng)的處理組件。

Handler:處理器,完成具體業(yè)務(wù)邏輯,相當于 Servlet 或 Action。

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

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

HandlerExecutionChain:處理器執(zhí)行鏈,包括兩部分內(nèi)容:Handler 和 HandlerInterceptor。

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

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

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


P7:Spring MVC 處理流程

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

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

之后 DispatcherServlet 使用 Handler 找到對應(yīng)的 HandlerApapter,通過 HandlerApapter 調(diào)用 Handler 的方法,將請求參數(shù)綁定到方法的形參上,執(zhí)行方法處理請求并得到 ModelAndView。

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

注解

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

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

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

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


P8:Spring Data JPA 框架

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 規(guī)范的基礎(chǔ)上封裝的一套 JPA 應(yīng)用框架,可使開發(fā)者用極簡的代碼實現(xiàn)對數(shù)據(jù)庫的訪問和操作。它提供了包括增刪改查等在內(nèi)的常用功能,且易于擴展,可以極大提高開發(fā)效率。

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

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

注解

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

@Table :關(guān)聯(lián)實體類和數(shù)據(jù)庫表。

@Column :關(guān)聯(lián)實體類屬性和數(shù)據(jù)庫表中字段。

@Id :聲明當前屬性為數(shù)據(jù)庫表主鍵對應(yīng)的屬性。

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

@OneToMany :配置一對多關(guān)系,mappedBy 屬性值為主表實體類在從表實體類中對應(yīng)的屬性名。

@ManyToOne :配置多對一關(guān)系,targetEntity 屬性值為主表對應(yīng)實體類的字節(jié)碼。

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

對象導航查詢

通過 get 方法查詢一個對象的同時,通過此對象可以查詢它的關(guān)聯(lián)對象。

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

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

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


P9:Mybatis 框架

Mybatis 是一個實現(xiàn)了數(shù)據(jù)持久化的 ORM 框架,簡單理解就是對 JDBC 進行了封裝。

優(yōu)點

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

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

提供 XML 標簽,支持編寫動態(tài) SQL 語句。

提供映射標簽,支持對象與數(shù)據(jù)庫的 ORM 字段映射關(guān)系。

缺點

SQL 語句編寫工作量較大,尤其是字段和關(guān)聯(lián)表多時。

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

映射文件標簽

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

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

resultType 表示返回的結(jié)果類型,包括基本數(shù)據(jù)類型和對應(yīng)的包裝類型、String 和 Java Bean 類型。還可以使用把返回結(jié)果封裝為復(fù)雜類型的 resultMap 。

緩存

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

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

P10:Spring Cloud 框架

單體應(yīng)用存在的問題

隨著業(yè)務(wù)發(fā)展,開發(fā)越來越復(fù)雜。

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

一個模塊出現(xiàn)問題,可能導致整個系統(tǒng)崩潰。

多個開發(fā)團隊同時對數(shù)據(jù)進行管理,容易產(chǎn)生安全漏洞。

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

分布式和集群的區(qū)別

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

分布式:將一個復(fù)雜的問題拆分成若干簡單的小問題,將一個大型的項目架構(gòu)拆分成若干個微服務(wù)來協(xié)同完成,在軟件設(shè)計層面解決問題。

微服務(wù)的優(yōu)點

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

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

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

服務(wù)治理 Eureka

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

服務(wù)注冊:在分布式系統(tǒng)架構(gòu)中,每個微服務(wù)在啟動時,將自己的信息存儲在注冊中心。

服務(wù)發(fā)現(xiàn):服務(wù)消費者從注冊中心獲取服務(wù)提供者的網(wǎng)絡(luò)信息,通過該信息調(diào)用服務(wù)。

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

服務(wù)網(wǎng)關(guān) Zuul

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

負載均衡 Ribbon

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

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

聲明式接口調(diào)用 Feign

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

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

服務(wù)熔斷 Hystrix

熔斷器的作用是在不改變各個微服務(wù)調(diào)用關(guān)系的前提下,針對錯誤情況進行預(yù)先處理。

設(shè)計原則:服務(wù)隔離機制、服務(wù)降級機制、熔斷機制、提供實時監(jiān)控和報警功能和提供實時配置修改功能

Hystrix 數(shù)據(jù)監(jiān)控需要結(jié)合 Spring Boot Actuator 使用,Actuator 提供了對服務(wù)的數(shù)據(jù)監(jiān)控、數(shù)據(jù)統(tǒng)計,可以通過 hystirx-stream 節(jié)點獲取監(jiān)控的請求數(shù)據(jù),同時提供了可視化監(jiān)控界面。

服務(wù)配置 Config

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

服務(wù)跟蹤 Zipkin

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


MySQL 15

P1:邏輯架構(gòu)

第一層是服務(wù)器層,主要提供連接處理、授權(quán)認證、安全等功能,該層的服務(wù)不是 MySQL 獨有的,大多數(shù)基于網(wǎng)絡(luò)的 C/S 服務(wù)都有類似架構(gòu)。

第二層實現(xiàn)了 MySQL 核心服務(wù)功能,包括查詢解析、分析、優(yōu)化、緩存以及日期和時間等所有內(nèi)置函數(shù),所有跨存儲引擎的功能都在這一層實現(xiàn),例如存儲過程、觸發(fā)器、視圖等。

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


P2:鎖

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

讀寫鎖

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

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

鎖策略

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

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

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

死鎖

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

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

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

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


P3:事務(wù)

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

ACID 特性

一個運行良好的事務(wù)處理系統(tǒng)必須具備 ACID 特性,實現(xiàn)了 ACID 的數(shù)據(jù)庫需要更強的CPU處理能力、更大的內(nèi)存和磁盤空間。

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

隔離級別

在 SQL 標準中定義了四種隔離級別,每一種隔離級別都規(guī)定了一個事務(wù)中所做的修改,哪些在事務(wù)內(nèi)和事務(wù)間是可見的,哪些是不可見的。較低級別的隔離通常可以執(zhí)行更高的并發(fā),系統(tǒng)的開銷也更低。

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

MySQL 中的事務(wù)

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

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

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

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

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


P4:MVCC 多版本并發(fā)控制

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

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

不同的存儲引擎的 MVCC 實現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制。

InnoDB 的 MVCC 實現(xiàn)

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

REPEATABLE READ 級別下 MVCC 的具體實現(xiàn)

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

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

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

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

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

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

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


P5:InnoDB 存儲引擎

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

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

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

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

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

InnoDB 內(nèi)部做了很多優(yōu)化,包括從磁盤讀取數(shù)據(jù)時采用的可預(yù)測性預(yù)讀,能夠自動在內(nèi)存中創(chuàng)建加速讀操作的自適應(yīng)哈希索引,以及能夠加速插入操作的插入緩沖區(qū)等。

選擇合適的存儲引擎

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

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

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


P6:MyISAM 存儲引擎

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

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

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

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

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

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

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


P7:Memory 存儲引擎

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

Memory 表適合的場景:查找或者映射表、緩存周期性聚合數(shù)據(jù)的結(jié)果、保存數(shù)據(jù)分析中產(chǎn)生的中間數(shù)據(jù)。

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

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


P8:數(shù)據(jù)類型

整數(shù)類型

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

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

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

實數(shù)類型

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

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

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

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

VARCHAR

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

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

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

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

CHAR

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

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

BLOB 和 TEXT 類型

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

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

DATETIME

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

TIMESTAMP

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


P9:索引的分類

索引在也叫做鍵,是存儲引擎用于快速找到記錄的一種數(shù)據(jù)結(jié)構(gòu)。索引對于良好的性能很關(guān)鍵,尤其是當表中數(shù)據(jù)量越來越大時,索引對性能的影響愈發(fā)重要。在數(shù)據(jù)量較小且負載較低時,不恰當?shù)乃饕龑π阅艿挠绊懣赡苓€不明顯,但數(shù)據(jù)量逐漸增大時,性能會急劇下降。

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

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

B-Tree 索引

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

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

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

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

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

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

B-Tree索引的限制:

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

B-Tree 和 B+Tree 的區(qū)別:

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

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


哈希索引

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

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

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

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

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

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


空間索引

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


全文索引

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

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

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


聚簇索引

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

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

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


覆蓋索引

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

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


P10:索引使用原則

建立索引

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

使用前綴索引

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

選擇合適的索引順序

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

刪除無用索引

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

減少碎片

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

索引失效情況

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

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

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

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

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

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

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


P11:優(yōu)化數(shù)據(jù)類型

更小的通常更好

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

盡可能簡單

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

盡量避免 NULL

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

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

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


P12:優(yōu)化查詢概述

優(yōu)化數(shù)據(jù)訪問

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

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

MySQL 是否在掃描額外的記錄:在確定查詢只返回需要的數(shù)據(jù)后,應(yīng)該看看查詢?yōu)榱朔祷亟Y(jié)果是否掃描了過多的數(shù)據(jù),最簡單的三個衡量指標時響應(yīng)時間、掃描的行數(shù)和返回的行數(shù)。如果發(fā)現(xiàn)查詢需要掃描大量數(shù)據(jù)但只返回少數(shù)的行,可以使用以下手動優(yōu)化:① 使用覆蓋索引掃描,把所有需要用的列都放到索引中,這樣存儲引擎無需回表查詢對應(yīng)行就可以返回結(jié)果。② 改變庫表結(jié)構(gòu)。 ③ 重寫這個復(fù)雜的查詢,讓 MySQL 優(yōu)化器能夠以更優(yōu)化的方式執(zhí)行這個查詢。

重構(gòu)查詢方式

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

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

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


P13:查詢執(zhí)行流程

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

查詢緩存

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

查詢優(yōu)化處理

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

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

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

查詢執(zhí)行引擎

在解析和優(yōu)化階段,MySQL 將生成查詢對應(yīng)的執(zhí)行計劃,MySQL 的查詢執(zhí)行引擎則根據(jù)這個計劃來完成整個查詢。執(zhí)行計劃是一個數(shù)據(jù)結(jié)構(gòu),而不是其他關(guān)系型數(shù)據(jù)庫那樣會生成對應(yīng)的字節(jié)碼。查詢執(zhí)行階段并不復(fù)雜,MySQL 只是簡單的根據(jù)執(zhí)行計劃給出的指令逐步執(zhí)行,再根據(jù)執(zhí)行計劃執(zhí)行的過程中,有大量操作需要通過調(diào)用存儲引擎實現(xiàn)的接口來完成。

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

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


P14:優(yōu)化 SQL

定位低效 SQL

可以通過兩種方式來定位執(zhí)行效率較低的 SQL 語句。一種是通過慢查詢?nèi)罩径ㄎ?#xff0c;可以通過慢查詢?nèi)罩径ㄎ荒切┮呀?jīng)執(zhí)行完畢的 SQL 語句。另一種是使用 SHOW PROCESSLIST 查詢,慢查詢?nèi)罩驹诓樵兘Y(jié)束以后才記錄,所以在應(yīng)用反應(yīng)執(zhí)行效率出現(xiàn)問題的時候查詢慢查詢?nèi)罩静荒芏ㄎ粏栴},此時可以使用 SHOW PROCESSLIST 命令查看當前 MySQL 正在進行的線程,包括線程的狀態(tài)、是否鎖表等,可以實時查看 SQL 的執(zhí)行情況,同時對一些鎖表操作進行優(yōu)化。找到執(zhí)行效率低的 SQL 語句后,就可以通過 SHOW PROFILE、EXPLAIN 或 trace 等豐富來繼續(xù)優(yōu)化語句。

SHOW PROFILE

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

TRACE

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

EXPLAIN

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

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

優(yōu)化 COUNT 查詢

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

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

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

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

優(yōu)化關(guān)聯(lián)查詢

確保 ON 或 USING 子句中的列上有索引,在創(chuàng)建索引時就要考慮到關(guān)聯(lián)的順序。

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

在 MySQL 5.5 及以下版本盡量避免子查詢,可以用關(guān)聯(lián)查詢代替,因為執(zhí)行器會先執(zhí)行外部的 SQL 再執(zhí)行內(nèi)部的 SQL。

優(yōu)化 GROUP BY

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

優(yōu)化 LIMIT 分頁

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

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

優(yōu)化 UNION 查詢

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

使用用戶自定義變量

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

優(yōu)化 INSERT

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


P15:復(fù)制

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

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

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

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

復(fù)制步驟

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

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

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

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

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


Redis 10

P1:特點

基于鍵值對的數(shù)據(jù)結(jié)構(gòu)服務(wù)器

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

豐富的功能

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

簡單穩(wěn)定

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

客戶端語言多

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

持久化

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

數(shù)據(jù)結(jié)構(gòu)和內(nèi)部編碼

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

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

高性能

Redis 使用了單線程架構(gòu)和 IO 多路復(fù)用模型來實現(xiàn)高性能的內(nèi)存數(shù)據(jù)庫服務(wù)。

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

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


P2:字符串

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

設(shè)置值

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

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

獲取值

get key,如果不存在返回 nil

批量設(shè)置值

mset key value [key value…]

批量獲取值

mget key [key…]

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

計數(shù)

incr key

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

內(nèi)部編碼

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

典型使用場景

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

P3:哈希

哈希類型是指鍵值本身又是一個鍵值對結(jié)構(gòu),哈希類型中的映射關(guān)系叫做 field-value,這里的 value 是指 field 對于的值而不是鍵對于的值。

設(shè)置值

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

獲取值

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

刪除 field

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

計算 field 個數(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 壓縮列表:當哈希類型元素個數(shù)和值小于配置值(默認 512 個和 64 字節(jié))時會使用 ziplist 作為內(nèi)部實現(xiàn),使用更緊湊的結(jié)構(gòu)實現(xiàn)多個元素的連續(xù)存儲,在節(jié)省內(nèi)存方面比 hashtable 更優(yōu)秀。
  • hashtable 哈希表:當哈希類型無法滿足 ziplist 的條件時會使用 hashtable 作為哈希的內(nèi)部實現(xiàn),因為此時 ziplist 的讀寫效率會下降,而 hashtable 的讀寫時間復(fù)雜度都為 O(1)。

使用場景

緩存用戶信息,有三種實現(xiàn):

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

P4:列表

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

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

添加操作

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

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

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

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

查找

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

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

獲取列表長度:llen key

刪除

從列表左側(cè)彈出元素:lpop key

從列表右側(cè)彈出元素: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會從左至右遍歷鍵,一旦有一個鍵能彈出元素,客戶端立即返回。

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

內(nèi)部編碼

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

Redis 3.2 提供了 quicklist 內(nèi)部編碼,它是以一個 ziplist 為節(jié)點的 linkedlist,它結(jié)合了兩者的優(yōu)勢,為列表類提供了一種更為優(yōu)秀的內(nèi)部編碼實現(xiàn)。

使用場景

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

lpush + lpop = 棧、lpush + rpop = 隊列、lpush + ltrim = 優(yōu)先集合、lpush + brpop = 消息隊列


P5:集合

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

添加元素

sadd key element [element…],返回結(jié)果為添加成功的元素個數(shù)。

刪除元素

srem key element [element…],返回結(jié)果為成功刪除的元素個數(shù)。

計算元素個數(shù)

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

判斷元素是否在集合中

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

隨機從集合返回指定個數(shù)個元素

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

從集合隨機彈出元素

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

獲取所有元素

smembers key

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

sinter key [key…]

sunion key [key…]

sdiff key [key…]

保存交集、并集、差集的結(jié)果

sinterstore/sunionstore/sdiffstore destination key [key…]

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

內(nèi)部編碼

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

使用場景

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

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


P6:有序集合

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

數(shù)據(jù)結(jié)構(gòu) 是否允許元素重復(fù) 是否有序 有序?qū)崿F(xiàn)方式 應(yīng)用場景
列表 是 是 下標 時間軸,消息隊列
集合 否 否 / 標簽,社交
有序集合 否 是 分值 排行榜,社交

添加成員

zadd key score member [score member…],返回結(jié)果是成功添加成員的個數(shù)

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

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

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

計算成員個數(shù)

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

計算某個成員的分數(shù)

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

計算成員排名

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

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

刪除成員

zrem key member [member…],返回結(jié)果是成功刪除的個數(shù)。

增加成員的分數(shù)

zincrby key increment member

返回指定排名范圍的成員

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

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

返回指定分數(shù)范圍的成員

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

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

返回指定分數(shù)范圍成員個數(shù)

zcount key min max

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

內(nèi)部編碼

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

使用場景

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


P7:鍵和數(shù)據(jù)庫管理

鍵重命名

rename key newkey

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

隨機返回一個鍵

random key

鍵過期

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

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

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

鍵遷移

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

切換數(shù)據(jù)庫

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

flushdb/flushall

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


P8:發(fā)布訂閱

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

發(fā)布消息

publish channel message,返回結(jié)果為訂閱者的個數(shù)。

訂閱消息

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

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

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

取消訂閱

unsubscribe [channel [channel…]]

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

按照模式訂閱和取消訂閱

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


P9:RDB 持久化

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

觸發(fā)機制

手動觸發(fā)分別對應(yīng) save 和 bgsave 命令:

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

除了手動觸發(fā)外,Redis 內(nèi)部還存在自動觸發(fā) RDB 的持久化機制,例如:

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

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

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

② 父進程執(zhí)行 fork 操作創(chuàng)建子進程,fork 操作過程中父進程會阻塞。

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

④ 子進程創(chuàng)建 RDB 文件,根據(jù)父進程內(nèi)存生成臨時快照文件,完成后對原有文件進行原子替換。

⑤ 進程發(fā)送信號給父進程表示完成,父進程更新統(tǒng)計信息。

優(yōu)點

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

Redis 加載 RDB 恢復(fù)數(shù)據(jù)遠遠快于 AOF 的方式。

缺點

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

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


P10:AOF 持久化

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

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

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

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

命令寫入

AOF 命令寫入的內(nèi)容直接是文本協(xié)議格式,采用文本協(xié)議格式的原因:

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

AOF 把命令追加到緩沖區(qū)的原因:

Redis 使用單線程響應(yīng)命令,如果每次寫 AOF 文件命令都直接追加到硬盤,那么性能完全取決于當前硬盤負載。先寫入緩沖區(qū)中還有另一個好處,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)用一次。是建議的策略,也是默認配置,兼顧性能和數(shù)據(jù)安全。
  • no:命令寫入緩沖區(qū)后調(diào)用系統(tǒng) write 操作,不對 AOF 文件做 fsync 同步,同步硬盤操作由操作系統(tǒng)負責,周期通常最長 30 秒。由于操作系統(tǒng)每次同步 AOF 文件的周期不可控,而且會加大每次同步硬盤的數(shù)據(jù)量,雖然提升了性能,但安全性無法保證。

文件重寫

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

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

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

AOF 重寫分為手動觸發(fā)和自動觸發(fā),手動觸發(fā)直接調(diào)用 bgrewriteaof 命令,自動觸發(fā)根據(jù) auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 參數(shù)確定自動觸發(fā)時機。

重寫流程:

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

② 父進程執(zhí)行 fork 創(chuàng)建子進程,開銷等同于 bgsave 過程。

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

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

⑤ 新 AOF 文件寫入完成后,子進程發(fā)送信號給父進程,父進程更新統(tǒng)計信息。

⑥ 父進程把 AOF 重寫緩沖區(qū)的數(shù)據(jù)寫入到新的 AOF 文件并替換舊文件,完成重寫。


重啟加載

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

① AOF 持久化開啟且存在 AOF 文件時,優(yōu)先加載 AOF 文件。

② AOF 關(guān)閉時且存在 RDB 文件時,記載 RDB 文件。

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

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


總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

国产av无码专区亚洲a∨毛片 | 一本色道婷婷久久欧美 | 欧美熟妇另类久久久久久不卡 | 久久99精品国产麻豆蜜芽 | 欧美老妇与禽交 | 色婷婷av一区二区三区之红樱桃 | 久久久久久久女国产乱让韩 | 亚洲中文字幕无码中字 | 亚洲日韩av一区二区三区四区 | 亚洲精品成人福利网站 | 人妻天天爽夜夜爽一区二区 | 99久久精品无码一区二区毛片 | 日本熟妇大屁股人妻 | 国产人妻人伦精品1国产丝袜 | 99久久精品午夜一区二区 | 婷婷综合久久中文字幕蜜桃三电影 | 麻豆国产人妻欲求不满 | 全球成人中文在线 | 一本大道久久东京热无码av | 欧美性黑人极品hd | 成人影院yy111111在线观看 | 亚洲人成影院在线无码按摩店 | 久久精品中文字幕大胸 | 98国产精品综合一区二区三区 | 大乳丰满人妻中文字幕日本 | 水蜜桃亚洲一二三四在线 | 日韩精品无码一区二区中文字幕 | 无码国模国产在线观看 | 西西人体www44rt大胆高清 | 激情亚洲一区国产精品 | 国产网红无码精品视频 | 无码国产乱人伦偷精品视频 | 永久免费精品精品永久-夜色 | 全黄性性激高免费视频 | 国产精品久久久久久久影院 | 黑人玩弄人妻中文在线 | 久久久国产一区二区三区 | 免费观看激色视频网站 | 亚洲中文字幕av在天堂 | 青青青爽视频在线观看 | 国产成人无码a区在线观看视频app | 中文字幕人妻无码一区二区三区 | 国产激情一区二区三区 | 亚洲熟妇自偷自拍另类 | 国产人妻精品一区二区三区不卡 | 亚洲s色大片在线观看 | 久久精品国产大片免费观看 | 国产精品久久久久9999小说 | 九九久久精品国产免费看小说 | 久久国产36精品色熟妇 | 欧美一区二区三区视频在线观看 | 国产人妻大战黑人第1集 | 久久综合九色综合欧美狠狠 | 狂野欧美性猛xxxx乱大交 | 成人欧美一区二区三区黑人 | 日韩 欧美 动漫 国产 制服 | 日日橹狠狠爱欧美视频 | 男女爱爱好爽视频免费看 | 亚洲国产精品久久久天堂 | 激情亚洲一区国产精品 | 图片小说视频一区二区 | 欧美日韩一区二区三区自拍 | 女高中生第一次破苞av | 青青青爽视频在线观看 | 国产精华av午夜在线观看 | 爽爽影院免费观看 | 日日橹狠狠爱欧美视频 | 特大黑人娇小亚洲女 | 午夜男女很黄的视频 | www国产亚洲精品久久久日本 | 久久精品女人的天堂av | 成 人 免费观看网站 | 丰满人妻一区二区三区免费视频 | 一个人免费观看的www视频 | 欧美变态另类xxxx | 中文字幕 亚洲精品 第1页 | 天干天干啦夜天干天2017 | 精品国产麻豆免费人成网站 | 国产九九九九九九九a片 | 精品久久久久久亚洲精品 | 国产一精品一av一免费 | a片免费视频在线观看 | 亚洲精品综合一区二区三区在线 | 久久精品女人天堂av免费观看 | 久久久久免费精品国产 | 欧美丰满熟妇xxxx性ppx人交 | 婷婷丁香六月激情综合啪 | 亚洲毛片av日韩av无码 | 最近的中文字幕在线看视频 | 久久无码专区国产精品s | 国产午夜无码精品免费看 | 国产亚洲视频中文字幕97精品 | 日日鲁鲁鲁夜夜爽爽狠狠 | 日产精品99久久久久久 | 色妞www精品免费视频 | 亚洲精品美女久久久久久久 | 欧美人与牲动交xxxx | 免费观看的无遮挡av | 亚洲色在线无码国产精品不卡 | √天堂资源地址中文在线 | 玩弄少妇高潮ⅹxxxyw | 久久久国产一区二区三区 | 超碰97人人做人人爱少妇 | 色综合久久久久综合一本到桃花网 | 极品尤物被啪到呻吟喷水 | 日韩精品无码一区二区中文字幕 | 中文字幕av无码一区二区三区电影 | 四虎4hu永久免费 | 日本xxxx色视频在线观看免费 | 又大又硬又黄的免费视频 | 欧美zoozzooz性欧美 | 国产在线精品一区二区三区直播 | 欧美激情内射喷水高潮 | 3d动漫精品啪啪一区二区中 | 午夜男女很黄的视频 | 亚洲啪av永久无码精品放毛片 | 亚洲天堂2017无码中文 | 蜜桃无码一区二区三区 | 亚洲中文字幕无码中文字在线 | 国产av剧情md精品麻豆 | 亚洲爆乳无码专区 | 精品无码国产自产拍在线观看蜜 | 久久国内精品自在自线 | 久久国产精品精品国产色婷婷 | 国产精品久久久久久久影院 | 少妇被粗大的猛进出69影院 | 无码乱肉视频免费大全合集 | 中文字幕无码人妻少妇免费 | 国产人妻精品一区二区三区 | 亚洲国产精品一区二区美利坚 | 99国产精品白浆在线观看免费 | 精品国产成人一区二区三区 | 无码av最新清无码专区吞精 | 男人的天堂2018无码 | 国产一精品一av一免费 | 好男人www社区 | 2020久久超碰国产精品最新 | 玩弄少妇高潮ⅹxxxyw | 在线视频网站www色 | 国产亚洲人成a在线v网站 | 老头边吃奶边弄进去呻吟 | 麻豆精品国产精华精华液好用吗 | 国产精品.xx视频.xxtv | 任你躁在线精品免费 | 日韩精品无码一本二本三本色 | 久久熟妇人妻午夜寂寞影院 | 日本一卡2卡3卡四卡精品网站 | 中文字幕无码乱人伦 | 国产黄在线观看免费观看不卡 | 麻豆av传媒蜜桃天美传媒 | 成人欧美一区二区三区黑人 | 成年美女黄网站色大免费视频 | 国产xxx69麻豆国语对白 | 欧美丰满熟妇xxxx | 欧美老人巨大xxxx做受 | 日本护士毛茸茸高潮 | 牲欲强的熟妇农村老妇女视频 | 在线播放免费人成毛片乱码 | 午夜无码人妻av大片色欲 | 国产精品亚洲а∨无码播放麻豆 | 一本久道久久综合狠狠爱 | 给我免费的视频在线观看 | 亚洲欧洲日本综合aⅴ在线 | 人人妻人人澡人人爽人人精品浪潮 | 精品国产av色一区二区深夜久久 | 黑人玩弄人妻中文在线 | 一区二区三区高清视频一 | ass日本丰满熟妇pics | 久久99国产综合精品 | 精品久久8x国产免费观看 | 午夜精品久久久内射近拍高清 | www成人国产高清内射 | 成在人线av无码免费 | 老太婆性杂交欧美肥老太 | 中文久久乱码一区二区 | 欧美性色19p | 国产乱码精品一品二品 | 人妻少妇精品无码专区二区 | 特级做a爰片毛片免费69 | aa片在线观看视频在线播放 | 日韩亚洲欧美中文高清在线 | 特黄特色大片免费播放器图片 | a片在线免费观看 | 亚洲色欲色欲欲www在线 | 国产精品久久久av久久久 | 人人爽人人澡人人高潮 | 中文字幕中文有码在线 | 男女猛烈xx00免费视频试看 | 免费人成网站视频在线观看 | 一个人看的www免费视频在线观看 | a片免费视频在线观看 | 女人被爽到呻吟gif动态图视看 | 亚洲狠狠色丁香婷婷综合 | 亚洲中文字幕乱码av波多ji | 国産精品久久久久久久 | 国产成人精品一区二区在线小狼 | 亚洲a无码综合a国产av中文 | 国产成人无码a区在线观看视频app | 久久久精品欧美一区二区免费 | 国产真实乱对白精彩久久 | 国产成人无码专区 | 97精品国产97久久久久久免费 | 狠狠噜狠狠狠狠丁香五月 | 东京一本一道一二三区 | 亚洲成av人在线观看网址 | 国产精品亚洲一区二区三区喷水 | 国产精品久久久久7777 | 久久精品人人做人人综合试看 | 一本色道久久综合亚洲精品不卡 | 亚洲人成人无码网www国产 | 亚洲人成人无码网www国产 | 国产精品久久久久影院嫩草 | 无码人妻久久一区二区三区不卡 | 成人亚洲精品久久久久软件 | 一个人免费观看的www视频 | 蜜桃臀无码内射一区二区三区 | 大地资源中文第3页 | 99久久精品国产一区二区蜜芽 | 伊人久久大香线焦av综合影院 | 国产无遮挡吃胸膜奶免费看 | 欧美黑人性暴力猛交喷水 | 免费观看的无遮挡av | 特大黑人娇小亚洲女 | 成人精品天堂一区二区三区 | 久久亚洲日韩精品一区二区三区 | 亚洲 另类 在线 欧美 制服 | 在线看片无码永久免费视频 | 久久熟妇人妻午夜寂寞影院 | 日韩人妻系列无码专区 | 久久久久久久女国产乱让韩 | 我要看www免费看插插视频 | 无码人妻av免费一区二区三区 | 在线a亚洲视频播放在线观看 | 国产免费观看黄av片 | 亚洲国产日韩a在线播放 | 国产在线aaa片一区二区99 | 日日干夜夜干 | 精品国产福利一区二区 | 欧美兽交xxxx×视频 | 久久久无码中文字幕久... | 欧美激情综合亚洲一二区 | 亚洲成av人影院在线观看 | 无码毛片视频一区二区本码 | 天海翼激烈高潮到腰振不止 | 日本在线高清不卡免费播放 | 亚洲国产成人a精品不卡在线 | 宝宝好涨水快流出来免费视频 | 成人av无码一区二区三区 | 欧美亚洲国产一区二区三区 | 国产成人一区二区三区在线观看 | 欧美日韩一区二区综合 | 久久亚洲a片com人成 | 亚洲日韩av一区二区三区中文 | 日韩无套无码精品 | 免费观看又污又黄的网站 | 亚洲中文字幕无码中文字在线 | 婷婷六月久久综合丁香 | 在线天堂新版最新版在线8 | 亚洲综合无码一区二区三区 | 午夜精品一区二区三区在线观看 | 国产内射老熟女aaaa | 色婷婷综合激情综在线播放 | 国产精品久久久久9999小说 | 野外少妇愉情中文字幕 | 中文字幕乱码中文乱码51精品 | 国内精品人妻无码久久久影院蜜桃 | 久久久久久国产精品无码下载 | 少妇无码吹潮 | 国产精品久久国产三级国 | 久久亚洲a片com人成 | 捆绑白丝粉色jk震动捧喷白浆 | 十八禁真人啪啪免费网站 | 久久久久久九九精品久 | 国产无套内射久久久国产 | 国产两女互慰高潮视频在线观看 | 欧美喷潮久久久xxxxx | 久久久久久久女国产乱让韩 | 亚洲另类伦春色综合小说 | 国产成人综合色在线观看网站 | 国产真人无遮挡作爱免费视频 | 一本色道久久综合狠狠躁 | 中文字幕人成乱码熟女app | 欧美精品在线观看 | 狠狠躁日日躁夜夜躁2020 | 在线播放免费人成毛片乱码 | 国产精品内射视频免费 | 99久久婷婷国产综合精品青草免费 | 老熟妇乱子伦牲交视频 | 伊人久久婷婷五月综合97色 | 亚洲熟妇自偷自拍另类 | 亚洲精品成人av在线 | 国产香蕉尹人综合在线观看 | 久久无码中文字幕免费影院蜜桃 | 欧美日韩人成综合在线播放 | 东京热无码av男人的天堂 | 国产精品无码mv在线观看 | 午夜不卡av免费 一本久久a久久精品vr综合 | 亚洲爆乳精品无码一区二区三区 | 久久亚洲日韩精品一区二区三区 | 午夜肉伦伦影院 | 亚洲精品一区三区三区在线观看 | 久久久国产一区二区三区 | 国产乱人偷精品人妻a片 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 色婷婷香蕉在线一区二区 | 性生交大片免费看l | 国产午夜视频在线观看 | 国产免费观看黄av片 | 久久精品成人欧美大片 | 久久精品国产一区二区三区肥胖 | 日本一卡二卡不卡视频查询 | 性生交片免费无码看人 | 伊人色综合久久天天小片 | 久久久国产一区二区三区 | 亚洲精品一区二区三区在线观看 | 美女极度色诱视频国产 | 少妇久久久久久人妻无码 | 日韩 欧美 动漫 国产 制服 | 亚洲一区二区三区国产精华液 | 夜精品a片一区二区三区无码白浆 | 真人与拘做受免费视频 | 精品无码国产自产拍在线观看蜜 | 欧美日韩亚洲国产精品 | 内射巨臀欧美在线视频 | 国产农村乱对白刺激视频 | 日产国产精品亚洲系列 | 国产口爆吞精在线视频 | 国产日产欧产精品精品app | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 欧美老妇交乱视频在线观看 | 少妇激情av一区二区 | 亚洲国产精品美女久久久久 | 永久免费精品精品永久-夜色 | 亚洲欧美国产精品久久 | 又大又黄又粗又爽的免费视频 | 国产在线一区二区三区四区五区 | 蜜臀av无码人妻精品 | 丰满诱人的人妻3 | 国产精品理论片在线观看 | 国产亚洲人成a在线v网站 | 麻豆md0077饥渴少妇 | 亚洲中文字幕无码一久久区 | 女人被男人躁得好爽免费视频 | 精品无人国产偷自产在线 | 欧美国产日韩亚洲中文 | 亚洲 高清 成人 动漫 | 国产精品高潮呻吟av久久4虎 | 亚洲中文字幕va福利 | 亚洲精品中文字幕 | 高潮喷水的毛片 | 婷婷五月综合激情中文字幕 | 久久久国产精品无码免费专区 | 亚洲毛片av日韩av无码 | 久久亚洲精品中文字幕无男同 | 亚洲国产欧美国产综合一区 | 鲁大师影院在线观看 | 色综合天天综合狠狠爱 | 黑人巨大精品欧美黑寡妇 | 精品国产福利一区二区 | 国产av剧情md精品麻豆 | 东京热一精品无码av | 一本久久a久久精品亚洲 | 欧美性猛交内射兽交老熟妇 | 久久综合九色综合97网 | 精品人妻人人做人人爽夜夜爽 | 色狠狠av一区二区三区 | 精品乱码久久久久久久 | 一本无码人妻在中文字幕免费 | 国产在线精品一区二区三区直播 | 亚洲精品一区二区三区在线 | 人人妻人人澡人人爽欧美精品 | 国产日产欧产精品精品app | 亚洲人亚洲人成电影网站色 | 国产精品人妻一区二区三区四 | 国产乱子伦视频在线播放 | 麻豆md0077饥渴少妇 | 亚洲精品综合一区二区三区在线 | 国产真人无遮挡作爱免费视频 | 国产极品视觉盛宴 | 97se亚洲精品一区 | 亚洲娇小与黑人巨大交 | 中文字幕av日韩精品一区二区 | 国产激情无码一区二区app | 国产av无码专区亚洲awww | 国产无遮挡又黄又爽又色 | 久久久久久久久蜜桃 | 老熟妇仑乱视频一区二区 | 精品一区二区三区无码免费视频 | 欧美人妻一区二区三区 | 国产美女极度色诱视频www | 亚洲无人区午夜福利码高清完整版 | 性啪啪chinese东北女人 | 欧美黑人性暴力猛交喷水 | 日本精品少妇一区二区三区 | 成人免费视频视频在线观看 免费 | 国产成人无码午夜视频在线观看 | 国内精品久久毛片一区二区 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 久久久亚洲欧洲日产国码αv | 久久久久亚洲精品中文字幕 | 无码人中文字幕 | 日韩精品乱码av一区二区 | 久9re热视频这里只有精品 | 久久99精品国产麻豆蜜芽 | 日本精品高清一区二区 | 综合人妻久久一区二区精品 | 亚洲色欲色欲天天天www | 国产 精品 自在自线 | 高清无码午夜福利视频 | 一本大道伊人av久久综合 | 99久久精品午夜一区二区 | 国产成人无码av一区二区 | а√天堂www在线天堂小说 | 人妻aⅴ无码一区二区三区 | 99精品国产综合久久久久五月天 | 无遮无挡爽爽免费视频 | 国产亚洲精品久久久久久国模美 | 白嫩日本少妇做爰 | 午夜精品一区二区三区的区别 | 亚洲中文字幕av在天堂 | 成人精品视频一区二区三区尤物 | 精品国产一区二区三区四区在线看 | 天下第一社区视频www日本 | 天天摸天天透天天添 | 国产在线一区二区三区四区五区 | 鲁鲁鲁爽爽爽在线视频观看 | 少妇愉情理伦片bd | 免费无码一区二区三区蜜桃大 | 久热国产vs视频在线观看 | 全球成人中文在线 | 国内揄拍国内精品少妇国语 | 成人精品天堂一区二区三区 | 午夜福利电影 | 人人妻人人澡人人爽人人精品浪潮 | 3d动漫精品啪啪一区二区中 | 久久亚洲国产成人精品性色 | 久久久国产精品无码免费专区 | 乌克兰少妇性做爰 | 色欲久久久天天天综合网精品 | 亚洲国产精品无码一区二区三区 | 国产成人精品无码播放 | 国产精品亚洲综合色区韩国 | 国产亚洲精品久久久久久国模美 | 亚洲の无码国产の无码步美 | 亚洲乱亚洲乱妇50p | 爱做久久久久久 | 久久久久久亚洲精品a片成人 | 久久99久久99精品中文字幕 | 在线亚洲高清揄拍自拍一品区 | 国产婷婷色一区二区三区在线 | 日日鲁鲁鲁夜夜爽爽狠狠 | 亚洲自偷精品视频自拍 | 一个人看的视频www在线 | 呦交小u女精品视频 | 亚洲色无码一区二区三区 | 亚洲熟妇自偷自拍另类 | 国产偷抇久久精品a片69 | 国产成人精品久久亚洲高清不卡 | 西西人体www44rt大胆高清 | 麻豆国产丝袜白领秘书在线观看 | 天海翼激烈高潮到腰振不止 | 小sao货水好多真紧h无码视频 | 99国产欧美久久久精品 | 又大又硬又爽免费视频 | 久久久久亚洲精品男人的天堂 | 欧美一区二区三区视频在线观看 | 日本饥渴人妻欲求不满 | 国产97人人超碰caoprom | 国产又爽又猛又粗的视频a片 | 欧美激情综合亚洲一二区 | 国产 浪潮av性色四虎 | 中文字幕人妻无码一区二区三区 | 国产精品理论片在线观看 | 荫蒂添的好舒服视频囗交 | 日韩精品无码一区二区中文字幕 | 麻豆国产人妻欲求不满 | 2019nv天堂香蕉在线观看 | 国产黄在线观看免费观看不卡 | 最新版天堂资源中文官网 | 精品无码一区二区三区的天堂 | 骚片av蜜桃精品一区 | 狠狠色色综合网站 | 欧美人与善在线com | 中文字幕色婷婷在线视频 | 无码一区二区三区在线观看 | 曰本女人与公拘交酡免费视频 | 成 人 免费观看网站 | 亚洲欧美日韩综合久久久 | 国产无套内射久久久国产 | 亚洲娇小与黑人巨大交 | 国产超碰人人爽人人做人人添 | 老熟妇仑乱视频一区二区 | 日韩人妻无码中文字幕视频 | 99久久精品无码一区二区毛片 | 国产九九九九九九九a片 | 亚洲熟妇色xxxxx欧美老妇y | 中文字幕人妻无码一夲道 | 亚洲综合另类小说色区 | 人妻aⅴ无码一区二区三区 | 老司机亚洲精品影院 | 思思久久99热只有频精品66 | 亚洲精品无码国产 | 国产成人无码a区在线观看视频app | 成人试看120秒体验区 | 色一情一乱一伦一区二区三欧美 | 无码免费一区二区三区 | 77777熟女视频在线观看 а天堂中文在线官网 | 久久99国产综合精品 | av在线亚洲欧洲日产一区二区 | 人人妻人人澡人人爽人人精品浪潮 | 国产精品高潮呻吟av久久4虎 | 成年美女黄网站色大免费全看 | 亚洲伊人久久精品影院 | 久久精品人人做人人综合 | 久久久久成人片免费观看蜜芽 | 亚洲成av人在线观看网址 | 亚洲熟悉妇女xxx妇女av | 国产成人无码av一区二区 | 精品无码av一区二区三区 | 少妇被黑人到高潮喷出白浆 | 久久精品国产大片免费观看 | 男女超爽视频免费播放 | 久久久亚洲欧洲日产国码αv | 蜜桃臀无码内射一区二区三区 | 老熟妇仑乱视频一区二区 | 性欧美熟妇videofreesex | 97se亚洲精品一区 | 国产精品va在线播放 | 狠狠色噜噜狠狠狠7777奇米 | 色婷婷综合中文久久一本 | 成 人 免费观看网站 | 天天拍夜夜添久久精品大 | 男女猛烈xx00免费视频试看 | 男女下面进入的视频免费午夜 | 国产av无码专区亚洲a∨毛片 | 牲交欧美兽交欧美 | 国产成人人人97超碰超爽8 | 美女极度色诱视频国产 | 色一情一乱一伦 | 成人免费视频在线观看 | 日本一卡二卡不卡视频查询 | 国产农村妇女高潮大叫 | 亚洲 a v无 码免 费 成 人 a v | 300部国产真实乱 | www国产精品内射老师 | 精品人人妻人人澡人人爽人人 | 一本久道高清无码视频 | 天堂无码人妻精品一区二区三区 | 成人精品一区二区三区中文字幕 | 人妻与老人中文字幕 | 任你躁国产自任一区二区三区 | 久久久久免费精品国产 | 亚洲人成影院在线无码按摩店 | 2020最新国产自产精品 | 丰满人妻精品国产99aⅴ | 亚洲人成网站在线播放942 | 久久综合给久久狠狠97色 | 久久午夜无码鲁丝片午夜精品 | 人妻无码久久精品人妻 | 欧美高清在线精品一区 | 日韩视频 中文字幕 视频一区 | 国产乱人无码伦av在线a | 偷窥村妇洗澡毛毛多 | 一本色道婷婷久久欧美 | 十八禁真人啪啪免费网站 | 国产精品人人妻人人爽 | 国产人妻精品一区二区三区 | 丰满肥臀大屁股熟妇激情视频 | 国产精品久久久午夜夜伦鲁鲁 | 夜夜高潮次次欢爽av女 | 国产小呦泬泬99精品 | 国产亚洲精品久久久闺蜜 | 在线精品亚洲一区二区 | 国产精品99爱免费视频 | 日日橹狠狠爱欧美视频 | 天堂在线观看www | 人人澡人人透人人爽 | 久久精品国产精品国产精品污 | 少妇无码一区二区二三区 | 水蜜桃亚洲一二三四在线 | 亚洲成av人综合在线观看 | 久精品国产欧美亚洲色aⅴ大片 | 性欧美熟妇videofreesex | 强开小婷嫩苞又嫩又紧视频 | 狠狠噜狠狠狠狠丁香五月 | ass日本丰满熟妇pics | 亚洲国产精品无码一区二区三区 | 国产又爽又黄又刺激的视频 | 性做久久久久久久免费看 | 无码国模国产在线观看 | 双乳奶水饱满少妇呻吟 | 狠狠色噜噜狠狠狠狠7777米奇 | 日韩少妇内射免费播放 | 无码一区二区三区在线观看 | 久久久成人毛片无码 | 乌克兰少妇性做爰 | 色狠狠av一区二区三区 | 日本乱人伦片中文三区 | 国产精华av午夜在线观看 | 亚洲精品国产a久久久久久 | 亚洲一区二区三区在线观看网站 | 暴力强奷在线播放无码 | 人妻尝试又大又粗久久 | 色综合久久久无码网中文 | 欧美猛少妇色xxxxx | 午夜男女很黄的视频 | 国产精品久久久久7777 | 久久五月精品中文字幕 | 久久国产精品二国产精品 | 亚洲国产精华液网站w | 亚洲精品久久久久中文第一幕 | 国产午夜精品一区二区三区嫩草 | 久久亚洲日韩精品一区二区三区 | 九九在线中文字幕无码 | 欧美xxxxx精品 | 国产绳艺sm调教室论坛 | 少妇厨房愉情理9仑片视频 | 老司机亚洲精品影院 | 377p欧洲日本亚洲大胆 | 久久久亚洲欧洲日产国码αv | 成人片黄网站色大片免费观看 | 日韩视频 中文字幕 视频一区 | 精品无人国产偷自产在线 | 国产人成高清在线视频99最全资源 | 国产精品无套呻吟在线 | 人人妻人人澡人人爽欧美一区 | 在线观看国产一区二区三区 | 精品一区二区三区波多野结衣 | 东京无码熟妇人妻av在线网址 | 久久亚洲日韩精品一区二区三区 | 丰腴饱满的极品熟妇 | 高潮毛片无遮挡高清免费视频 | 99riav国产精品视频 | 国产欧美精品一区二区三区 | 中文无码精品a∨在线观看不卡 | 2019午夜福利不卡片在线 | 亚洲七七久久桃花影院 | 成年美女黄网站色大免费全看 | 日日噜噜噜噜夜夜爽亚洲精品 | 国产97色在线 | 免 | 亚洲а∨天堂久久精品2021 | 亚洲国产av精品一区二区蜜芽 | 99久久人妻精品免费二区 | 国产精品亚洲一区二区三区喷水 | 国产在热线精品视频 | 伊人色综合久久天天小片 | 性开放的女人aaa片 | 无码午夜成人1000部免费视频 | 中文字幕人妻无码一夲道 | 久久综合香蕉国产蜜臀av | 亚洲精品一区国产 | 日本精品少妇一区二区三区 | 亚拍精品一区二区三区探花 | 爽爽影院免费观看 | 2020最新国产自产精品 | 精品久久久久久亚洲精品 | 国产成人午夜福利在线播放 | 国产精品高潮呻吟av久久4虎 | 中文久久乱码一区二区 | 中文字幕乱码人妻无码久久 | 我要看www免费看插插视频 | 熟妇激情内射com | 亚洲小说春色综合另类 | 国产成人无码区免费内射一片色欲 | 无码吃奶揉捏奶头高潮视频 | 欧美成人高清在线播放 | 天堂在线观看www | 蜜桃av抽搐高潮一区二区 | 国产精品美女久久久久av爽李琼 | 久久精品中文字幕大胸 | 欧洲熟妇色 欧美 | а天堂中文在线官网 | 国产午夜福利100集发布 | 噜噜噜亚洲色成人网站 | 亚洲爆乳大丰满无码专区 | 免费国产黄网站在线观看 | 偷窥村妇洗澡毛毛多 | 红桃av一区二区三区在线无码av | 国产麻豆精品精东影业av网站 | 中文字幕久久久久人妻 | 中文无码成人免费视频在线观看 | 中文字幕人成乱码熟女app | 久久精品丝袜高跟鞋 | 男人和女人高潮免费网站 | 国产高清不卡无码视频 | 久久亚洲日韩精品一区二区三区 | 久久久无码中文字幕久... | 少妇高潮喷潮久久久影院 | 女人色极品影院 | 黑人巨大精品欧美黑寡妇 | 国模大胆一区二区三区 | 亚洲中文字幕av在天堂 | 97精品人妻一区二区三区香蕉 | 国产无遮挡吃胸膜奶免费看 | 亚洲自偷精品视频自拍 | 久久国内精品自在自线 | 午夜无码人妻av大片色欲 | 在线观看国产一区二区三区 | 色一情一乱一伦一视频免费看 | 日韩精品无码一本二本三本色 | 我要看www免费看插插视频 | 鲁大师影院在线观看 | 亚洲综合精品香蕉久久网 | 国产97在线 | 亚洲 | 国产人妖乱国产精品人妖 | аⅴ资源天堂资源库在线 | 人妻中文无码久热丝袜 | 在教室伦流澡到高潮hnp视频 | 九九在线中文字幕无码 | 国产成人无码av片在线观看不卡 | 粗大的内捧猛烈进出视频 | 中文无码精品a∨在线观看不卡 | 麻豆人妻少妇精品无码专区 | 最近中文2019字幕第二页 | 精品熟女少妇av免费观看 | 中文久久乱码一区二区 | 亚洲第一无码av无码专区 | 少妇激情av一区二区 | 日韩人妻无码一区二区三区久久99 | av在线亚洲欧洲日产一区二区 | 国产成人久久精品流白浆 | 国产亲子乱弄免费视频 | 日本肉体xxxx裸交 | 亚洲精品鲁一鲁一区二区三区 | 18无码粉嫩小泬无套在线观看 | 免费视频欧美无人区码 | 欧美熟妇另类久久久久久不卡 | 国产两女互慰高潮视频在线观看 | 天下第一社区视频www日本 | 国产精品久久久久久亚洲影视内衣 | 中文字幕av伊人av无码av | 久久伊人色av天堂九九小黄鸭 | 中文字幕乱码亚洲无线三区 | 亚洲色欲久久久综合网东京热 | 精品成在人线av无码免费看 | 狠狠综合久久久久综合网 | 综合人妻久久一区二区精品 | 中文字幕日韩精品一区二区三区 | 野外少妇愉情中文字幕 | 国产人成高清在线视频99最全资源 | 精品无码国产一区二区三区av | 永久免费观看美女裸体的网站 | 亚洲成a人片在线观看日本 | 97久久国产亚洲精品超碰热 | 三级4级全黄60分钟 | 成人无码影片精品久久久 | 久久精品无码一区二区三区 | 欧美性生交xxxxx久久久 | 日本又色又爽又黄的a片18禁 | 成人性做爰aaa片免费看 | 国产在线精品一区二区三区直播 | 久久精品国产99久久6动漫 | 国产精品多人p群无码 | 在线视频网站www色 | 丝袜 中出 制服 人妻 美腿 | 精品乱子伦一区二区三区 | 久久久国产精品无码免费专区 | 久久99精品久久久久婷婷 | 欧美亚洲日韩国产人成在线播放 | 十八禁视频网站在线观看 | 日日噜噜噜噜夜夜爽亚洲精品 | 呦交小u女精品视频 | 日本护士xxxxhd少妇 | 亚洲精品国产第一综合99久久 | 日本熟妇人妻xxxxx人hd | 亚洲欧洲日本综合aⅴ在线 | 亚洲综合伊人久久大杳蕉 | а√资源新版在线天堂 | 欧美日韩视频无码一区二区三 | 欧美精品一区二区精品久久 | 奇米影视7777久久精品人人爽 | 成人精品天堂一区二区三区 | 亚洲男人av香蕉爽爽爽爽 | 99久久人妻精品免费一区 | 国产精品久久久久久无码 | 国产精品久久国产精品99 | 国产成人无码午夜视频在线观看 | 东京热男人av天堂 | 国产午夜福利100集发布 | 国产午夜精品一区二区三区嫩草 | 亚洲gv猛男gv无码男同 | 国产一区二区不卡老阿姨 | 成人影院yy111111在线观看 | 乌克兰少妇xxxx做受 | 国产av无码专区亚洲awww | 麻豆成人精品国产免费 | 久久国产精品二国产精品 | 曰本女人与公拘交酡免费视频 | 欧美成人免费全部网站 | 少妇高潮一区二区三区99 | 国产乱子伦视频在线播放 | 成人无码精品一区二区三区 | 牛和人交xxxx欧美 | 亚洲性无码av中文字幕 | 熟女体下毛毛黑森林 | 女人被男人躁得好爽免费视频 | 中文无码精品a∨在线观看不卡 | 久久人人爽人人爽人人片av高清 | 亚洲色偷偷偷综合网 | 欧美老妇与禽交 | 免费无码的av片在线观看 | 国产精品视频免费播放 | 午夜不卡av免费 一本久久a久久精品vr综合 | yw尤物av无码国产在线观看 | 性色av无码免费一区二区三区 | 久久久久人妻一区精品色欧美 | 亚洲の无码国产の无码影院 | 国产亚洲精品久久久久久国模美 | 国产av无码专区亚洲a∨毛片 | 精品 日韩 国产 欧美 视频 | 亚洲成av人片天堂网无码】 | 亚洲成av人综合在线观看 | 亚洲一区二区三区在线观看网站 | 国产两女互慰高潮视频在线观看 | 国内精品人妻无码久久久影院蜜桃 | 亚洲人成网站色7799 | 任你躁国产自任一区二区三区 | 久热国产vs视频在线观看 | 国产成人精品一区二区在线小狼 | 四虎国产精品免费久久 | 午夜男女很黄的视频 | 无人区乱码一区二区三区 | 激情国产av做激情国产爱 | 成人综合网亚洲伊人 | 暴力强奷在线播放无码 | 国产午夜无码精品免费看 | 少妇一晚三次一区二区三区 | 在线观看免费人成视频 | 一区二区三区高清视频一 | 亚洲精品国产精品乱码视色 | 一二三四社区在线中文视频 | 亚洲色偷偷偷综合网 | 国产精品美女久久久网av | 亚洲中文字幕在线观看 | 丰满妇女强制高潮18xxxx | 人妻体内射精一区二区三四 | 东京热男人av天堂 | 成人欧美一区二区三区黑人免费 | 人妻少妇精品视频专区 | 国产日产欧产精品精品app | 午夜无码人妻av大片色欲 | 青草视频在线播放 | 奇米影视7777久久精品 | a国产一区二区免费入口 | 青草青草久热国产精品 | 欧美放荡的少妇 | 又大又硬又爽免费视频 | 久久亚洲中文字幕精品一区 | 久久久精品欧美一区二区免费 | 国产一区二区不卡老阿姨 | 初尝人妻少妇中文字幕 | 日韩人妻无码中文字幕视频 | 55夜色66夜色国产精品视频 | 大色综合色综合网站 | 国产成人无码专区 | 无码精品人妻一区二区三区av | 中文毛片无遮挡高清免费 | 日日麻批免费40分钟无码 | 日本在线高清不卡免费播放 | 亚洲娇小与黑人巨大交 | 牲欲强的熟妇农村老妇女 | 大屁股大乳丰满人妻 | 成人亚洲精品久久久久软件 | 欧美黑人巨大xxxxx | 粉嫩少妇内射浓精videos | 欧美人与禽猛交狂配 | 天天摸天天碰天天添 | 亚洲日本一区二区三区在线 | 丰满妇女强制高潮18xxxx | 四虎永久在线精品免费网址 | 亚洲精品国产a久久久久久 | 在线播放免费人成毛片乱码 | 领导边摸边吃奶边做爽在线观看 | 中文字幕色婷婷在线视频 | 国产精品人人爽人人做我的可爱 | 波多野结衣av一区二区全免费观看 | 亚洲欧洲中文日韩av乱码 | 亚洲日本一区二区三区在线 | 日本精品人妻无码77777 天堂一区人妻无码 | 欧美精品免费观看二区 | 最新版天堂资源中文官网 | 免费国产黄网站在线观看 | 曰本女人与公拘交酡免费视频 | 中文字幕乱码亚洲无线三区 | 蜜臀aⅴ国产精品久久久国产老师 | 丝袜 中出 制服 人妻 美腿 | 精品无码av一区二区三区 | 人妻熟女一区 | 亚洲 另类 在线 欧美 制服 | 永久免费观看美女裸体的网站 | 无码人妻丰满熟妇区五十路百度 | 国内老熟妇对白xxxxhd | 男女猛烈xx00免费视频试看 | 99精品国产综合久久久久五月天 | 亚洲日本在线电影 | 国产精品va在线观看无码 | 中文字幕无码乱人伦 | 国产真实夫妇视频 | 特黄特色大片免费播放器图片 | 99精品国产综合久久久久五月天 | 野狼第一精品社区 | 欧美乱妇无乱码大黄a片 | 巨爆乳无码视频在线观看 | 国产欧美熟妇另类久久久 | 欧美精品国产综合久久 | 男女下面进入的视频免费午夜 | 亚洲成a人片在线观看无码 | 亚洲а∨天堂久久精品2021 | 丰满少妇熟乱xxxxx视频 | 无码播放一区二区三区 | 波多野结衣高清一区二区三区 | 人妻与老人中文字幕 | 国产又粗又硬又大爽黄老大爷视 | 天堂а√在线中文在线 | 亚洲春色在线视频 | 鲁鲁鲁爽爽爽在线视频观看 | 奇米影视7777久久精品人人爽 | 免费无码午夜福利片69 | 三上悠亚人妻中文字幕在线 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国产情侣作爱视频免费观看 | 国产香蕉尹人综合在线观看 | 免费观看激色视频网站 | 麻花豆传媒剧国产免费mv在线 | 国产精品va在线观看无码 | 日韩精品乱码av一区二区 | 强辱丰满人妻hd中文字幕 | 老司机亚洲精品影院无码 | 激情人妻另类人妻伦 | 强伦人妻一区二区三区视频18 | 成人一在线视频日韩国产 | 2019nv天堂香蕉在线观看 | 亚洲一区二区三区含羞草 | 国产激情无码一区二区app | 国产真实伦对白全集 | 激情内射亚州一区二区三区爱妻 | 综合网日日天干夜夜久久 | 国产精品无码久久av | 亚洲日韩一区二区三区 | 东北女人啪啪对白 | 男女下面进入的视频免费午夜 | 日韩精品一区二区av在线 | 久久99精品国产麻豆蜜芽 | 国产精品久久国产精品99 | 在线播放免费人成毛片乱码 | ass日本丰满熟妇pics | 国产精品久久国产三级国 | 国产sm调教视频在线观看 | 2020久久香蕉国产线看观看 | 精品国产精品久久一区免费式 | 色情久久久av熟女人妻网站 | 兔费看少妇性l交大片免费 | 成人性做爰aaa片免费看不忠 | 玩弄少妇高潮ⅹxxxyw | 啦啦啦www在线观看免费视频 | 亚洲自偷自拍另类第1页 | 欧美大屁股xxxxhd黑色 | 久久久久久久女国产乱让韩 | 伦伦影院午夜理论片 | 久久午夜无码鲁丝片 | 青青久在线视频免费观看 | 久久精品人妻少妇一区二区三区 | 性色欲网站人妻丰满中文久久不卡 | 午夜熟女插插xx免费视频 | 成人一区二区免费视频 | 精品人妻人人做人人爽夜夜爽 | 中文字幕乱码人妻二区三区 | 高潮毛片无遮挡高清免费视频 | 特大黑人娇小亚洲女 | 亚洲精品国产品国语在线观看 | 久久精品国产99久久6动漫 | 日本爽爽爽爽爽爽在线观看免 | 精品乱码久久久久久久 | 最新版天堂资源中文官网 | 亚洲国产高清在线观看视频 | 国产97在线 | 亚洲 | 中文字幕人成乱码熟女app | 人人爽人人爽人人片av亚洲 | 亚洲 a v无 码免 费 成 人 a v | 久久亚洲日韩精品一区二区三区 | 18精品久久久无码午夜福利 | 精品久久久无码人妻字幂 | 久久无码人妻影院 | 国产精品久久久久久亚洲影视内衣 | 国产精品美女久久久网av | 精品久久久无码人妻字幂 | 亚洲国产高清在线观看视频 | 天堂亚洲免费视频 | 一本久久伊人热热精品中文字幕 | 亚洲精品午夜国产va久久成人 | 亚洲成av人综合在线观看 | 少妇性荡欲午夜性开放视频剧场 | 国产精品久久久久久亚洲毛片 | 一本精品99久久精品77 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | а√天堂www在线天堂小说 | 国产精品18久久久久久麻辣 | 精品乱子伦一区二区三区 | 给我免费的视频在线观看 | 天堂亚洲2017在线观看 | 妺妺窝人体色www婷婷 | 麻豆果冻传媒2021精品传媒一区下载 | 久久精品女人的天堂av | 亚洲综合色区中文字幕 | 国产色在线 | 国产 | 高清国产亚洲精品自在久久 | 少妇无码av无码专区在线观看 | 久9re热视频这里只有精品 | 亚洲精品国产精品乱码不卡 | 中文字幕无码av激情不卡 | 少妇性荡欲午夜性开放视频剧场 | 亚洲综合精品香蕉久久网 | 中文毛片无遮挡高清免费 | 少妇人妻偷人精品无码视频 | 99久久无码一区人妻 | 激情人妻另类人妻伦 | 亚洲七七久久桃花影院 | 色妞www精品免费视频 | 久久国语露脸国产精品电影 | 在线精品亚洲一区二区 | 久久午夜无码鲁丝片秋霞 | 午夜性刺激在线视频免费 | 亚洲春色在线视频 | 少妇被黑人到高潮喷出白浆 | 久久精品一区二区三区四区 | 好男人社区资源 | 波多野结衣aⅴ在线 | 国产美女极度色诱视频www | 久久国产劲爆∧v内射 | 色欲久久久天天天综合网精品 | 免费人成网站视频在线观看 | 99久久精品日本一区二区免费 | 久久午夜无码鲁丝片午夜精品 | 亚洲精品欧美二区三区中文字幕 | 无码人妻少妇伦在线电影 | 乱人伦人妻中文字幕无码 | 无码人妻久久一区二区三区不卡 | 午夜成人1000部免费视频 | 日韩少妇内射免费播放 | 特黄特色大片免费播放器图片 | 亚洲狠狠色丁香婷婷综合 | 在线亚洲高清揄拍自拍一品区 | 中文字幕无码日韩专区 | 在线精品国产一区二区三区 | 2019午夜福利不卡片在线 | 美女黄网站人色视频免费国产 | 日韩精品a片一区二区三区妖精 | 国产精品高潮呻吟av久久4虎 | 未满成年国产在线观看 | 妺妺窝人体色www婷婷 | 成 人影片 免费观看 | 无套内谢的新婚少妇国语播放 | 国产超级va在线观看视频 | 激情综合激情五月俺也去 | 88国产精品欧美一区二区三区 | 国产女主播喷水视频在线观看 | 日本精品高清一区二区 | 无码av中文字幕免费放 | 精品国产一区二区三区av 性色 | 18精品久久久无码午夜福利 | 天干天干啦夜天干天2017 | 乱人伦人妻中文字幕无码久久网 | 成人性做爰aaa片免费看 | 性生交大片免费看女人按摩摩 | 亚洲一区二区三区播放 | 亚洲精品成人福利网站 | 99re在线播放 | 久久久久成人精品免费播放动漫 | 亚洲国产成人a精品不卡在线 | 人人妻人人澡人人爽欧美一区 | 久久人人爽人人爽人人片ⅴ | 特大黑人娇小亚洲女 | 国产成人av免费观看 | 欧美放荡的少妇 | 亚洲色欲久久久综合网东京热 | 国产亚洲tv在线观看 | 国产激情精品一区二区三区 | 免费观看激色视频网站 | 999久久久国产精品消防器材 | 青春草在线视频免费观看 | 色情久久久av熟女人妻网站 | 奇米影视888欧美在线观看 | 国产亚洲精品久久久久久久久动漫 | 久久国产36精品色熟妇 | 日韩精品乱码av一区二区 | 国产精品美女久久久网av | 蜜桃臀无码内射一区二区三区 | 色婷婷久久一区二区三区麻豆 | 日本大乳高潮视频在线观看 | 久在线观看福利视频 | 亚洲の无码国产の无码步美 | 国产精品无码一区二区桃花视频 | 亚洲日韩乱码中文无码蜜桃臀网站 | 亚洲爆乳大丰满无码专区 | 天海翼激烈高潮到腰振不止 | 国产精品丝袜黑色高跟鞋 | 亚洲色欲色欲天天天www | аⅴ资源天堂资源库在线 | 内射老妇bbwx0c0ck | 亚洲精品久久久久中文第一幕 | 99久久人妻精品免费二区 | 欧美三级a做爰在线观看 | 永久黄网站色视频免费直播 | www国产精品内射老师 | 丰满妇女强制高潮18xxxx | 男女超爽视频免费播放 | 成人免费无码大片a毛片 | 精品乱子伦一区二区三区 | 性生交片免费无码看人 | 久久综合色之久久综合 | 亚洲精品久久久久avwww潮水 | 欧美三级不卡在线观看 | 天堂无码人妻精品一区二区三区 | 国产精品人人妻人人爽 | 中文字幕无码免费久久99 | 国产艳妇av在线观看果冻传媒 | 亚洲精品久久久久avwww潮水 | 人人妻人人澡人人爽人人精品 | 露脸叫床粗话东北少妇 | 黄网在线观看免费网站 | 国产卡一卡二卡三 | 亚洲综合在线一区二区三区 | 亚洲另类伦春色综合小说 | 亚洲精品成人福利网站 | 亚洲一区二区三区香蕉 | 亚洲成av人片在线观看无码不卡 | 无套内射视频囯产 | 亚洲国产成人av在线观看 | 久久久精品国产sm最大网站 | 中文字幕+乱码+中文字幕一区 | 免费无码的av片在线观看 | 永久黄网站色视频免费直播 | 好爽又高潮了毛片免费下载 | 玩弄人妻少妇500系列视频 | 夜夜躁日日躁狠狠久久av | 久久无码专区国产精品s | 国产精品久久久 | 人人澡人摸人人添 | 欧美成人家庭影院 | 国产两女互慰高潮视频在线观看 | 色一情一乱一伦一区二区三欧美 | 一本色道婷婷久久欧美 | 国产精品久久久 | 性欧美videos高清精品 | 人人妻人人澡人人爽人人精品浪潮 | 国产亚洲日韩欧美另类第八页 | 纯爱无遮挡h肉动漫在线播放 | 97精品人妻一区二区三区香蕉 | 国产内射爽爽大片视频社区在线 | 亚洲国产精品无码一区二区三区 | 一区二区三区高清视频一 | √天堂中文官网8在线 | 亚洲最大成人网站 | 女人被爽到呻吟gif动态图视看 | 蜜桃无码一区二区三区 | 四虎4hu永久免费 | 久久99精品久久久久婷婷 | 蜜桃视频插满18在线观看 | 国产美女极度色诱视频www | 国产成人精品优优av | 狂野欧美性猛xxxx乱大交 | 无码av免费一区二区三区试看 | 久久综合九色综合97网 | 乱中年女人伦av三区 | 无码人妻av免费一区二区三区 | 无码毛片视频一区二区本码 | 日本护士毛茸茸高潮 | 亚洲春色在线视频 | 欧美激情内射喷水高潮 | 精品国产国产综合精品 | 日日干夜夜干 | 樱花草在线播放免费中文 | 中文字幕乱妇无码av在线 | 人人爽人人爽人人片av亚洲 | 亚洲一区av无码专区在线观看 | 久久久久成人片免费观看蜜芽 | 三上悠亚人妻中文字幕在线 | 99er热精品视频 | 亚洲中文字幕在线观看 | 国产乱码精品一品二品 | 内射巨臀欧美在线视频 | 久久亚洲精品成人无码 | 国产偷自视频区视频 | 国产精品人人爽人人做我的可爱 | 日日夜夜撸啊撸 | 青青青手机频在线观看 | 99久久久无码国产aaa精品 | 无码中文字幕色专区 | 国产无遮挡又黄又爽免费视频 | 香港三级日本三级妇三级 | 中文字幕无码乱人伦 | 无码人妻精品一区二区三区下载 | 国产成人久久精品流白浆 | 久久99精品久久久久久动态图 | 少妇无码av无码专区在线观看 | 精品久久久中文字幕人妻 | 久久五月精品中文字幕 | 国产精品亚洲综合色区韩国 | 国产精华av午夜在线观看 | 午夜熟女插插xx免费视频 | 久久精品99久久香蕉国产色戒 | 国产精品内射视频免费 | 牲欲强的熟妇农村老妇女视频 | 男人和女人高潮免费网站 | 欧美国产亚洲日韩在线二区 | 日韩精品乱码av一区二区 | 国产精品99久久精品爆乳 | 最近免费中文字幕中文高清百度 | 久久久中文字幕日本无吗 | a国产一区二区免费入口 | 国产亚洲欧美在线专区 | 人妻aⅴ无码一区二区三区 | 国产免费观看黄av片 | 理论片87福利理论电影 | 男人的天堂av网站 | 内射老妇bbwx0c0ck | 国产亚洲精品久久久久久大师 | 波多野结衣av在线观看 | 亚洲乱码日产精品bd | 大色综合色综合网站 | 波多野结衣av一区二区全免费观看 | 久久午夜无码鲁丝片秋霞 | 亚洲日韩一区二区 | 无码人妻精品一区二区三区不卡 | 四虎永久在线精品免费网址 | 成人亚洲精品久久久久软件 | 99麻豆久久久国产精品免费 | 欧美人与禽zoz0性伦交 | 久9re热视频这里只有精品 | 全黄性性激高免费视频 | 欧美丰满熟妇xxxx性ppx人交 | 亚洲中文字幕乱码av波多ji | 亚洲区小说区激情区图片区 | 亚洲色成人中文字幕网站 | 一本色道久久综合狠狠躁 | 欧美zoozzooz性欧美 | 人妻人人添人妻人人爱 | 亚洲国产精品毛片av不卡在线 | 日韩视频 中文字幕 视频一区 | 国产sm调教视频在线观看 | 久久99精品久久久久久动态图 | 中文字幕亚洲情99在线 | 国产乱子伦视频在线播放 | 国语精品一区二区三区 | 成人aaa片一区国产精品 | 亚洲成色在线综合网站 | 亚拍精品一区二区三区探花 | 精品久久久无码中文字幕 | 丰满人妻被黑人猛烈进入 | 麻豆蜜桃av蜜臀av色欲av | 日日摸夜夜摸狠狠摸婷婷 | 国产激情无码一区二区 | 色综合久久88色综合天天 | 激情亚洲一区国产精品 | 国产乡下妇女做爰 | 日韩精品一区二区av在线 | 亚洲国产午夜精品理论片 | 成人av无码一区二区三区 | 午夜无码人妻av大片色欲 | 激情五月综合色婷婷一区二区 | 少妇无码一区二区二三区 | 国产av一区二区三区最新精品 | 伊在人天堂亚洲香蕉精品区 | 日本一区二区三区免费高清 | 精品国产av色一区二区深夜久久 | 激情内射日本一区二区三区 | 国产无遮挡又黄又爽免费视频 | 香港三级日本三级妇三级 | 亚洲 日韩 欧美 成人 在线观看 | 成在人线av无码免费 | 我要看www免费看插插视频 | 欧美xxxxx精品 | 精品一区二区三区无码免费视频 | 日韩人妻少妇一区二区三区 | 中国女人内谢69xxxxxa片 | 亚洲日韩av一区二区三区四区 | 国产精品鲁鲁鲁 | 亚洲小说春色综合另类 | 亚洲中文字幕无码中文字在线 | 日本免费一区二区三区最新 | 午夜精品久久久内射近拍高清 | 国产色视频一区二区三区 | 欧美人与禽zoz0性伦交 | 久精品国产欧美亚洲色aⅴ大片 | 久久久成人毛片无码 | 爽爽影院免费观看 | 欧美日韩人成综合在线播放 | 欧美国产日产一区二区 | 亚洲一区二区三区香蕉 | 国产精品久久久久久久9999 | 婷婷五月综合激情中文字幕 | 老熟女重囗味hdxx69 | 18黄暴禁片在线观看 | 99在线 | 亚洲 | 未满小14洗澡无码视频网站 | 少妇性l交大片 | 国产成人无码av片在线观看不卡 | 亚洲熟妇色xxxxx欧美老妇y | 久久精品无码一区二区三区 | 久久精品丝袜高跟鞋 | 久久久久久a亚洲欧洲av冫 | 成人无码视频免费播放 | 欧美xxxx黑人又粗又长 | 精品一二三区久久aaa片 | 水蜜桃亚洲一二三四在线 | 鲁一鲁av2019在线 | 国产精品.xx视频.xxtv | 又大又黄又粗又爽的免费视频 | 亚洲综合另类小说色区 | 国产成人无码午夜视频在线观看 | 精品久久综合1区2区3区激情 | 丰满少妇女裸体bbw | 日日天干夜夜狠狠爱 | 国产97在线 | 亚洲 | 精品国产麻豆免费人成网站 | 国产成人无码av在线影院 | 欧美放荡的少妇 | 亚洲精品久久久久久一区二区 | 美女极度色诱视频国产 | 999久久久国产精品消防器材 | 人人澡人人透人人爽 | 亚洲一区二区三区无码久久 | 国产又爽又猛又粗的视频a片 | 精品国产一区av天美传媒 | 久久综合久久自在自线精品自 | 永久黄网站色视频免费直播 | 性欧美大战久久久久久久 | 最近中文2019字幕第二页 | 亚洲成av人影院在线观看 | 欧美精品免费观看二区 | 久久精品国产一区二区三区肥胖 | 亚洲国产欧美日韩精品一区二区三区 | 亚洲国产日韩a在线播放 | 人妻少妇被猛烈进入中文字幕 | 婷婷五月综合缴情在线视频 | 成人无码视频在线观看网站 | 无码av岛国片在线播放 | 亚洲人亚洲人成电影网站色 | 九九久久精品国产免费看小说 | 中文字幕无线码 | 特大黑人娇小亚洲女 | 无遮挡国产高潮视频免费观看 | 欧美丰满少妇xxxx性 | 娇妻被黑人粗大高潮白浆 | 亚洲自偷精品视频自拍 | √8天堂资源地址中文在线 | 久久综合香蕉国产蜜臀av | 免费看男女做好爽好硬视频 | 中文字幕av无码一区二区三区电影 | 乱码av麻豆丝袜熟女系列 | 国产成人综合在线女婷五月99播放 | 国产成人av免费观看 | 欧美真人作爱免费视频 | 亚洲日韩一区二区 | 欧美人妻一区二区三区 | 亚洲精品一区二区三区在线 | 欧美阿v高清资源不卡在线播放 | 精品亚洲成av人在线观看 | 日韩精品无码免费一区二区三区 | 亚洲综合无码久久精品综合 | 一区二区三区乱码在线 | 欧洲 | 婷婷五月综合缴情在线视频 | 欧美三级不卡在线观看 | 色综合久久网 | 好屌草这里只有精品 | 5858s亚洲色大成网站www | 亚洲日本va中文字幕 | 99久久精品国产一区二区蜜芽 | 又黄又爽又色的视频 | 国产精品无码永久免费888 | 无码一区二区三区在线观看 | 久久国产精品偷任你爽任你 | 中文字幕乱码人妻无码久久 | av无码电影一区二区三区 | 亚洲七七久久桃花影院 | 国产午夜亚洲精品不卡下载 | 国产人成高清在线视频99最全资源 | 亚洲一区二区三区含羞草 | 亚洲国产精品毛片av不卡在线 | 国产成人综合色在线观看网站 | 丝袜足控一区二区三区 | 日韩 欧美 动漫 国产 制服 | 欧美国产日韩久久mv | 鲁一鲁av2019在线 | 亚洲国产精品久久久久久 | 性欧美疯狂xxxxbbbb | 国产精品毛多多水多 | 999久久久国产精品消防器材 | 亚洲熟妇色xxxxx欧美老妇y | 国产无遮挡又黄又爽又色 | 久久久久久a亚洲欧洲av冫 | 国产精华av午夜在线观看 | 国产欧美亚洲精品a | 国产97人人超碰caoprom | 国产精品人人爽人人做我的可爱 | 乱人伦中文视频在线观看 | 女人被爽到呻吟gif动态图视看 | a片在线免费观看 | 亚洲高清偷拍一区二区三区 | 亚洲国产精品无码久久久久高潮 | 亚洲 高清 成人 动漫 | 亚洲精品一区二区三区大桥未久 | 国产美女极度色诱视频www | 国产深夜福利视频在线 | 日韩少妇白浆无码系列 | 强伦人妻一区二区三区视频18 | 任你躁国产自任一区二区三区 | 免费无码肉片在线观看 | 中文字幕日韩精品一区二区三区 | 少妇被黑人到高潮喷出白浆 | 理论片87福利理论电影 | 亚洲中文字幕无码中字 | 九九综合va免费看 | 18无码粉嫩小泬无套在线观看 | 草草网站影院白丝内射 | 亚洲无人区一区二区三区 | 成人精品视频一区二区 | 乱人伦人妻中文字幕无码 | 欧美第一黄网免费网站 | 在线亚洲高清揄拍自拍一品区 | 国产绳艺sm调教室论坛 | 对白脏话肉麻粗话av | 2020久久超碰国产精品最新 | 又大又硬又爽免费视频 | 亚洲精品一区二区三区婷婷月 | 四虎国产精品一区二区 | 成人综合网亚洲伊人 | 蜜桃视频插满18在线观看 | 成熟妇人a片免费看网站 | 久久伊人色av天堂九九小黄鸭 | 中文字幕av日韩精品一区二区 | av无码久久久久不卡免费网站 | 久久久久99精品国产片 | 无码国产色欲xxxxx视频 | 中文字幕中文有码在线 | 又大又黄又粗又爽的免费视频 | 3d动漫精品啪啪一区二区中 | 噜噜噜亚洲色成人网站 | 欧美日韩在线亚洲综合国产人 | 精品国产一区二区三区四区 | 中文字幕无码av波多野吉衣 | 国产人成高清在线视频99最全资源 | 国产精品无码mv在线观看 | 又粗又大又硬又长又爽 | 亚洲人成网站在线播放942 | 永久免费精品精品永久-夜色 | 无码人妻出轨黑人中文字幕 | 精品人妻人人做人人爽夜夜爽 | 精品夜夜澡人妻无码av蜜桃 | 日日鲁鲁鲁夜夜爽爽狠狠 | 天干天干啦夜天干天2017 | 人人妻人人澡人人爽人人精品 | 久久久中文字幕日本无吗 | 日韩人妻系列无码专区 | 亚洲精品久久久久avwww潮水 | 免费人成在线视频无码 | 国产成人人人97超碰超爽8 | 国内精品人妻无码久久久影院 | 国产人妻久久精品二区三区老狼 | 欧美亚洲日韩国产人成在线播放 | 亚洲毛片av日韩av无码 | 永久黄网站色视频免费直播 | 精品久久久无码中文字幕 | 亚洲爆乳精品无码一区二区三区 | 亚洲国产精品久久人人爱 | 精品无码av一区二区三区 | 在线 国产 欧美 亚洲 天堂 | 国产精品福利视频导航 | 国产va免费精品观看 | 国内精品久久毛片一区二区 | 无码人妻少妇伦在线电影 | 精品国产乱码久久久久乱码 | 午夜性刺激在线视频免费 | 国产一精品一av一免费 | 内射白嫩少妇超碰 | 中文字幕精品av一区二区五区 | 久久99精品久久久久婷婷 | 午夜成人1000部免费视频 | 亚洲 a v无 码免 费 成 人 a v | 又大又紧又粉嫩18p少妇 | 波多野结衣一区二区三区av免费 | 一个人免费观看的www视频 | 荡女精品导航 | 中文字幕无码免费久久9一区9 | 国産精品久久久久久久 | 丰满少妇人妻久久久久久 | 日本熟妇乱子伦xxxx | 熟妇人妻中文av无码 | 日本丰满护士爆乳xxxx | 无遮无挡爽爽免费视频 | 亚洲色欲色欲欲www在线 | 欧美丰满熟妇xxxx性ppx人交 | 国产精品永久免费视频 | 香港三级日本三级妇三级 | 内射后入在线观看一区 | 国产精品亚洲а∨无码播放麻豆 | 国产无遮挡又黄又爽免费视频 | 日本熟妇人妻xxxxx人hd | 亚洲成在人网站无码天堂 | 麻豆果冻传媒2021精品传媒一区下载 | 中文字幕无码免费久久9一区9 | 亚洲欧美日韩国产精品一区二区 | 欧美激情内射喷水高潮 | 荫蒂被男人添的好舒服爽免费视频 | 男人的天堂2018无码 | 女人被男人躁得好爽免费视频 | a在线观看免费网站大全 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 人妻少妇精品久久 | 丝袜足控一区二区三区 | 丰满少妇熟乱xxxxx视频 | 国产美女极度色诱视频www | 色综合久久88色综合天天 | 亚洲狠狠婷婷综合久久 | 又粗又大又硬又长又爽 | 无码人妻丰满熟妇区五十路百度 | 美女黄网站人色视频免费国产 | 蜜桃臀无码内射一区二区三区 | 国产一区二区三区四区五区加勒比 | 高潮毛片无遮挡高清免费 | av在线亚洲欧洲日产一区二区 | 国产欧美精品一区二区三区 | 牲欲强的熟妇农村老妇女视频 | 国产精品无码一区二区三区不卡 | 久激情内射婷内射蜜桃人妖 | 精品久久8x国产免费观看 | 精品久久久久久亚洲精品 | 日本一本二本三区免费 | 日韩精品无码免费一区二区三区 | 国产欧美精品一区二区三区 | 无码人中文字幕 | 少妇性俱乐部纵欲狂欢电影 | 丁香啪啪综合成人亚洲 | 国模大胆一区二区三区 | 亚洲国产精品久久久久久 | 欧美黑人性暴力猛交喷水 | 亚洲熟妇色xxxxx欧美老妇y | 中文亚洲成a人片在线观看 | 国产亚洲美女精品久久久2020 | 欧美xxxxx精品 | 永久免费观看国产裸体美女 | 亚洲欧美色中文字幕在线 | 给我免费的视频在线观看 | 亚洲天堂2017无码中文 | 女人高潮内射99精品 | 天天拍夜夜添久久精品大 | 欧美精品一区二区精品久久 | 东京一本一道一二三区 | 夜夜高潮次次欢爽av女 | 精品久久久无码中文字幕 | a在线观看免费网站大全 | 小泽玛莉亚一区二区视频在线 | 性欧美疯狂xxxxbbbb | 欧美日韩一区二区三区自拍 | 成年女人永久免费看片 | 欧美亚洲国产一区二区三区 | 高清国产亚洲精品自在久久 | 免费人成网站视频在线观看 | 一区二区三区高清视频一 | 99精品国产综合久久久久五月天 | 东京热无码av男人的天堂 | 亚洲狠狠色丁香婷婷综合 | 国产情侣作爱视频免费观看 | 无码精品人妻一区二区三区av | 国产人成高清在线视频99最全资源 | 国产精品va在线观看无码 | 又大又紧又粉嫩18p少妇 | 亚洲精品欧美二区三区中文字幕 | 少妇人妻偷人精品无码视频 | 国产精品a成v人在线播放 | 亚洲 日韩 欧美 成人 在线观看 | 亚洲成色www久久网站 | 97se亚洲精品一区 | 丰满少妇女裸体bbw | 国产亚洲tv在线观看 | 久久人人爽人人爽人人片ⅴ | 又大又紧又粉嫩18p少妇 | 欧洲极品少妇 | 中文字幕无码av波多野吉衣 | 亚洲精品国产第一综合99久久 | 午夜不卡av免费 一本久久a久久精品vr综合 | 精品人人妻人人澡人人爽人人 | 亚洲人成人无码网www国产 | 国产无套粉嫩白浆在线 | 老司机亚洲精品影院 | 搡女人真爽免费视频大全 | 少妇太爽了在线观看 | 国产午夜福利亚洲第一 | 福利一区二区三区视频在线观看 | 国产一区二区三区四区五区加勒比 | 国内揄拍国内精品人妻 | 国产美女精品一区二区三区 | 欧美丰满熟妇xxxx性ppx人交 | 蜜桃视频韩日免费播放 | 亚洲 另类 在线 欧美 制服 | 亚洲 欧美 激情 小说 另类 | 精品日本一区二区三区在线观看 | 国产精品人人爽人人做我的可爱 | 亚洲精品一区三区三区在线观看 | 欧美黑人性暴力猛交喷水 | 日日鲁鲁鲁夜夜爽爽狠狠 | 人妻与老人中文字幕 | 亚洲色无码一区二区三区 | 东京热男人av天堂 | 久久久久se色偷偷亚洲精品av | 国产又爽又黄又刺激的视频 | 久久人人爽人人爽人人片av高清 | 男人和女人高潮免费网站 | 国内揄拍国内精品人妻 | 国产成人无码a区在线观看视频app | 国产精品99爱免费视频 | 欧美一区二区三区视频在线观看 | 国产精品人妻一区二区三区四 | 香港三级日本三级妇三级 | 免费男性肉肉影院 | 四虎4hu永久免费 | 精品久久久中文字幕人妻 | 亚洲色欲色欲天天天www | 77777熟女视频在线观看 а天堂中文在线官网 | 免费无码午夜福利片69 | 国产麻豆精品精东影业av网站 | 高潮喷水的毛片 | 国产高清不卡无码视频 | 国产suv精品一区二区五 | 免费无码的av片在线观看 | 亚洲国产欧美日韩精品一区二区三区 | 国产婷婷色一区二区三区在线 | 色欲久久久天天天综合网精品 | 亚洲国产精品成人久久蜜臀 | 成人片黄网站色大片免费观看 | 97久久精品无码一区二区 | 日韩欧美中文字幕在线三区 | 成 人影片 免费观看 | 日韩 欧美 动漫 国产 制服 | 日日躁夜夜躁狠狠躁 | 窝窝午夜理论片影院 | 男女下面进入的视频免费午夜 | 四虎4hu永久免费 | 亚洲 欧美 激情 小说 另类 | 内射爽无广熟女亚洲 | 蜜臀aⅴ国产精品久久久国产老师 | 国产成人无码a区在线观看视频app | 精品日本一区二区三区在线观看 | 久久综合九色综合欧美狠狠 | 亚洲国产精华液网站w | 综合网日日天干夜夜久久 | 在线播放亚洲第一字幕 | 成人av无码一区二区三区 | 国产免费观看黄av片 | 国精品人妻无码一区二区三区蜜柚 | 欧美精品一区二区精品久久 | 一本久道久久综合狠狠爱 | 色婷婷欧美在线播放内射 | 久久99精品久久久久久动态图 | 亚洲国产欧美国产综合一区 | 国产内射爽爽大片视频社区在线 | 国产一区二区不卡老阿姨 | 久久国产精品_国产精品 | 国产精品人人妻人人爽 | 最近免费中文字幕中文高清百度 | 国产亚av手机在线观看 | 国产人成高清在线视频99最全资源 | 亚洲精品午夜国产va久久成人 | 亚洲爆乳精品无码一区二区三区 | 曰韩无码二三区中文字幕 | 色一情一乱一伦一视频免费看 | 亚洲码国产精品高潮在线 | 亚洲综合伊人久久大杳蕉 | 欧美熟妇另类久久久久久不卡 | 熟妇人妻无乱码中文字幕 | 亚洲中文字幕av在天堂 | 99久久婷婷国产综合精品青草免费 | 国模大胆一区二区三区 | 人妻天天爽夜夜爽一区二区 | 国产人妻精品一区二区三区 | 国产精品对白交换视频 |