肝了15000字效能調優系列專題(JVM、MySQL、Nginx and Tomcat),看不完先收藏

fly1north發表於2021-04-22

效能調優,無疑是個龐大的話題,也是很多專案中非常重要的一環,效能調優難做是眾所周知的,畢竟效能調優涵蓋的面實在是太多了,在這裡我就大概的講一下企業中最常用的四種調優——JVM調優、MySQL調優、Nginx調優以及Tomcat調優,一家之言,有什麼說的不對的還請多包涵補充。

篇幅所限,有些東西是肯定寫不到的,所以本文只是挑了一些重要部分來剖析,如果需要完整詳細的掌握效能調優,可以來領取系統整理的效能調優筆記和相關學習資料

話不多說,坐穩扶好,發車嘍!

1、JVM類載入機制詳解

如下圖所示,JVM類載入機制分為五個部分:載入,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。

1.1 載入

在載入階段,虛擬機器需要完成以下三件事情:

1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。注意這裡的二進位制位元組流不一定非得要從一個Class檔案獲取,這裡既可以從ZIP包中讀取(比如從jar包和war包中讀取),也可以從網路中獲取,也可以在執行時計算生成(動態代理),也可以由其它檔案生成(比如將JSP檔案轉換成對應的Class類)。

2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

3)在Java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。

相對於類載入過程的其他階段,載入階段(準確地說,是載入階段中獲取類的二進位制位元組流的動作)是開發期可控性最強的階段,因為載入階段既可以使用系統提供的類載入器來完成,也可以由使用者自定義的類載入器去完成,開發人員們可以通過定義自己的類載入器去控制位元組流的獲取方式。

1.2 驗證

這一階段的主要目的是為了確保Class檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

1.3 準備

準備階段是正式為類變數分配記憶體並設定類變數的初始值階段,即在方法區中分配這些變數所使用的記憶體空間。注意這裡所說的初始值概念,比如一個類變數定義為:

public static int v = 8080;

實際上變數v在準備階段過後的初始值為0而不是8080,將v賦值為8080的putstatic指令是程式被編譯後,存放於類構造器<client>方法之中,這裡我們後面會解釋。

但是注意如果宣告為:

public static final int v = 8080;

在編譯階段會為v生成ConstantValue屬性,在準備階段虛擬機器會根據ConstantValue屬性將v賦值為8080。

1.4 解析

解析階段是指虛擬機器將常量池中的符號引用替換為直接引用的過程。符號引用就是class檔案中的:

  • CONSTANT_Class_info
  • CONSTANT_Field_info
  • CONSTANT_Method_info

等型別的常量。

下面我們解釋一下符號引用和直接引用的概念:

  • 符號引用與虛擬機器實現的佈局無關,引用的目標並不一定要已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。

  • 直接引用可以是指向目標的指標,相對偏移量或是一個能間接定位到目標的控制程式碼。如果有了直接引用,那引用的目標必定已經在記憶體中存在。

1.5 初始化

初始化階段是類載入最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程式程式碼。

初始化階段是執行類構造器<clint>方法的過程。<clint>方法是由編譯器自動收集類中的類變數的賦值操作和靜態語句塊中的語句合併而成的。虛擬機器會保證<clint>方法執行之前,父類的<clint>方法已經執行完畢。p.s: 如果一個類中沒有對靜態變數賦值也沒有靜態語句塊,那麼編譯器可以不為這個類生成<clint>()方法。

注意以下幾種情況不會執行類初始化:

  • 通過子類引用父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義物件陣列,不會觸發該類的初始化。
  • 常量在編譯期間會存入呼叫類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
  • 通過類名獲取Class物件,不會觸發類的初始化。
  • 通過Class.forName載入指定類時,如果指定引數initialize為false時,也不會觸發類初始化,其實這個引數是告訴虛擬機器,是否要對類進行初始化。
  • 通過ClassLoader預設的loadClass方法,也不會觸發初始化動作。

1.6 類載入器

虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼模組被稱為“類載入器”。

對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提之下才有意義,否則,即使這兩個類是來源於同一個Class檔案,只要載入它們的類載入器不同,那這兩個類就必定不相等。這裡所指的“相等”,包括代表類的Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括了使用instanceof關鍵字做物件所屬關係判定等情況。如果沒有注意到類載入器的影響,在某些情況下可能會產生具有迷惑性的結果。

JVM提供了3種類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。
  • 擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。
  • 應用程式類載入器(Application ClassLoader):負責載入使用者路徑(classpath)上的類庫。

JVM通過雙親委派模型進行類的載入,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類載入器。

jvm classloader

雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父載入器的程式碼。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

採用雙親委派的一個好處是比如載入位於rt.jar包中的類java.lang.Object,不管是哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入器最終得到的都是同樣一個Object物件。

在有些情境中可能會出現要我們自己來實現一個類載入器的需求,由於這裡涉及的內容比較廣泛,我想以後單獨寫一篇文章來講述,不過這裡我們還是稍微來看一下。我們直接看一下jdk中的ClassLoader的原始碼實現:

protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
  • 首先通過Class c = findLoadedClass(name);判斷一個類是否已經被載入過。
  • 如果沒有被載入過執行if (c == null)中的程式,遵循雙親委派的模型,首先會通過遞迴從父載入器開始找,直到父類載入器是Bootstrap ClassLoader為止。
  • 最後根據resolve的值,判斷這個class是否需要解析。

而上面的findClass()的實現如下,直接丟擲一個異常,並且方法是protected,很明顯這是留給我們開發者自己去實現的,這裡我們以後我們單獨寫一篇文章來講一下如何重寫findClass方法來實現我們自己的類載入器。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

2、JVM記憶體模型

2.1 各部分的功能

這幾個儲存區最主要的就是棧區和堆區,那麼什麼是棧什麼是堆呢?說的簡單點,棧裡面存放的是基本的資料型別和引用,而堆裡面則是存放各種物件例項的。

堆與棧分開設計是為什麼呢?

  • 棧儲存了處理邏輯、堆儲存了具體的資料,這樣隔離設計更為清晰
  • 堆與棧分離,使得堆可以被多個棧共享。
  • 棧儲存了上下文的資訊,因此只能向上增長;而堆是動態分配

棧的大小可以通過-XSs設定,如果不足的話,會引起java.lang.StackOverflowError的異常

棧區

執行緒私有,生命週期與執行緒相同。每個方法執行的時候都會建立一個棧幀(stack frame)用於存放 區域性變數表、操作棧、動態連結、方法出口。

存放物件例項,所有的物件的記憶體都在這裡分配。垃圾回收主要就是作用於這裡的。

  • 堆得記憶體由-Xms指定,預設是實體記憶體的1/64;最大的記憶體由-Xmx指定,預設是實體記憶體的1/4。
  • 預設空餘的堆記憶體小於40%時,就會增大,直到-Xmx設定的記憶體。具體的比例可以由-XX:MinHeapFreeRatio指定
  • 空餘的記憶體大於70%時,就會減少記憶體,直到-Xms設定的大小。具體由-XX:MaxHeapFreeRatio指定。

因此一般都建議把這兩個引數設定成一樣大,可以避免JVM在不斷調整大小。

2.2 程式計數器

這裡記錄了執行緒執行的位元組碼的行號,在分支、迴圈、跳轉、異常、執行緒恢復等都依賴這個計數器。

2.3 方法區

型別資訊、欄位資訊、方法資訊、其他資訊

2.4總結

3、垃圾收集機制詳解

3.1如何定義垃圾

有兩種方式,一種是引用計數(但是無法解決迴圈引用的問題);另一種就是可達性分析。

判斷物件可以回收的情況:

  • 顯示的把某個引用置位NULL或者指向別的物件
  • 區域性引用指向的物件
  • 弱引用關聯的物件

3.2 垃圾回收的方法

3.2.1Mark-Sweep標記-清除演算法

這種方法優點就是減少停頓時間,但是缺點是會造成記憶體碎片。

3.2.2 Copying複製演算法

這種方法不涉及到物件的刪除,只是把可用的物件從一個地方拷貝到另一個地方,因此適合大量物件回收的場景,比如新生代的回收。

3.2.3 Mark-Compact標記-整理演算法

這種方法可以解決記憶體碎片問題,但是會增加停頓時間。

3.2.4 Generational Collection 分代收集

最後的這種方法是前面幾種的合體,即目前JVM主要採取的一種方法,思想就是把JVM分成不同的區域。每種區域使用不同的垃圾回收方法。

上面可以看到堆分成兩個個區域:

  • 新生代(Young Generation):用於存放新建立的物件,採用複製回收方法,如果在s0和s1之間複製一定次數後,轉移到年老代中。這裡的垃圾回收叫做minor GC;
  • 年老代(Old Generation):這些物件垃圾回收的頻率較低,採用的標記整理方法,這裡的垃圾回收叫做 major GC。

這裡可以詳細的說一下新生代複製回收的演算法流程:

在新生代中,分為三個區:Eden, from survivor, to survior。

  • 當觸發minor GC時,會先把Eden中存活的物件複製到to Survivor中;
  • 然後再看from survivor,如果次數達到年老代的標準,就複製到年老代中;如果沒有達到則複製到to survivor中,如果to survivor滿了,則複製到年老代中。
  • 然後調換from survivor 和 to survivor的名字,保證每次to survivor都是空的等待物件複製到那裡的。

3.3 垃圾回收器

3.3.1 序列收集器 Serial

這種收集器就是以單執行緒的方式收集,垃圾回收的時候其他執行緒也不能工作。

3.3.2 並行收集器 Parallel

以多執行緒的方式進行收集

3.3.3併發標記清除收集器 Concurrent Mark Sweep Collector, CMS

大致的流程為:初始標記–併發標記–重新標記–併發清除

3.3.4 G1收集器 Garbage First Collector

大致的流程為:初始標記–併發標記–最終標記–篩選回收

篇幅所限,關於類位元組碼檔案、調優工具以及GC日誌分析這裡就不寫了,如果有感興趣的朋友可以點選領取我整理的完整JVM效能調優筆記,裡面會有詳細敘述。

1、SQL執行原理詳解

1.1 SQL Server組成部分

1.1.1 關係引擎:主要作用是優化和執行查詢。

包含三大元件:

(1)命令解析器:檢查語法和轉換查詢樹。

(2)查詢執行器:優化查詢。

(3)查詢優化器:負責執行查詢。

1.1.2 儲存引擎:管理所有資料及涉及的IO

包含三大元件:

(1)事務管理器:通過鎖來管理資料及維持事務的ACID屬性。

(2)資料訪問方法:處理對行、索引、頁、行版本、空間分配等的I/O請求。

(3)緩衝區管理器:管理SQL Server的主要記憶體消耗元件Buffer Pool。

1.1.3Buffer Pool

包含SQL Server的所有快取。如計劃快取和資料快取。

1.1.4事務日誌

記錄事務的所有更改。保證事務ACID屬性的重要元件。

1.1.5資料檔案

資料庫的物理儲存檔案。

6.SQL Server網路介面
建立在客戶端和伺服器之間的網路連線的協議層

1.2 查詢的底層原理

1.2.1 當客戶端執行一條T-SQL語句給SQL Server伺服器時,會首先到達伺服器的網路介面,網路介面和客戶端之間有協議層。

1.2.2 客戶端和網路介面之間建立連線。使用稱為“表格格式資料流”(TDS) 資料包的 Microsoft 通訊格式來格式化通訊資料。

1.2.3 客戶端傳送TDS包給協議層。協議層接收到TDS包後,解壓並分析包裡面包含了什麼請求。

1.2.4 命令解析器解析T-SQL語句。命令解析器會做下面幾件事情:

(1)檢查語法。發現有語法錯誤就返回給客戶端。下面的步驟不執行。

(2)檢查緩衝池(Buffer Pool)中是否存在一個對應該T-SQL語句的執行計劃快取。

(3)如果找到已快取的執行計劃,就從執行計劃快取中直接讀取,並傳輸給查詢執行器執行。

(4)如果未找到執行計劃快取,則在查詢執行器中進行優化併產生執行計劃,存放到Buffer Pool中。

1.2.5 查詢優化器優化SQL語句

當Buffer Pool中沒有該SQL語句的執行計劃時,就需要將SQL傳到查詢優化器,通過一定的演算法,分析SQL語句,產生一個或多個候選執行計劃。選出開銷最小的計劃作為最終執行計劃。然後將執行計劃傳給查詢執行器。

1.2.6 查詢執行器執行查詢

查詢執行器把執行計劃通過OLE DB介面傳給儲存引擎的資料訪問方法。

1.2.7 資料訪問方法生成執行程式碼

資料訪問方法將執行計劃生成SQL Server可運算元據的程式碼,不會實際執行這些程式碼,傳送給緩衝區管理器來執行。

1.2.8 緩衝區管理器讀取資料。

先在緩衝池的資料快取中檢查是否存在這些資料,如果存在,就把結果返回給儲存引擎的資料訪問方法;如果不存在,則從磁碟(資料檔案)中讀出資料並放入資料快取中,然後將讀出的資料返回給儲存引擎的資料訪問方法。

1.2.9 對於讀取資料,將會申請共享鎖,事務管理器分配共享鎖給讀操作。

1.2.10儲存引擎的資料訪問方法將查詢到的結果返回關係引擎的查詢執行器。

1.2.11 查詢執行器將結果返回給協議層。

1.2.12 協議層將資料封裝成TDS包,然後協議層將TDS包傳給客戶端。

2、索引底層剖析

2.1為何要有索引?

一般的應用系統,讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現效能問題,在生產環境中,我們遇到最多的,也是最容易出問題的,還是一些複雜的查詢操作,因此對查詢語句的優化顯然是重中之重。說起加速查詢,就不得不提到索引了。

2.2 什麼是索引?

索引在MySQL中也叫做“鍵”或者”key”(primary key,unique key,還有一個index key),是儲存引擎用於快速找到記錄的一種資料結構。索引對於良好的效能非常關鍵,尤其是當表中的資料量越來越大時,索引對於效能的影響愈發重要,減少io次數,加速查詢。(其中primary key和unique key,除了有加速查詢的效果之外,還有約束的效果,primary key 不為空且唯一,unique key 唯一,而index key只有加速查詢的效果,沒有約束效果)

索引優化應該是對查詢效能優化最有效的手段了。索引能夠輕易將查詢效能提高好幾個數量級。
索引相當於字典的音序表,如果要查某個字,如果不使用音序表,則需要從幾百頁中逐頁去查。

強調:一旦為表建立了索引,以後的查詢最好先查索引,再根據索引定位的結果去找資料

2.3 索引原理

索引的目的在於提高查詢效率,與我們查閱圖書所用的目錄是一個道理:先定位到章,然後定位到該章下的一個小節,然後找到頁數。相似的例子還有:查字典,查火車車次,飛機航班等,下面內容看不懂的同學也沒關係,能明白這個目錄的道理就行了。 那麼你想,書的目錄佔不佔頁數,這個頁是不是也要存到硬碟裡面,也佔用硬碟空間。

你再想,你在沒有資料的情況下先建索引或者說目錄快,還是已經存在好多的資料了,然後再去建索引,哪個快,肯定是沒有資料的時候快,因為如果已經有了很多資料了,你再去根據這些資料建索引,是不是要將資料全部遍歷一遍,然後根據資料建立索引。你再想,索引建立好之後再新增資料快,還是沒有索引的時候新增資料快,索引是用來幹什麼的,是用來加速查詢的,那對你寫入資料會有什麼影響,肯定是慢一些了,因為你但凡加入一些新的資料,都需要把索引或者說書的目錄重新做一個,所以索引雖然會加快查詢,但是會降低寫入的效率。

2.4 索引的資料結構

前面講了索引的基本原理,資料庫的複雜性,又講了作業系統的相關知識,目的就是讓大家瞭解,現在我們來看看索引怎麼做到減少IO,加速查詢的。任何一種資料結構都不是憑空產生的,一定會有它的背景和使用場景,我們現在總結一下,我們需要這種資料結構能夠做些什麼,其實很簡單,那就是:每次查詢資料時把磁碟IO次數控制在一個很小的數量級,最好是常數數量級。那麼我們就想到如果一個高度可控的多路搜尋樹是否能滿足需求呢?就這樣,b+樹應運而生。

3、Mysql鎖機制與事務隔離級別詳解

3.1 為什麼需要學習資料庫鎖知識

即使我們不會這些鎖知識,我們的程式在一般情況下還是可以跑得好好的。因為這些鎖資料庫隱式幫我們加了

  • 對於UPDATE、DELETE、INSERT語句,InnoDB自動給涉及資料集加排他鎖(X)
  • MyISAM在執行查詢語句SELECT前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖

只會在某些特定的場景下才需要手動加鎖,學習資料庫鎖知識就是為了:

  • 能讓我們在特定的場景下派得上用場
  • 更好把控自己寫的程式
  • 在跟別人聊資料庫技術的時候可以搭上幾句話
  • 構建自己的知識庫體系!在面試的時候不虛

3.2 表鎖簡單介紹

首先,從鎖的粒度,我們可以分成兩大類:

  • 表鎖
    • 開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突概率高,併發度最低
  • 行鎖
    • 開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖衝突的概率低,併發度高

不同的儲存引擎支援的鎖粒度是不一樣的:

  • InnoDB行鎖和表鎖都支援
  • MyISAM只支援表鎖

InnoDB只有通過索引條件檢索資料才使用行級鎖,否則,InnoDB將使用表鎖

  • 也就是說,InnoDB的行鎖是基於索引的

表鎖下又分為兩種模式

  • 表讀鎖(Table Read Lock)
  • 表寫鎖(Table Write Lock)
  • 從下圖可以清晰看到,在表讀鎖和表寫鎖的環境下:讀讀不阻塞,讀寫阻塞,寫寫阻塞
    • 讀讀不阻塞:當前使用者在讀資料,其他的使用者也在讀資料,不會加鎖
    • 讀寫阻塞:當前使用者在讀資料,其他的使用者不能修改當前使用者讀的資料,會加鎖!
    • 寫寫阻塞:當前使用者在修改資料,其他的使用者不能修改當前使用者正在修改的資料,會加鎖!
    • 寫鎖和其他鎖均布相容,只有讀和讀之間相容

從上面已經看到了:讀鎖和寫鎖是互斥的,讀寫操作是序列

  • 如果某個程式想要獲取讀鎖,同時另外一個程式想要獲取寫鎖。在mysql裡邊,寫鎖是優先於讀鎖的
  • 寫鎖和讀鎖優先順序的問題是可以通過引數調節的:max_write_lock_countlow-priority-updates

值得注意的是:

  • MyISAM可以支援查詢和插入操作的併發進行。可以通過系統變數concurrent_insert來指定哪種模式,在MyISAM中它預設是:如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個程式讀表的同時,另一個程式從表尾插入記錄。
  • 但是InnoDB儲存引擎是不支援的

    3.3 MVCC和事務的隔離級別

    資料庫事務有不同的隔離級別,不同的隔離級別對鎖的使用是不同的,鎖的應用最終導致不同事務的隔離級別

MVCC(Multi-Version Concurrency Control)多版本併發控制,可以簡單地認為:MVCC就是行級鎖的一個變種(升級版)。

  • 事務的隔離級別就是通過鎖的機制來實現,只不過隱藏了加鎖細節
    在表鎖中我們讀寫是阻塞的,基於提升併發效能的考慮,MVCC一般讀寫是不阻塞的(所以說MVCC很多情況下避免了加鎖的操作)

  • MVCC實現的讀寫不阻塞正如其名:多版本併發控制—>通過一定機制生成一個資料請求時間點的一致性資料快照(Snapshot),並用這個快照來提供一定級別(語句級或事務級)的一致性讀取。從使用者的角度來看,好像是資料庫可以提供同一資料的多個版本。
    快照有兩個級別:

  • 語句級
    針對於Read committed隔離級別

  • 事務級別
    針對於Repeatable read隔離級別
    我們在初學的時候已經知道,事務的隔離級別有4種:

  • Read uncommitted
    會出現髒讀,不可重複讀,幻讀

  • Read committed
    會出現不可重複讀,幻讀

  • Repeatable read
    會出現幻讀(但在Mysql實現的Repeatable read配合gap鎖不會出現幻讀!)

  • Serializable
    序列,避免以上的情況!


Read uncommitted會出現的現象—>髒讀:一個事務讀取到另外一個事務未提交的資料

  • 例子:A向B轉賬,A執行了轉賬語句,但A還沒有提交事務,B讀取資料,發現自己賬戶錢變多了!B跟A說,我已經收到錢了。A回滾事務【rollback】,等B再檢視賬戶的錢時,發現錢並沒有多。

  • 出現髒讀的本質就是因為操作(修改)完該資料就立馬釋放掉鎖,導致讀的資料就變成了無用的或者是錯誤的資料。
    Read committed避免髒讀的做法其實很簡單:

  • 就是把釋放鎖的位置調整到事務提交之後,此時在事務提交前,其他程式是無法對該行資料進行讀取的,包括任何操作
    但Read committed出現的現象—>不可重複讀:一個事務讀取到另外一個事務已經提交的資料,也就是說一個事務可以看到其他事務所做的修改

  • 注:A查詢資料庫得到資料,B去修改資料庫的資料,導致A多次查詢資料庫的結果都不一樣【危害:A每次查詢的結果都是受B的影響的,那麼A查詢出來的資訊就沒有意思了】
    上面也說了,Read committed是語句級別的快照!每次讀取的都是當前最新的版本!

Repeatable read避免不可重複讀是事務級別的快照!每次讀取的都是當前事務的版本,即使被修改了,也只會讀取當前事務版本的資料。

呃…如果還是不太清楚,我們來看看InnoDB的MVCC是怎麼樣的吧(摘抄《高效能MySQL》)

InnoDB 的 MVCC,是通過在每行記錄後面儲存兩個隱藏的列來實現的,這兩個列一個儲存了行的建立時間,一個儲存了行的過期(刪除)時間。當然儲存的並不是真正的時間值,而是系統版本號。每開始一個新的事務,系統版本號會自動遞增,事務開始的時候的系統版本號會作為事務的版本號,用來和查詢到的每行記錄的版本號進行比較。
select
  InnoDB 會根據以下兩個條件檢查每行記錄:
    a. InnoDB 只查詢版本早於當前事務版本的資料行,這樣可以確保事務讀取到的資料,要麼是在事務開始前就存在的,要麼是事務自身插入或更新的
    b. 行的刪除版本要麼未定義要麼大於當前事務版本號,確保了事務讀取到的行,在事務開始前未被刪除

至於虛讀(幻讀):是指在一個事務內讀取到了別的事務插入的資料,導致前後讀取不一致。

  • 注:和不可重複讀類似,但虛讀(幻讀)會讀到其他事務的插入的資料,導致前後讀取不一致
  • MySQL的Repeatable read隔離級別加上GAP間隙鎖已經處理了幻讀了。

1、Nginx定義

nginx 常用做靜態內容服務和反向代理伺服器,以及頁面前端高併發伺服器。適合做負載均衡,直面外來請求轉發給後面的應用服務(tomcat什麼的)

2、熟練掌握Nginx核心配置

2.1 全域性配置塊

user  root;  #執行worker程式的賬戶,user   使用者   [],預設以nobody賬戶執行
worker_processes  7;  #要使用的worker程式數,可設定為數值、auto(根據機器效能自動設定),預設值1

error_log  logs/error.log;  #nginx程式(master+worker)的日誌設定,儲存位置、輸出級別,此即為預設儲存位置
#error_log  logs/error.log  notice;  #輸出級別可選,由低到高依次為:debug(輸出資訊最多),info,notice,warn,error,erit(輸出資訊最少)

pid  logs/nginx.pid;  #nginx主程式的pid的儲存位置,此即為預設值

worker_rlimit_nofile 65535;  #單個worker程式可開啟的最大檔案描述符數

*worker_processes: *

實際運營時一般設定為很接近CPU的執行緒數,比如說CPU是8執行緒,一般設定為6、7。

我們自己開發、用時一般設定為1、2即可,不然太吃資源。

worker_rlimit_nofile:

r是read,limit是限制,單個worker程式最多隻能開啟指定個數的檔案,超過便不能再讀取檔案。開啟一次檔案便會產生一個檔案描述符。

此設定是為了防止單個worker程式消耗大量的系統資源。

ps -ef | grep nginx 查詢下nginx的程式:

不管設定多少個worker程式,主程式只有一個(即執行sbin/nginx)。

主程式由Linux當前登入的賬戶執行,工作程式由user指令指定的賬戶執行。第一列數字是程式的PID。

nginx工作程式和nginx主程式都是Linux中的程式,但主程式(父程式)可以控制worker程式(子程式)的開啟、結束。

master程式可以看做老闆,worker程式可以看做打工仔。

2.2 events塊

events {
   accept_mutex on;  #防止驚群
   multi_accept on;  #允許單個worker程式可同時接收多個網路連線的請求,預設為off
  use epoll;  #設定worker程式使用高效模式
   worker_connections 1024;  #指定單個worker程式最多可建立的網路連線數,預設值1024}

accept_mutex:

驚群現象:一個網路連線到來,所有沉睡的worker程式都會被喚醒,但只用一個worker處理連線,其餘被喚醒的worker又開始沉睡。

設定為on:要使用幾個worker就喚醒幾個,不全部喚醒,預設值就是on。

設定為off:一律全部喚醒。一片worker醒來是要佔用資源的,會影響效能。

use:

指定nginx的工作模式,可選的值:select、poll、kqueue、epoll、rtsig、/dev/poll。

其中select、poll都是標準模式,kqueue、epoll都是高效模式,

kqueue是在BSD系統中用的,epoll是在Linux系統中用的。(BSD是Unix的一個分支,Linux是一種類Unix系統)。

全域性塊中的worker_processes、events塊中的worker_connections是nginx支援高併發的關鍵,這2個數值相乘即nginx可建立的最大連線數。

一個連線要用一個檔案來儲存,

worker_connections設定的單個worker程式的最大連線數,受全域性塊中worker_rlimit_nofile設定的單個worker程式可開啟的最大檔案數限制。

而worker_rlimit_nofile只是nginx對單個worker程式的限制,要受Linux系統對單個程式可開啟的最大檔案描述符數限制。

Linux預設單個程式最多隻能開啟1024個檔案描述符,需要我們修改下Linux的資源限制,設定單個程式可開啟的最大檔案描述符數:

ulimit -n 65536

ulimit命令可以限制單個程式使用的系統資源的尺寸、數量,包括記憶體、緩衝區、套接字、棧、佇列、CPU佔用時間等。

可用ulimit –help檢視引數。

2.3 http塊

http{
    #http全域性塊
    #server塊
}

可以有多個server塊。

(1)http全域性塊

http全域性塊的配置作用於整個http塊(http塊內的所有server塊)。

include       mime.types;  #將conf/mime.types包含進來
    default_type  application/octet-stream;  #設定預設的MIME型別,二進位制流。如果使用的MIME型別在mime.types中沒有,就當作預設型別處理。
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '  
    #                '$status $body_bytes_sent "$http_referer" '
    #                '"$http_user_agent" "$http_x_forwarded_for"';   access_log logs/access.log; #設定日誌,這個日誌儲存的是客戶端請求的資訊,包括客戶端地址、使用的瀏覽器、瀏覽器核心版本、請求的url、請求時間、請求方式、響應狀態等。   #access_log  logs/access.log  main;  #可指定日誌格式,上面定義的main格式即預設格式。儲存位置預設是logs/access.log

    sendfile         on;  #開啟檔案高效傳輸模式,預設為off,不開啟。
    #tcp_nopush      on;  #如果響應體積過大,預設會分多個批次傳輸給客戶端,設定為on會一次性傳給客戶端,可防止網路阻塞
   #tcp_nodelay     on;   #如果響應體積過小,預設會放在緩衝區,緩衝區滿了才刷給客戶端,設定為on直接刷給客戶端,可防止網路阻塞
    keepalive_timeout  65;  #與客戶端保持連線的超時時間,在指定時間內,如果客戶端沒有向Nginx傳送任何資料(無活動),Nginx會關閉該連線。
    gzip  on;  #使用gzip模組壓縮響應資料。啟用後響應體積變小,傳輸到客戶端所需時間更少,節省頻寬,但nginx壓縮、客戶端解壓都有額外的時間、資源開銷,nginx的負擔也會加大。

    upstream  servers{  #設定負載均衡器,可同時設定多個負載均衡器。負載均衡器的名稱中不能含有_,此處指定名稱為servers
        server  192.168.1.7:8080;  #tomcat伺服器節點
        server  192.168.1.8:8081;        server  192.168.1.7:8080 down;  #down表示該節點下線,暫不使用     server  192.168.1.8:8081 backup;  #backup表示該節點是備胎,只有在其他節點忙不過來時才會啟用(比如一些節點出故障了、其他節點負載變大)。     server  192.168.1.8:8081 max_fails=3  fail_timeout=60s;  #如果對該節點的請求失敗3次,就60s內暫時不使用該節點,60s後恢復使用
    }

日誌格式常用的值:

  • $remote_addr 客戶端的ip地址
  • $time_local : 訪問時間與時區
  • $request : 請求的url與http協議
  • $status : 請求狀態,成功是200
  • $http_referer :從那個頁面連結訪問過來的
  • $http_user_agent :客戶端瀏覽器的資訊

(2)server模組

server{     #server全域性塊     listen       80;  #要監聽的埠
        server_name  localhost;  #虛擬主機(即域名),要在dns上註冊過才有效,沒有註冊的話只能用localhost。可指定多個虛擬主機,空格分開即可
        charset utf-8;  #使用的字符集。
        #access_log  logs/host.access.log  main;  #在http全域性塊、server全域性塊中任意一處設定日誌即可。http全域性塊已經設定了日誌,此處可不用設定。     #錯誤頁設定     error_page  404  /404.html;  #html目錄下預設只有index.html(nginx首頁)50x.html,需要自己寫404.html        location = /404.html {            root   html;  #指定404.html所在目錄,此處使用相對路徑,nginx主目錄下的html目錄,也可以使用絕對路徑        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }     #處理靜態資源     location ~* \.(html|css|js|gif|jpg|png|mp4)$ {  #使用正規表示式匹配url,如果請求的是這些檔案,就使用下面的處理方式            root static;    #如果使用nginx處理靜態資源,需使用root指定靜態資源所在目錄。在nginx主目錄下新建目錄static,把靜態資源放進去即可。       expires 30d;  #設定快取過期時間             #proxy_pass http://192.168.1.10:80;  #如果使用apache等其他機器處理靜態資源,使用proxy_pass轉發過去即可,多臺機器叢集時使用負載均衡器即可。        }
         #設定預設處理方式     location / {  #如果url沒有指定匹配,就使用預設的處理方式來處理            root   html;  #指定處理請求的根目錄。nginx本身作為web伺服器直接處理客戶端請求時,比如請求login.jsp,會呼叫root指定目錄下的login來處理請求。            index  index.html index.htm;  #指定nginx伺服器的首頁地址。root、index2項配置都是必需的。            proxy_pass http://servers; #指定要使用的負載均衡器,轉發給其中某個節點處理。如不設定此項(代理),則預設nginx本身作為web伺服器,直接處理請求,會到root指定目錄下找請求的檔案        }
    }

設定的錯誤頁面是nginx作為web伺服器(處理靜態資源)出現問題時,比如nginx上的靜態資源找不到,返回給客戶端的。

如果是tomcat出現的問題,比如tomcat上的xxx.jsp找不到,返回的是tomcat的錯誤頁面,不是nginx的。

如果使用nginx本身要作為web伺服器,直接處理客戶端請求,比如處理靜態資源,要將全域性塊中user 設定為執行nginx的賬戶(即當前登陸Linux的賬戶),

否則worker程式(預設nobody賬戶)無許可權讀取當前賬戶(即執行nginx主程式的賬戶)的靜態資源,客戶端會顯示403禁止訪問。

可以使用正規表示式來過濾客戶端ip,也可以把客戶端的ip過濾規則寫在檔案中,然後包含進來。

3、掌握Nginx負載演算法配置

(1)輪詢

將列表中的伺服器排成一圈,從前往後,找空閒的伺服器來處理請求。

輪詢適合伺服器效能差不多的情況。預設使用的就是輪詢,不需要設定什麼。

(2)加權輪詢

upstream  servers{
    server  192.168.1.7:8080 weight=1;
    server  192.168.1.8:8081 weight=2;
}

設定權重,權重大的輪到的機會更大,適合伺服器效能有明顯差別的情況。

(3)ip_hash

upstream  servers{
    ip_hash;
    server  192.168.1.7:8080;
    server  192.168.1.8:8081;
}

根據客戶端ip的hash值來轉發請求,同一客戶端(ip)的請求都會被轉發給同一個伺服器處理,可解決session問題。

(4)url_hash(第三方)

upstream  servers{
    hash $request_uri;
    server  192.168.1.7:8080;
    server  192.168.1.8:8081;
}

根據請求的url來轉發,會將url相同的請求轉發給同一伺服器處理。

一直處理某個url,伺服器上一般都有該url的快取,可直接從快取中獲取資料作為響應返回,減少時間開銷。

(5)fair(第三方)

upstream  servers{
    fair;
    server  192.168.1.7:8080;
    server  192.168.1.8:8081;
}

根據伺服器響應時間來分發請求,響應時間短的分發的請求多。

fair 公平,nginx先計算每個節點的平均響應時間,響應時間短說明該節點負載小(閒),要多轉發給它;響應時間長說明該節點負載大,要少轉發給它。

ip_hash、url_hash都是使用特定節點來處理特定請求,如果特定節點故障,nginx會剔除不可用的節點,將特定請求轉發給其它節點處理,url_hash影響不大,但ip_hash會丟失之前的session資料。

1、基礎引數設定

在server.xml中配置:

  • maxThreads:Tomcat使用執行緒來處理接收的每個請求。這個值表示Tomcat可建立的最大的執行緒數。
  • acceptCount:指定當所有可以使用的處理請求的執行緒數都被使用時,可以放到處理佇列中的請求數,超過這個數的請求將不予處理。
  • connnectionTimeout:網路連線超時,單位:毫秒。設定為0表示永不超時,這樣設定有隱患的。通常可設定為30000毫秒。
  • minSpareThreads:Tomcat初始化時建立的執行緒數。
  • maxSpareThreads:一旦建立的執行緒超過這個值,Tomcat就會關閉不再需要的socket執行緒

    2、Tomat的4種連線方式對比

    tomcat預設的http請求處理模式是bio(即阻塞型,下面第二種),每次請求都新開一個執行緒處理。下面做一個介紹
    <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" 
      connectionTimeout="20000" redirectPort="8443"/>
    <Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000"
      redirectPort="8443"/>
    <Connector executor="tomcatThreadPool"
      port="8081" protocol="HTTP/1.1"
      connectionTimeout="20000"
      redirectPort="8443" />
    <Connector executor="tomcatThreadPool"
      port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol"
      connectionTimeout="20000"
      redirectPort="8443" />
    我們姑且把上面四種Connector按照順序命名為 NIO, HTTP, POOL, NIOP。測試效能對比,數值為每秒處理的請求數,越大效率越高
    NIO   HTTP   POOL  NIOP
    281   65     208    365
    666   66     110    398
    692   65     66     263
    256   63     94     459
    440   67     145    363
    得出結論:NIOP > NIO > POOL > HTTP 雖然Tomcat預設的HTTP效率最低,但是根據測試次數可以看出是最穩定的。且這只是一個簡單頁面測試,具體會根據複雜度有所波動。

配置參考:Linux系統每個程式支援的最大執行緒數是1000,windos是2000。具體跟伺服器的記憶體,Tomcat配置的數量有關聯。

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
              maxThreads="500" minSpareThreads="25" maxSpareThreads="250"
              enableLookups="false" redirectPort="8443" acceptCount="300" connectionTimeout="20000" disableUploadTimeout="true"/>  

3、Tomcat的叢集

Tomcat的部署,是一臺伺服器部署一個Tomcat(上線多個專案),還是一臺伺服器部署多個tomact(每個tomcat部署1~n個專案)。多核必選配置多個Tomcat,微服務多執行緒的思想模式。

4、Tomcat記憶體設定

修改/bin/catalina.sh,增加如下設定:

JAVA_OPTS='-Xms【初始化記憶體大小】 -Xmx【可以使用的最大記憶體】'

需要把這個兩個引數值調大,大小的可以根據伺服器記憶體的大小進行調整。例如:

JAVA_OPTS='-Xms1024m –Xmx2048m'

伺服器是8G 記憶體,跑了3個tomcat服務,給分配了2G的記憶體,因為還有其他程式。


本篇文章寫到這裡差不多就結束了,當然也有很多東西還沒有寫到,不過限於篇幅也是沒轍,我整理了很詳細的JVM、MySQL、NGINX和Tomcat的學習筆記以及資料,需要的朋友直接點選領取就可以了。

最後,碼字不易,所以,可以點個贊和收藏嗎兄弟們!


end

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章