一條更新語句如:update T set c = c + 1 where ID = 2;和查詢的流程差很少,可是更新流程還設計到兩個重要的日誌模塊:分別是redo log(重作日誌)和binlog(歸檔日誌).mysql
當有一條記錄須要更新的時候,InnoDB引擎就會把記錄寫到redo log(粉板)裏面,並更新內存,InnoDB引擎會在適當的時候,將這個操做記錄更新到磁盤裏面,而這個更新每每是在系統比較閒的時候作,這就像打烊之後的掌櫃作的事,sql
InnoDB的redo log是固定大小的,好比能夠配置一組4個文件,每一個文件的大小是1GB,那麼這個「粉板」總共就能夠記錄4GB的操做,從頭開始寫,寫到末尾就又回到開頭循環寫,以下圖:數據庫
有了redo log,InnoDB就能夠保證即便數據庫發生異常重啓,以前提交的記錄都不會丟失,這個能力稱之爲crash-safe;數組
##binlog redo log是引擎特有的日誌,server層也有本身的日誌,稱爲binlog(歸檔日誌)安全
這兩種日誌的不一樣點:性能優化
update語句的內部流程:bash
將redo log的寫入拆成兩個步驟:prepare和commit ,這就是兩階段提交.服務器
若是不採用兩階段提交產生的後果:數據結構
哈希表架構
是一種以鍵-值(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裏面對應一顆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裏,非主鍵索引也被稱爲二級索引
主鍵索引和普通索引的查詢區別:
自增主鍵的插入數據模式,符合遞增插入,沒插入一條新紀錄,都是追加操做,都不涉及到挪動其餘記錄,也不會觸發葉子結點的分裂
而用有業務邏輯的字段作主鍵,每每不容易保證有序插入,這樣寫數據成本相對較高
從存儲空間的角度看,假設表中確實有一個惟一的字段,好比字符串類型的身份證號,那就應該用身份證號作主鍵?因爲每一個非主鍵索引的葉子節點上都是主鍵的值.因此用身份證號作主鍵,那麼每一個二級索引的葉子節點佔用約20個字節,而若是用整型作主鍵,則只要4個字節,若是是長整型(bigint)則是8個字節
顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引佔用的空間也越小
有些場景適合用業務字段直接作主鍵:
InnoDB採用的是B+樹結構,B+樹可以很好的配合磁盤的讀寫特性,減小單次查詢的磁盤訪問次數,因爲InnoDB是索引組織表,通常狀況下建立一個自增主鍵,這樣非主鍵索引佔用的空間最小
如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語句的執行流程:
在這個過程:回到主鍵索引樹搜索的過程,稱爲回表
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替代
根據加鎖的範圍,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再經過重試命令重複這個過程.
在InnoDB事物中,行鎖是在須要的時候才加上的,當並非不須要了就馬上釋放,而是要等到事物結束時才釋放,這個就是兩階段鎖協議
若是事物中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖儘可能日後放.
當出現死鎖後,有兩種策略:
因爲惟一索引用不上change buffer的優化機制,所以若是業務能夠接收,從性能角度出發建議使用非惟一索引.
若是某次寫入使用了change buffe機制,以後主機異常重啓,是否會丟失change buffer和數據?
雖然只是更新內存,可是在事物提交的時候,咱們把change buffer的操做也記錄到redo log裏了,因此崩潰恢復的時候,change buffer也能找回來.
咱們平時不斷地刪除歷史數據和新增數據,MySQL可能會選錯索引.
例如,咱們要根據郵箱查學生的信息,若是沒有建立索引,那麼就會全表掃描,若是在郵箱上建立索引
一、alter table SUser add index index1(email);
二、alter table SUser add index index2(email);
第一個語句建立在index1索引裏面,包含了每一個記錄的整個字符串;而第二個語句建立的index2索引裏面, 對於每一個記錄都是隻取前6個字節.
index1(即email整個字符串的索引結構):
index2(即email(6)索引結構):
使用前綴索引, 定義好長度,既能夠節省空間,又不用額外增長太多的查詢成本.
對比如下兩個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索引再查一下,由於系統並不肯定前綴索引的定義是否截斷了完整信息.
小結:
InnoDB的數據是按頁存儲的,刪掉一個數據頁上的全部數據記錄,整個數據頁就能夠被複用. 可是數據頁的複用和記錄的複用不一樣,若是把ID爲400的行刪除,再插入一個ID是800的行,就不能複用這個位置.而當整個頁從B+樹裏摘掉之後,能夠複用到任何位置,若是將數據頁pageA上的全部記錄刪除後,pageA會被標記爲可複用,這時候若是要插入一條ID=50的記錄須要用新頁的時候,pageA是能夠被複用的.若是相鄰的兩個數據頁利用率都很小,系統就會把這兩個頁上的數據合到其中的一個頁上,另一個數據頁也就被標記爲可複用.所以delete命令其實只是把記錄的位置或數據頁標記爲了「可複用」,但磁盤文件大小是不會變的.
重建表能夠解決數據空洞的問題(pageA滿了,在插入一個ID是500的數據時,就不得不申請一個新的頁面pageB來保存數據了,頁分裂完成後,pageA的末尾就留下空洞了)
** 解決辦法**
使用show processlist查看哪些事物是空閒的,
有些業務代碼會在短期內先大量申請數據庫鏈接作備用,若是確認是被數據庫打掛了,那麼一種可能的作法是讓數據庫跳過權限驗證階段.
在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,確保每一個語句都會被記錄入慢查詢日誌;