極客時間-MySQL實戰45講學習筆記

01|基礎架構:一條sql查詢語句是如何執行的

mysql能夠分爲sever層和存儲引擎層兩層

02|日誌系統:一條sql更新語句是如何執行的

一條更新語句如:update T set c = c + 1 where ID = 2;和查詢的流程差很少,可是更新流程還設計到兩個重要的日誌模塊:分別是redo log(重作日誌)和binlog(歸檔日誌).mysql

redo log

當有一條記錄須要更新的時候,InnoDB引擎就會把記錄寫到redo log(粉板)裏面,並更新內存,InnoDB引擎會在適當的時候,將這個操做記錄更新到磁盤裏面,而這個更新每每是在系統比較閒的時候作,這就像打烊之後的掌櫃作的事,sql

InnoDB的redo log是固定大小的,好比能夠配置一組4個文件,每一個文件的大小是1GB,那麼這個「粉板」總共就能夠記錄4GB的操做,從頭開始寫,寫到末尾就又回到開頭循環寫,以下圖:數據庫

有了redo log,InnoDB就能夠保證即便數據庫發生異常重啓,以前提交的記錄都不會丟失,這個能力稱之爲crash-safe;數組

##binlog redo log是引擎特有的日誌,server層也有本身的日誌,稱爲binlog(歸檔日誌)安全

這兩種日誌的不一樣點:性能優化

  • redo log是InnoDB引擎特有的;binlog是mysql的server層實現的,全部引擎均可以使用;
  • redo log是物理日誌,記錄的是「某個數據頁上作了什麼修改」;binlog是邏輯日誌,記錄的是這個語句的原始邏輯,好比「給ID=2這一行的c字段加1」;
  • redo log是循環寫的,空間固定會用完;binlog是能夠追加寫入的.「追加寫」是指binlog文件寫到必定大小後會切換到下一個,並不會覆蓋之前的日誌.

update語句的內部流程:bash

  • 一、執行器先找到引擎取ID=2這一行,ID是主鍵,引擎直接用樹索引找到這一行,若是ID=2這一行所在的數據頁原本就在內存中,就直接返回給執行器;不然,須要先從磁盤中讀入內存,而後再返回.
  • 執行器拿到引擎給的行數據,把這個值加上1,好比原來是N,如今就是N+1,獲得新的一行數據,再調用引擎接口寫入這行新數據.
  • 引擎將這行新數據更新到內存中,同時將這個更新操做記錄到redo log裏面,此時redo log處於prepare狀態.而後告知執行器執行完成了,隨時能夠提交事物.
  • 執行器生成這個操做的binlog,並把binlog寫入磁盤.
  • 執行器調用引擎的提交事物接口,引擎把剛剛寫入的redo log改爲提交狀態,更新完成.

將redo log的寫入拆成兩個步驟:prepare和commit ,這就是兩階段提交.服務器

若是不採用兩階段提交產生的後果:數據結構

  • 先寫redo log後寫binlog,假設redo log寫完,binlog尚未寫完的時候,mysql進程異常重啓,redo log寫完後,系統即便崩潰了,仍然可以把數據恢復回來,因此恢復後這一行c的值是1. 可是因爲binlog沒寫完就crash了,這時候binlog裏面沒有這個記錄這個語句,所以,以後備份日誌的時候,存起來的binlog裏面就沒有這條語句果用這個binlog來恢復臨時庫的話,因爲這個語句的binlog丟失,臨時庫就會少一次更新,恢復出來的這行c的值就是0,與原庫的值不一樣.
  • 先寫binlog後寫redo log,若是binlog寫完以後crash,因爲redo log還沒寫,崩潰恢復之後這個事務無效,因此這一行c的值是0,可是binlog裏面已經記錄了把c從0改爲了1這個日誌,因此,在以後用binlog來恢復的時候就多了一個事務出來,恢復出來的這一行c的值就是1,與原庫的值不一樣.

04|深刻淺出索引(上)

常見的索引模型

哈希表架構

是一種以鍵-值(key-value)存儲數據的結構,咱們只要輸入待查找的值即key,就能夠找到其對應的值即value.哈希的思路:把值放在數組裏,用一個哈希函數把key換算成一個肯定的位置,而後把value放在數組的這個位置.

不可避免,多個key值通過哈希函數的換算,會出現同一個值的狀況.處理這種狀況的一種方法是,拉出一個鏈表.

哈希表這種結構適用於只有等值查詢的場景,好比Memcached及其餘一些NoSQL引擎.

有序數組 假設身份證號沒有重複,這個數組就是按照身份證號遞增的順序保存的.這時候若是要查ID_card_n2對應的名字,用二分法就能夠快速獲得,這個時間複雜度是O(log(N)).

有序數組支持範圍查詢,要查詢身份證號在[ID_car_X,ID_card_Y]區間的User,能夠先用二分法找到ID_card_X(若是不存在ID_card_X,就找到大於ID_card_X的第一個User),而後向右遍歷,直到查到第一個大於ID_card_Y的身份證號,推出循環.

若是僅僅看查詢效率,有序數組就是最好的數據結構,可是,在須要更新數據的時候就麻煩了,往中間插入一個記錄就必須得挪動後面全部的記錄,成本過高.

因此,有序數組只適用於靜態存儲引擎,好比要保存的是2017年某個城市的全部人口信息,這類不會再修改的數據

二叉搜索樹

二叉搜索樹的特色是:每一個節點的左兒子小於父節點,父節點又小於右兒子.這樣要查詢ID_card_n2的話,按照圖中搜索順序就是按照UserA->UserC->UserF->User2這個路徑獲得,這個時間複雜度是O(log(N)).

固然爲了維持O(log(N))的複雜度,須要保持這棵樹是平衡二叉樹,爲了作這個保證,更新的時間複雜度也是O(log(N)).

二叉樹是搜索效率最高的,可是實際上大多數的數據庫存儲不是使用二叉樹,緣由是索引不止存在內存中,還要寫到磁盤上.能夠想象一顆100萬節點的平衡二叉樹,樹高20,一次查詢可能須要訪問20個數據快.在機械硬盤時代,從機械硬盤隨機讀取一個數據塊須要10ms左右的尋址時間.也就是說,對於一個100萬行的表,若是使用二叉樹來存儲,單獨訪問一行可能須要20個10ms的時間,這樣查詢就比較慢了

爲了讓一個查詢儘可能少的讀磁盤,就必須讓查詢過程訪問儘可能少的數據塊.那麼,咱們就不該該使用二叉樹,而是使用"N"叉樹,這裏的N取決於數據塊的大小.

以InnoDB的一個整數字段索引爲例,這個N差很少是1200,這棵樹高是4的時候,就能夠存1200的2次方個值,這已是17億了.考慮到樹根的數據塊老是在內存中,一個10億行的表上一個整數字段的索引,查找一個值最多隻須要訪問3次磁盤.

InnoDB的索引模型

在InnoDB中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱爲索引組織表.

每個索引在InnoDB裏面對應一顆B+樹

假設,咱們有一個主鍵列爲 ID 的表,表中有字段 k,而且在 k 上有索引。

建表語句爲:

mysql> create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;
複製代碼

表中 R1~R5 的 (ID,k) 值分別爲 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示意圖以下:

根據葉子節點的內容,索引類型分爲主鍵索引和非主鍵索引

主鍵索引的葉子節點存的是整行數據,在InnoDB裏,主鍵索引也被稱爲聚簇索引

非主鍵索引的葉子節點內容是主鍵的值,在InnoDB裏,非主鍵索引也被稱爲二級索引

主鍵索引和普通索引的查詢區別:

  • 若是語句是select * from T where ID = 500,即主鍵查詢方式,則只須要搜索ID這顆B+樹;
  • 若是語句是select * from T where K = 5,即普通索引查詢方式,則須要先搜索K索引樹,獲得ID的值爲500,再到ID索引樹搜索一次.這個過程稱爲回表. 也就是說,基於非主鍵索引的查詢須要多掃描一顆索引樹,所以,咱們在應用中應該儘可能使用主鍵查詢

索引維護

自增主鍵的插入數據模式,符合遞增插入,沒插入一條新紀錄,都是追加操做,都不涉及到挪動其餘記錄,也不會觸發葉子結點的分裂

而用有業務邏輯的字段作主鍵,每每不容易保證有序插入,這樣寫數據成本相對較高

從存儲空間的角度看,假設表中確實有一個惟一的字段,好比字符串類型的身份證號,那就應該用身份證號作主鍵?因爲每一個非主鍵索引的葉子節點上都是主鍵的值.因此用身份證號作主鍵,那麼每一個二級索引的葉子節點佔用約20個字節,而若是用整型作主鍵,則只要4個字節,若是是長整型(bigint)則是8個字節

顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引佔用的空間也越小

有些場景適合用業務字段直接作主鍵:

  • 一、只有一個索引
  • 二、該索引必須是惟一索引

InnoDB採用的是B+樹結構,B+樹可以很好的配合磁盤的讀寫特性,減小單次查詢的磁盤訪問次數,因爲InnoDB是索引組織表,通常狀況下建立一個自增主鍵,這樣非主鍵索引佔用的空間最小

05|深刻淺出索引(下)

如select * from T where k between 3 and 5; 建表語句:

mysql>create table T(
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine = InnoDB;

insert into T values(100,1,'aa'),(200,2,'bb'),(300,3,'cc'),(400,4,'dd'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg')
複製代碼

sql語句的執行流程:

  • 一、在k索引樹上找到k = 3的記錄,取得ID=300;
  • 二、再到ID索引樹查到ID=300對應的R3;
  • 三、在K索引樹下取下一個值k=5,取得ID=500;
  • 四、再回到ID索引樹查到ID=500對應的R4;
  • 四、在k索引樹取下一個值k=6,不知足條件,循環結束.

在這個過程:回到主鍵索引樹搜索的過程,稱爲回表

覆蓋索引

select ID from T where between 3 and 5,這時只須要查ID的值,而ID的值已經在K索引樹上了,所以能夠直接提供查詢結果,不須要回表,也就是說在這個查詢裏,索引k已經「覆蓋了」咱們的查詢需求,咱們稱爲覆蓋索引.

覆蓋索引能夠減小樹的搜索次數,顯著提高查詢性能,因此使用覆蓋索引是一個經常使用的性能優化手段

最左前綴原則

B+樹這種索引結構,能夠利用索引的‘最左前綴,來定位記錄’

在創建聯合索引的時候,如何安排索引內的字段的順序,第一原則是:若是經過調整順序,能夠少維護一個索引,那麼這個順序每每就是須要優先考慮採用的

在MySQL5.6引入的索引下推優化,能夠在索引遍歷過程當中,對索引中包含的字段先作判斷,直接過濾掉不知足條件的記錄,減小回表次數

對於例子中的InnoDB表T,若是要重建索引K,sql語句能夠是:

alter table T drop index k;
alter table T add index(k);
複製代碼

若是重建主鍵索引,能夠這麼寫

alter table T drop primary key;
alter table T add primary key(id);
複製代碼

對於這兩種重建索引的作法,重建索引k的作法是合理的,可是重建主鍵的過程不合理,不管是刪除主鍵仍是建立主鍵,都會將整個表重建.能夠用alter table T engine=InnoDB替代

06|全局鎖和表鎖:給表加個字段怎麼有這麼多阻礙

根據加鎖的範圍,MySQL裏面的鎖大體能夠分爲全局鎖、表級鎖和行鎖三類

全局鎖

mysql加全局讀鎖的方法:Flush talbes with read lock(FTWRL),

全局鎖的典型使用場景,作全庫邏輯備份,也就是把整庫每一個表都select出來存成文本.

mysql官方自帶的邏輯備份工具是mysqldump,當mysqldump使用參數-single-transaction的時候,導數據以前就會啓動一個事物,來確保拿到一致性視圖,因爲mvcc的支持,這個過程當中數據是能夠正常更新,可是single-transaction方法只適用於全部的表使用事物引擎的庫不支持事物的引擎,那麼備份就只能經過FTWRL方法

表級鎖

表級鎖有兩種:表鎖和元數據鎖

表鎖的語法是lock tables ... read/write ,釋放鎖:unlock tales

另外一類表級的鎖是MDL,MDL的做用是,保證讀寫的正確性,在mysql 5.5版本中引入來MDL,當對一個表作增刪改查操做的時候,加MDL讀鎖;當對錶作結構變動操做的時候,加MDL寫鎖.

  • 讀鎖之間不互斥,所以能夠有多個線程同時對一張表增刪改查.
  • 讀寫鎖之間、寫鎖之間是互斥的,用來保證變動表結構操做的安全性,所以,若是有兩個線程要同時給一個表加字段,其中一個要等另外一個執行完才能開始執行.

如何安全地給小表加字段: 首先解決長事物,事物不提交,就會一直佔着MDL鎖;

在alter table語句裏面設定等待時間,若是在這個指定的等待時間裏面可以拿到MDL寫鎖最好,拿不到也不阻塞後面的業務語句,先放棄,以後開發人員或者DBA再經過重試命令重複這個過程.

07|行鎖功過:怎麼減小行鎖對性能的影響?

在InnoDB事物中,行鎖是在須要的時候才加上的,當並非不須要了就馬上釋放,而是要等到事物結束時才釋放,這個就是兩階段鎖協議

若是事物中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖儘可能日後放.

死鎖和死鎖檢測

當出現死鎖後,有兩種策略:

  • 一種策略是,直接進入等待,直到超時,這個超時時間能夠經過參數innodb_lock_wait_timeout來設置.
  • 另外一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事物,讓其餘事物得以繼續執行.將參數innodb_deadlock_detect設置爲on,表示開啓這個邏輯.

09|普通索引和惟一索引,應該怎麼選擇?

因爲惟一索引用不上change buffer的優化機制,所以若是業務能夠接收,從性能角度出發建議使用非惟一索引.

若是某次寫入使用了change buffe機制,以後主機異常重啓,是否會丟失change buffer和數據?

雖然只是更新內存,可是在事物提交的時候,咱們把change buffer的操做也記錄到redo log裏了,因此崩潰恢復的時候,change buffer也能找回來.

10|MySQL爲何有時候會選錯索引

咱們平時不斷地刪除歷史數據和新增數據,MySQL可能會選錯索引.

索引選擇異常和處理

  • 採用force index強行選擇一個索引
  • 能夠修改語句,引導mysql使用咱們指望的索引,如將order by limit 1改爲order by b,a limit 1
  • 新建一個更合適的索引,來提供給優化器作選擇,或刪除誤用的索引

11|怎麼給字符串字段加索引

例如,咱們要根據郵箱查學生的信息,若是沒有建立索引,那麼就會全表掃描,若是在郵箱上建立索引

一、alter table SUser add index index1(email);

二、alter table SUser add index index2(email);

第一個語句建立在index1索引裏面,包含了每一個記錄的整個字符串;而第二個語句建立的index2索引裏面, 對於每一個記錄都是隻取前6個字節.

index1(即email整個字符串的索引結構):

  • 從index1索引樹找到知足索引值'zhangsanxyz@qq.com'的這條記錄,取得ID2的值;
  • 到主鍵上查到主鍵值是ID2的行,判斷email的值是否正確的,將這行記錄加入結果集;
  • 取index1索引樹上剛剛查到的位置的下一條記錄,發現已經不知足email= zhangsanxyz@qq.com'的條件了,循環結束.

index2(即email(6)索引結構):

  • 從index2索引找到知足索引值是'zhangs'的記錄,找到第一個是ID1;
  • 到主鍵上查到主鍵值是ID1的行,判斷出email的值不是'zhangsanxyz@qq.com',這行記錄丟棄;
  • 取index2上剛剛查到的位置的下一條記錄,發現仍然是'zhangs',取出ID2,再到ID索引上取整行而後判斷,此次值對了,將這行記錄加入結果集;
  • 重複上一步,直到index2上取到的值不是'zhangs'時,循環結束;

使用前綴索引, 定義好長度,既能夠節省空間,又不用額外增長太多的查詢成本.

前綴索引對覆蓋索引對影響

對比如下兩個sql語句:

select id,email from SUser where email='zhangssxyz@qq.com'

select id,email,name from SUser where email='zhangssxyz@qq.com'

使用index1能夠利用覆蓋索引,從index1查詢到結果後直接就返回了,不須要回到ID索引再去查一次.而若是使用index2的話,就不得不回到ID索引再去判斷email字段的值.

即便將index2定義的定義修改成email(18)的前綴索引,這時候雖然index2已經包含了全部的信息,但InnDB仍是要回到id索引再查一下,由於系統並不肯定前綴索引的定義是否截斷了完整信息.

小結:

  • 直接建立完整索引,這樣可能比較佔用空間;
  • 建立前綴索引,節省空間,但會增長查詢掃描次數,而且不能使用覆蓋索引;
  • 倒序存儲,在建立前綴索引,用於繞過字符串自己前綴的區分度不夠的問題;
  • 建立hash字段索引,查詢性能穩定,有額外的存儲和計算消耗,跟第三種方式同樣,都不支持範圍掃描

13|爲何表數據刪掉一半,表文件大小不變

InnoDB的數據是按頁存儲的,刪掉一個數據頁上的全部數據記錄,整個數據頁就能夠被複用. 可是數據頁的複用和記錄的複用不一樣,若是把ID爲400的行刪除,再插入一個ID是800的行,就不能複用這個位置.而當整個頁從B+樹裏摘掉之後,能夠複用到任何位置,若是將數據頁pageA上的全部記錄刪除後,pageA會被標記爲可複用,這時候若是要插入一條ID=50的記錄須要用新頁的時候,pageA是能夠被複用的.若是相鄰的兩個數據頁利用率都很小,系統就會把這兩個頁上的數據合到其中的一個頁上,另一個數據頁也就被標記爲可複用.所以delete命令其實只是把記錄的位置或數據頁標記爲了「可複用」,但磁盤文件大小是不會變的.

重建表能夠解決數據空洞的問題(pageA滿了,在插入一個ID是500的數據時,就不得不申請一個新的頁面pageB來保存數據了,頁分裂完成後,pageA的末尾就留下空洞了)

22|MySQL有哪些「飲鳩止渴」提升性能的方法?

短連接風暴

** 解決辦法**

先處理掉哪些佔着的鏈接可是不工做的線程

使用show processlist查看哪些事物是空閒的,

減小鏈接過程的消耗

有些業務代碼會在短期內先大量申請數據庫鏈接作備用,若是確認是被數據庫打掛了,那麼一種可能的作法是讓數據庫跳過權限驗證階段.

慢查詢性能問題

在mysql中,會引起性能問題的慢查詢,大致有如下三種可能:

  • 索引沒有設計好
  • sql語句沒有寫好
  • mysql選錯了索引

對於索引沒有設計好,解決方法:假設服務器是一主一備,主庫A、備庫B,能夠如今備庫上執行set sql_log_bin=off ,也就是不寫binlog,而後執行alter table語句加上索引;執行主備切換;這時候主庫是B,備庫是A,在A上執行set sql_log_bin=off,而後執行alter table語句加上索引,更好的方案是相似gh-ost這樣的方案,更加穩妥.

針對sql語句沒有寫好,解決方法:mysql 5.7提供query_rewrite功能,能夠把輸入的一種語句改寫成另一種模式 好比語句錯誤地寫成了select * from t where id + 1=10000,能夠經過insert into query_rewrite.rewrite_rules(pattern,replacement,pattern_database) values("select * from t where id + 1= ?","select * from t where id = ? - 1","db1"); call query_rewrite.flush_rewrite_rules();

call query_rewrite.flush_rewrite_rules();這個存儲過程,是讓插入的新規則生效,也就是「查詢重寫」

上線前能夠在測試環境把慢查詢日誌(slow log)打開,而且把long_query_time設置成0,確保每一個語句都會被記錄入慢查詢日誌;

相關文章
相關標籤/搜索