1. 程式人生 > >gonefuture的學習部落格

gonefuture的學習部落格

零碎的知識點-4

select、poll、epoll之間的區別總結

select、poll、epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。

select的幾大缺點:

  1. 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
  2. 同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
  3. select支援的檔案描述符數量太小了,預設是1024

poll實現

poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,相比select模型,poll使用連結串列儲存檔案描述符,因此沒有了監視檔案數量的限制,但其他三個缺點依然存在。

epoll (事件驅動)

epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的呼叫介面上的不同,select和poll都只提供了一個函式——select或者poll函式。而epoll提供了三個函式,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll控制代碼;epoll_ctl是註冊要監聽的事件型別;epoll_wait則是等待事件的產生。

總結:
1. select,poll實現需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要呼叫 epoll_wait不斷輪詢就緒連結串列,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,呼叫回撥函式,把就緒fd放入就緒連結串列中,並喚醒在 epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的 時候只要判斷一下就緒連結串列是否為空就行了,這節省了大量的CPU時間。這就是回撥機制帶來的效能提升。

  1. select,poll每次呼叫都要把fd集合從使用者態往核心態拷貝一次,並且要把current往裝置等待佇列中掛一次,而epoll只要 一次拷貝,而且把current往等待佇列上掛也只掛一次(在epoll_wait的開始,注意這裡的等待佇列並不是裝置等待佇列,只是一個epoll內 部定義的等待佇列)。這也能節省不少的開銷。

Top K 問題

堆排解法

堆排序利用的大(小)堆頂所有節點元素都比父節點小(大)的性質來實現的。
既然一個大頂堆的頂是最大的元素,那我們要找最小的K個元素,是不是可以先建立一個包含K個元素的堆,然後遍歷集合,如果集合的元素比堆頂元素小(說明它目前應該在K個最小之列),那就用該元素來替換堆頂元素,同時維護該堆的性質,那在遍歷結束的時候,堆中包含的K個元素是不是就是我們要找的最小的K個元素?

實現:

在堆排的基礎上,稍作了修改,buildHeap和heapify函式都是一樣的實現。
速記口訣:最小的K個用最大堆,最大的K個用最小堆。

public class TopK {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] a = { 1, 17, 3, 4, 5, 6, 7, 16, 9, 10, 11, 12, 13, 14, 15, 8 };
        int[] b = topK(a, 4);
        for (int i = 0; i < b.length; i++) {
            System.out.print(b[i] + ", ");
        }
    }

    public static void heapify(int[] array, int index, int length) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = index;
        if (left < length && array[left] > array[index]) {
            largest = left;
        }
        if (right < length && array[right] > array[largest]) {
            largest = right;
        }
        if (index != largest) {
            swap(array, largest, index);
            heapify(array, largest, length);
        }
    }

    public static void swap(int[] array, int a, int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }

    public static void buildHeap(int[] array) {
        int length = array.length;
        for (int i = length / 2 - 1; i >= 0; i--) {
            heapify(array, i, length);
        }
    }

    public static void setTop(int[] array, int top) {
        array[0] = top;
        heapify(array, 0, array.length);
    }

    public static int[] topK(int[] array, int k) {
        int[] top = new int[k];
        for (int i = 0; i < k; i++) {
            top[i] = array[i];
        }
        //先建堆,然後依次比較剩餘元素與堆頂元素的大小,比堆頂小的, 說明它應該在堆中出現,則用它來替換掉堆頂元素,然後沉降。
        buildHeap(top);
        for (int j = k; j < array.length; j++) {
            int temp = top[0];
            if (array[j] < temp) {
                setTop(top, array[j]);
            }
        }
        return top;
    }
}

快排解法

用快排的思想來解Top K問題,必然要運用到”分治”結果的使用上。我們知道,分治函式會返回一個position,在position左邊的數都比第position個數小,在position右邊的數都比第position大。我們不妨不斷呼叫分治函式,直到它輸出的position = K-1,此時position前面的K個數(0到K-1)就是要找的前K個數。

實現:

“分治”還是原來的那個分治,關鍵是getTopK的邏輯

public class TopK {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] array = { 9, 3, 1, 10, 5, 7, 6, 2, 8, 0 };
        getTopK(array, 4);
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + ", ");
        }
    }

    // 分治
    public static int partition(int[] array, int low, int high) {
        if (array != null && low < high) {
            int flag = array[low];
            while (low < high) {
                while (low < high && array[high] >= flag) {
                    high--;
                }
                array[low] = array[high];
                while (low < high && array[low] <= flag) {
                    low++;
                }
                array[high] = array[low];
            }
            array[low] = flag;
            return low;
        }
        return 0;
    }

    public static void getTopK(int[] array, int k) {
        if (array != null && array.length > 0) {
            int low = 0;
            int high = array.length - 1;
            int index = partition(array, low, high);
            //不斷調整分治的位置,直到position = k-1
            while (index != k - 1) {
                //大了,往前調整
                if (index > k - 1) {
                    high = index - 1;
                    index = partition(array, low, high);
                }
                //小了,往後調整
                if (index < k - 1) {
                    low = index + 1;
                    index = partition(array, low, high);
                }
            }
        }
    }
}

CAP 理論 、BASE思想, 最終一致性和五分鐘原則

CAP理論

  • C : Consisitency 一致性
  • A: Availablity 可用性(指得事快速獲取資料)
  • P: Tolerance of network Partition 分割槽容忍性(分散式)

定理:任何分散式系統只可同時滿足二點,沒法三者兼顧。三者只能選其二。

  • CA: 傳統關係資料庫
  • AP:key-value 資料庫

ACID

關係資料庫的ACID模型擁有 高一致性 + 可用性 很難進行分割槽:

  • Atomicity原子性:一個事務中所有操作都必須全部完成,要麼全部不完成。
  • Consistency一致性: 在事務開始或結束時,資料庫應該在一致狀態。
  • Isolation隔離層: 事務將假定只有它自己在操作資料庫,彼此不知曉。
  • Durability: 一旦事務完成,就不能返回。

跨資料庫兩段提交事務:2PC (two-phase commit), 2PC is the anti-scalability pattern (Pat Helland) 是反可伸縮模式的,JavaEE中的JTA事務可以支援2PC。因為2PC是反模式,儘量不要使用2PC,使用BASE來回避。

BASE思想

BASE模型反ACID模型,完全不同ACID模型,犧牲高一致性,獲得可用性或可靠性:

  • Basically Available基本可用。支援分割槽失敗(e.g. sharding碎片劃分資料庫)
  • Soft state軟狀態 狀態可以有一段時間不同步,非同步。
  • E
  • ventually consistent最終一致,最終資料是一致的就可以了,而不是時時高一致。

最終一致性

過程鬆,結果緊,最終結果必須保持一致性 。(DNS系統)

  • 強一致性 任何時刻,所有節點中的資料是一樣的。用一時間點,你在節點A中獲取到key1的值與節點B中獲取到key1的值應該都是一樣的。
  • 弱一致性 包含多種不同的實現,目前分散式系統中廣泛實現的是最終一致性。(其他還有因果一致性)

I/O的五分鐘法則

如果一個數據的訪問週期在5分鐘以內則存放在記憶體中,否則應該存放在硬碟中。

Spring 生命週期

對於普通的Java物件,當new的時候建立物件,當它沒有任何引用的時候被垃圾回收機制回收。而由Spring IoC容器託管的物件,它們的生命週期完全由容器控制。Spring中每個Bean的生命週期如下:
Spring生命週期
1. 例項化Bean
對於BeanFactory容器,當客戶向容器請求一個尚未初始化的bean時,或初始化bean的時候需要注入另一個尚未初始化的依賴時,容器就會呼叫createBean進行例項化。 對於ApplicationContext容器,當容器啟動結束後,便例項化所有的bean。 容器通過獲取BeanDefinition物件中的資訊進行例項化。並且這一步僅僅是簡單的例項化,並未進行依賴注入。 例項化物件被包裝在BeanWrapper物件中,BeanWrapper提供了設定物件屬性的介面,從而避免了使用反射機制設定屬性。
2. 設定物件屬性(依賴注入)
例項化後的物件被封裝在BeanWrapper物件中,並且此時物件仍然是一個原生的狀態,並沒有進行依賴注入。 緊接著,Spring根據BeanDefinition中的資訊進行依賴注入。 並且通過BeanWrapper提供的設定屬性的介面完成依賴注入。
3. 注入Aware介面
緊接著,Spring會檢測該物件是否實現了xxxAware介面,並將相關的xxxAware例項注入給bean,包括包括了BeanNameAware、BeanFactoryAware,ApplicationContextAware介面。
4. BeanPostProcessor
當經過上述幾個步驟後,bean物件已經被正確構造,但如果你想要物件被使用前再進行一些自定義的處理,就可以通過BeanPostProcessor介面實現。 該介面提供了兩個函式:postProcessBeforeInitialzation( Object bean, String beanName ) 當前正在初始化的bean物件會被傳遞進來,我們就可以對這個bean作任何處理。 這個函式會先於InitialzationBean執行,因此稱為前置處理。 所有Aware介面的注入就是在這一步完成的。postProcessAfterInitialzation( Object bean, String beanName ) 當前正在初始化的bean物件會被傳遞進來,我們就可以對這個bean作任何處理。 這個函式會在InitialzationBean完成後執行,因此稱為後置處理。
5. InitializingBean與init-method
當BeanPostProcessor的前置處理完成後就會進入本階段。 InitializingBean介面只有一個函式:afterPropertiesSet()這一階段也可以在bean正式構造完成前增加我們自定義的邏輯,但它與前置處理不同,由於該函式並不會把當前bean物件傳進來,因此在這一步沒辦法處理物件本身,只能增加一些額外的邏輯。 若要使用它,我們需要讓bean實現該介面,並把要增加的邏輯寫在該函式中。然後Spring會在前置處理完成後檢測當前bean是否實現了該介面,並執行afterPropertiesSet函式。當然,Spring為了降低對客戶程式碼的侵入性,給bean的配置提供了init-method屬性,該屬性指定了在這一階段需要執行的函式名。Spring便會在初始化階段執行我們設定的函式。init-method本質上仍然使用了InitializingBean介面。
6. DisposableBean
和destroy-method和init-method一樣,通過給destroy-method指定函式,就可以在bean銷燬前執行指定的邏輯。

Spring容器中Bean的作用域

當通過Spring容器建立一個Bean例項時,不僅可以完成Bean例項的例項化,還可以為Bean指定特定的作用域。Spring支援如下5種作用域:

  • singleton:單例模式,在整個Spring IoC容器中,使用singleton定義的Bean將只有一個例項
  • prototype:原型模式,每次通過容器的getBean方法獲取prototype定義的Bean時,都將產生一個新的Bean例項
  • request:對於每次HTTP請求,使用request定義的Bean都將產生一個新例項,即每次HTTP請求將會產生不同的Bean例項。只有在Web應用中使用Spring時,該作用域才有效
  • session:對於每次HTTP Session,使用session定義的Bean豆漿產生一個新例項。同樣只有在Web應用中使用Spring時,該作用域才有效
  • globalsession:每個全域性的HTTP Session,使用session定義的Bean都將產生一個新例項。典型情況下,僅在使用portlet context的時候有效。同樣只有在Web應用中使用Spring時,該作用域才有效

其中比較常用的是singleton和prototype兩種作用域。
對於singleton作用域的Bean,每次請求該Bean都將獲得相同的例項。容器負責跟蹤Bean例項的狀態,負責維護Bean例項的生命週期行為;
如果一個Bean被設定成prototype作用域,程式每次請求該id的Bean,Spring都會新建一個Bean例項,然後返回給程式。在這種情況下,Spring容器僅僅使用new 關鍵字建立Bean例項,一旦建立成功,容器不在跟蹤例項,也不會維護Bean例項的狀態。

如果不指定Bean的作用域,Spring預設使用singleton作用域。

SpringMVC執行流程

元件介紹:

  • DispatcherServlet
    前端控制器,作用就是接收請求,響應結果,相當於轉發器
  • HandleMapping
    處理器對映器,作用就是根據請求的URL查詢Handler
  • HandlerAdapter
    處理器介面卡,作用就是按照特定的規則去執行Handler,也就是開發Handler時需要滿足HandlerAdapter的規則,這樣HandlerAdapter才能執行Handler。
  • View resolver
    檢視解析器,作用根據邏輯檢視解析成真正的檢視(view)
  • view
    檢視,是一個介面,其實現類能支援不同的view型別,jsp、freemarker、Excel等

執行過程文字描述

  1. 使用者發起請求到前端控制器DispatcherServlet

  2. DispatcherServlet請求處理器對映器HandlerMapping查詢Handler
    可以是根據xml查詢,也可以是根據註解查詢

  3. HandlerMappingDispatcherServlet返回一個執行鏈HandlerExecutionChain,包含HandlerInterceptionHandler

  4. HandlerMapping呼叫處理器介面卡HandlerAdapter去執行Handler

  5. 處理器介面卡去執行Handler

  6. Handler執行完給處理器介面卡返回ModelAndView
    ModelAndView是SpringMVC框架的一個底層物件,包括Model和View

  7. 處理器介面卡給DispatcherServlet返回ModelAndView

  8. DispatcherServlet請求檢視解析器View resolver進行檢視解析
    根據邏輯檢視解析成真正的物理檢視(jsp等)

  9. 檢視解析器向DispatcherServlet返回view

  10. DispatcherServlet進行檢視渲染

  11. DispatcherServlet向用戶響應結果

SpringMVC執行流程

Java記憶體模型與共享變數可見性

Java記憶體模型(JMM)

Java記憶體模型的主要目標:定義在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。
注意:上邊的變數指的是共享變數(例項欄位、靜態欄位、陣列物件元素),不包括執行緒私有變數(區域性變數、方法引數),因為私有變數不會存在競爭關係。

JMM

說明:

  • 所有共享變數存於主記憶體
  • 每一條執行緒都有自己的工作記憶體(就是上圖所說的本地記憶體)
  • 工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本

注意:

  • 執行緒對變數的操作都要在工作記憶體中進行,不能直接操作主記憶體
  • 不同的執行緒之間無法直接訪問對方的工作記憶體中的變數
  • 不同執行緒之間的變數的傳遞必須通過主記憶體

8條記憶體屏障指令

  • lock(鎖定): 作用於主記憶體,把一個變數標識為一條執行緒獨佔的狀態
  • unlock(解鎖): 作用於主記憶體,把一個處於鎖定的變數解鎖。
  • use(使用): 作用於工作記憶體的變數,把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值): 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個變數複製的位元組碼指令時執行只個操作。

下表的四條是與volatile實現記憶體的可見性直接相關的四條(store、write、read、load)

  • store: 把工作記憶體中的變數的值傳到主記憶體變數中。
  • write: 把store操作從工作記憶體中得到的變數放到主記憶體的變數中
  • read: 把一個變數的值從主記憶體中傳輸到執行緒的工作記憶體
  • load: 把read操作從主記憶體中獲取到的變數放到工作記憶體的變數中去

注意:

  • 一個變數在同一時刻只允許一條執行緒對其進行lock操作
  • lock操作會將該變數在所有執行緒工作記憶體中的變數副本清空,否則就起不到鎖的作用了
  • lock操作可被同一條執行緒多次進行,lock幾次,就要unlock幾次(可重入鎖)
  • unlock之前必須先執行store-write
  • store-write必須成對出現(工作記憶體–>主記憶體)
  • read-load必須成對出現(主記憶體–>工作記憶體)

記憶體訪問重排序與Java記憶體模型

根據Java記憶體模型中的規定,可以總結出以下幾條happens-before規則。Happens-before的前後兩個操作不會被重排序且後者對前者的記憶體可見。

  • 程式次序法則:執行緒中的每個動作A都happens-before於該執行緒中的每一個動作B,其中,在程式中,所有的動作B都能出現在A之後。
  • 監視器鎖法則:對一個監視器鎖的解鎖 happens-before於每一個後續對同一監視器鎖的加鎖。
  • volatile變數法則:對volatile域的寫入操作happens-before於每一個後續對同一個域的讀寫操作。
  • 執行緒啟動法則:在一個執行緒裡,對Thread.start的呼叫會happens-before於每個啟動執行緒的動作。
  • 執行緒終結法則:執行緒中的任何動作都happens-before於其他執行緒檢測到這個執行緒已經終結、或者從Thread.join呼叫中成功返回,或Thread.isAlive返回false。
  • 中斷法則:一個執行緒呼叫另一個執行緒的interrupt happens-before於被中斷的執行緒發現中斷。
  • 終結法則:一個物件的建構函式的結束happens-before於這個物件finalizer的開始。
  • 傳遞性:如果A happens-before於B,且B happens-before於C,則A happens-before於C

實現記憶體可見性:

要實現共享變數的可見性,必須保證兩點

  • 執行緒修改後的共享變數值能夠及時從工作記憶體中重新整理到主記憶體中
  • 其他執行緒能夠及時把共享變數的最新值從主記憶體更新到自己的工作記憶體中
    *

synchronized實現可見性

synchronized能夠實現:

  • 原子性(同步)
  • 可見性

JMM關於synchronized的兩條規定:

  • 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中
  • 執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主存中重新讀取最新的值

執行緒解鎖前對共享變數的修改在下次加鎖時對其他執行緒可見

執行緒執行互斥程式碼的過程

  1. 獲得互斥鎖
  2. 清空工作記憶體
  3. 從主記憶體拷貝變數的最新副本到工作記憶體
  4. 執行程式碼
  5. 將更改後的共享變數的值重新整理到主記憶體中
  6. 釋放互斥鎖

volatile實現可見性

volatile關鍵字:

  • 能夠保證volatile變數的可見性
  • 不能保證volatile變數複合操作的原子

volatile如何實現記憶體的可見性 :

深入來說:通過加入記憶體屏障禁止重排序優化來實現的

  • 在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障
    • 在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障

通俗地講:volatile變數在每次被執行緒訪問時,都強迫從主記憶體中重讀該變數的值,而當該變數發生變化時,又會強迫將最新的值重新整理到主記憶體。這樣任何時刻,不同的執行緒總能看到該變數的最新值。

執行緒寫volatile變數的過程:

  1. 改變執行緒工作記憶體中volatile變數副本的值
  2. 將改變後的副本的值從工作記憶體重新整理到主記憶體

執行緒讀volatile變數的過程:

  1. 從主記憶體中讀取volatile變數的最新值到執行緒的工作記憶體中
  2. 從工作記憶體中讀取volatile變數的副本

synchronized vs volatile

  • volatile不需要加鎖,比synchronized更輕量級,不會阻塞執行緒
  • synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性

可作為GC Root的物件

在Java虛擬機器中判斷一個物件是否可以被回收,有一種做法叫可達性分析演算法,也就是從GC Root到各個物件,如果GC Root到某個物件還有可達的引用鏈,那麼這個物件就還不能被回收,否則就等著被收割吧。

所謂“GC roots”,或者說tracing GC的“根集合”,就是一組必須活躍的引用
例如說,這些引用可能包括:

  • 所有Java執行緒當前活躍的棧幀裡指向GC堆裡的物件的引用;換句話說,當前所有正在被呼叫的方法的引用型別的引數/區域性變數/臨時值
  • VM的一些靜態資料結構裡指向GC堆裡的物件的引用,例如說HotSpot VM裡的Universe裡有很多這樣的引用。
  • JNI handles,包括global handles和local handles
  • (看情況)所有當前被載入的Java類
  • (看情況)Java類的引用型別靜態變數
  • (看情況)Java類的執行時常量池裡的引用型別常量(String或Class型別)
  • (看情況)String常量池(StringTable)裡的引用

Tracing GC的根本思路就是:給定一個集合的引用作為根出發,通過引用關係遍歷物件圖,能被遍歷到的(可到達的)物件就被判定為存活,其餘物件(也就是沒有被遍歷到的)就自然被判定為死亡。注意再注意:tracing GC的本質是通過找出所有活物件來把其餘空間認定為“無用”,而不是找出所有死掉的物件並回收它們佔用的空間。

冪等性

概述

冪等性原本是數學上的概念,即使公式:f(x)=f(f(x)) 能夠成立的數學性質。用在程式設計領域,則意為對同一個系統,使用同樣的條件,一次請求和重複的多次請求對系統資源的影響是一致的。

冪等的常用思路

1. MVCC:

多版本併發控制,樂觀鎖的一種實現,在資料更新時需要去比較持有資料的版本號,版本號不一致的操作無法成功。例如部落格點贊次數自動+1的介面:

public boolean addCount(Long id, Long version);
UPDATE blogTable SET count=count+1,version=version+1 WHERE id=321 AND version=123

每一個version只有一次執行成功的機會,一旦失敗必須重新獲取。

2. 去重表

利用資料庫表單的特性來實現冪等,常用的一個思路是在表上構建唯一性索引,保證某一類資料一旦執行完畢,後續同樣的請求再也無法成功寫入。

例子還是上述的部落格點贊問題,要想防止一個人重複點贊,可以設計一張表,將部落格id與使用者id繫結建立唯一索引,每當使用者點贊時就往表中寫入一條資料,這樣重複點讚的資料就無法寫入。

3. TOKEN機制:

這種機制就比較重要了,適用範圍較廣,有多種不同的實現方式。其核心思想是為每一次操作生成一個唯一性的憑證,也就是token。一個token在操作的每一個階段只有一次執行權,一旦執行成功則儲存執行結果。對重複的請求,返回同一個結果。

以電商平臺為例子,電商平臺上的訂單id就是最適合的token。當用戶下單時,會經歷多個環節,比如生成訂單,減庫存,減優惠券等等。

每一個環節執行時都先檢測一下該訂單id是否已經執行過這一步驟,對未執行的請求,執行操作並快取結果,而對已經執行過的id,則直接返回之前的執行結果,不做任何操作。這樣可以在最大程度上避免操作的重複執行問題,快取起來的執行結果也能用於事務的控制等。

MySQL鎖機制

MySQL有三種鎖的級別:頁級、表級、行級。

  • MyISAM 表級鎖(table-level locking)
  • MEMORY 表級鎖(table-level locking)
  • BDB 頁面鎖(page-levellocking),但也支援表級鎖;
  • InnoDB 既支援行級鎖(row-level locking),也支援表級鎖,但預設情況下是採用行級鎖。

以下講的都是在innodb引擎的前提下。

  1. 共享鎖(Share Locks,即S鎖),使用方式select … LOCK IN SHARE MODE
    SELECT … FOR UPDATE同時只能有一個在語句執行,另一個會阻塞;SELECT … LOCK IN SHARE MODE可以多個同時執行(這也是和for update最大的區別)

  2. 排它鎖(Exclusive Locks,即X鎖),使用方式:select … FOR UPDATE
    select … for update鎖住的行記錄,其它事務不可修改,如果有修改會wait直到鎖釋放(事務commit)或超時

MySQL 樂觀鎖與悲觀鎖

悲觀鎖

悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。

樂觀鎖

樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個資料。樂觀鎖適用於讀多寫少的應用場景,這樣可以提高吞吐量。

樂觀鎖:假設不會發生併發衝突,只在提交操作時檢查是否違反資料完整性。

樂觀鎖一般來說有以下2種方式:

  1. 使用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。

  2. 使用時間戳(timestamp)。樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個欄位,名稱無所謂,欄位型別使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。

MySQL隱式和顯示鎖定

MySQL InnoDB採用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行過程中,隨時都可以執行鎖定,鎖只有在執行 COMMIT或者ROLLBACK的時候才會釋放,並且所有的鎖是在同一時刻被釋放。前面描述的鎖定都是隱式鎖定,InnoDB會根據事務隔離級別在需要的時候自動加鎖。

另外,InnoDB也支援通過特定的語句進行顯示鎖定,這些語句不屬於SQL規範:

  • SELECT ... LOCK IN SHARE MODE
    是IS鎖(意向共享鎖),即在符合條件的rows上都加了共享鎖,這樣的話,其他session可以讀取這些記錄,也可以繼續新增IS鎖,但是無法修改這些記錄直到你這個加鎖的session執行完成(否則直接鎖等待超時)。
  • SELECT ... FOR UPDATE
    是IX鎖(意向排它鎖),一種行級鎖,一旦使用者對某個行施加了行級加鎖,則該使用者可以查詢也可以更新被加鎖的資料行,其它使用者只能查詢但不能更新被加鎖的資料行。真正對錶進行更新時,是以獨佔方式鎖表,一直到提交或復原該事務為止。行鎖永遠是獨佔方式鎖。

總結:經過測試, 這兩種模式加鎖的無資料的情況下,鎖不會起作用,必須加鎖的表必須有資料。 lock in share mode 也叫間隙鎖,帶where 條件加鎖,where欄位是整型並且是主鍵,會變成行鎖。

樂觀鎖與悲觀鎖的區別

樂觀鎖的思路一般是表中增加版本欄位,更新時where語句中增加版本的判斷,算是一種CAS(Compare And Swep)操作,商品庫存場景中number起到了版本控制(相當於version)的作用( AND number=#{number})。

悲觀鎖之所以是悲觀,在於他認為本次操作會發生併發衝突,所以一開始就對商品加上鎖(SELECT … FOR UPDATE),然後就可以安心的做判斷和更新,因為這時候不會有別人更新這條商品庫存。

消費者的推拉模式

訊息中介軟體的主要功能是訊息的路由(Routing)和快取(Buffering)。在AMQP中提供類似功能的兩種域模型:Exchange 和 Message queue。

JMS中定義了兩種訊息模型:點對點(point to point, queue)和釋出/訂閱(publish/subscribe,topic)。主要區別就是是否能重複消費。

點對點:Queue,不可重複消費

訊息生產者生產訊息傳送到queue中,然後訊息消費者從queue中取出並且消費訊息。訊息被消費以後,queue中不再有儲存,所以訊息消費者不可能消費到已經被消費的訊息。
Queue支援存在多個消費者,但是對一個訊息而言,只會有一個消費者可以消費。
注:Kafka不遵守JMS協議,所以Kafka實際應用中,很可能會需要ack(確認),然後多個消費者能夠會同時消費。。需要具體看。

釋出/訂閱:Topic,可以重複消費

訊息生產者(釋出)將訊息釋出到topic中,同時有多個訊息消費者(訂閱)消費該訊息。
和點對點方式不同,釋出到topic的訊息會被所有訂閱者消費。

支援訂閱組的釋出訂閱模式:

釋出訂閱模式下,當釋出者訊息量很大時,顯然單個訂閱者的處理能力是不足的。實際上現實場景中是多個訂閱者節點組成一個訂閱組負載均衡消費topic訊息即分組訂閱,這樣訂閱者很容易實現消費能力線性擴充套件。

消費者獲取訊息:Push和Pull

  • Push方式:由訊息中介軟體主動地將訊息推送給消費者;
  • Pull方式:由消費者主動向訊息中介軟體拉取訊息。

流行模型比較:

  • RabbitMQ
    既支援記憶體佇列也支援持久化佇列,消費端為Push模型,消費狀態和訂閱關係由服務端負責維護,訊息消費完後立即刪除,不保留歷史訊息。
  • Kafka
    只支援訊息持久化,消費端為Pull模型,消費狀態和訂閱關係由客戶端端負責維護,訊息消費完後不會立即刪除,會保留歷史訊息。
  • ActiveMQ
    一條訊息從producer端發出之後,一旦被broker正確儲存,那麼它將會被consumer消費,然後ACK,broker端才會刪除;不過當訊息過期或者儲存裝置溢位時,也會終結它。

高併發系統的限流,快取,降級

快取

在大型高併發系統中,如果沒有快取資料庫將分分鐘被爆,系統也會瞬間癱瘓。使用快取不單單能夠提升系統訪問速度、提高併發訪問量,也是保護資料庫、保護系統的有效方式。
大型網站一般主要是“讀”,快取的使用很容易被想到。在大型“寫”系統中,快取也常常扮演者非常重要的角色。比如累積一些資料批量寫入,記憶體裡面的快取佇列(生產消費),以及HBase寫資料的機制等等也都是通過快取提升系統的吞吐量或者實現系統的保護措施。甚至訊息中介軟體,你也可以認為是一種分散式的資料快取。

降級

服務降級是當伺服器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放伺服器資源以保證核心任務的正常執行。降級往往會指定不同的級別,面臨不同的異常等級執行不同的處理。根據服務方式:可以拒接服務,可以延遲服務,也有時候可以隨機服務。根據服務範圍:可以砍掉某個功能,也可以砍掉某些模組。總之服務降級需要根據不同的業務需求採用不同的降級策略。主要的目的就是服務雖然有損但是總比沒有好。

限流

限流可以認為服務降級的一種,限流就是限制系統的輸入和輸出流量已達到保護系統的目的。一般來說系統的吞吐量是可以被測算的,為了保證系統的穩定執行,一旦達到的需要限制的閾值,就需要限制流量並採取一些措施以完成限制流量的目的。比如:延遲處理,拒絕處理,或者部分拒絕處理等等。

限流演算法

  1. 計數器
    計數器是最簡單粗暴的演算法。比如某個服務最多隻能每秒鐘處理100個請求。我們可以設定一個1秒鐘的滑動視窗,視窗中有10個格子,每個格子100毫秒,每100毫秒移動一次,每次移動都需要記錄當前服務請求的次數。記憶體中需要儲存10次的次數。可以用資料結構LinkedList來實現。格子每次移動的時候判斷一次,當前訪問次數和LinkedList中最後一個相差是否超過100,如果超過就需要限流了。

  2. 漏桶
    漏桶演算法即leaky bucket是一種非常常用的限流演算法,可以用來實現流量整形(Traffic Shaping)和流量控制(Traffic Policing)。

漏桶演算法的主要概念如下:

  1. 一個固定容量的漏桶,按照常量固定速率流出水滴;
  2. 如果桶是空的,則不需流出水滴;
    3.可以以任意速率流入水滴到漏桶;
  3. 如果流入水滴超出了桶的容量,則流入的水滴溢位了(被丟棄),而漏桶容量是不變的

漏桶演算法比較好實現,在單機系統中可以使用佇列來實現,在分散式環境中訊息中介軟體或者Redis都是可選的方案。

  1. 令牌桶

令牌桶演算法是一個存放固定容量令牌(token)的桶,按照固定速率往桶裡新增令牌。令牌桶演算法基本可以用下面的幾個概念來描述:

  1. 令牌將按照固定的速率被放入令牌桶中。比如每秒放10個。
  2. 桶中最多存放b個令牌,當桶滿時,新新增的令牌被丟棄或拒絕。
  3. 當一個n個位元組大小的資料包到達,將從桶中刪除n個令牌,接著資料包被髮送到網路上。
  4. 如果桶中的令牌不足n個,則不會刪除令牌,且該資料包將被限流(要麼丟棄,要麼緩衝區等待)。

漏桶和令牌桶的比較

令牌桶可以在執行時控制和調整資料處理的速率,處理某時的突發流量。放令牌的頻率增加可以提升整體資料處理的速度,而通過每次獲取令牌的個數增加或者放慢令牌的發放速度和降低整體資料處理速度。而漏桶不行,因為它的流出速率是固定的,程式處理速度也是固定的。

整體而言,令牌桶演算法更優,但是實現更為複雜一些。