MySQL實戰-1

 

目錄

SQL查詢

SQL更新

事務

索引

全局鎖,表鎖,行鎖

細說事務隔離機制

普通索引和唯一索引

索引選擇的問題

前綴索引

刷髒頁的過程

表數據的存儲方式

count(*)執行原理

參考


SQL查詢

一條SQL查詢是如何執行的

1.客戶端經過連接器連上mysql,校驗用戶名/密碼,如果連上之後長時間沒操作會自動斷開的
2.查詢緩存,對錶變化不大的可以用,表經常變動的一個update就會清空緩存反而效率變低
3.分析器,做sql語法分析語法校驗,弄出一個完整的語法樹
4.優化器,對選擇索引的優化,如果是兩個表join會判斷先讀哪個表
5.執行器,去存儲引擎中做真正的操作

 

SQL更新

redo log
相當於WAL(Write-Ahead Logging),先寫日誌再寫磁盤,可以保證crash後不丟數據,寫的是物理page
redo log大小是固定的,4個文件來回寫,如果write_pos的位置超過了check_point,則需要等待flush到磁盤

 


bin log
是Server級別,不是引擎級別的,所有的引擎都可以用
bin log記錄的是邏輯改動,是append的,用來做回放而不是恢復

一個update流程
1.執行器先找引擎獲取ID=2,ID是主鍵引起直接用樹搜索找到這一行,如果在內存中直接返回否則讀磁盤
2.執行器拿到後+1,再調用引起接口寫入這行數據
3.引擎將新數據更更新到內存中,再將更新操作記錄到redo log中,此時redo log是prepare狀態
4.執行器生成這個操作的bin log,並寫入磁盤
5.執行器調用引擎的提交事務接口,引擎把redo log改成commit狀態
完整的流程圖如下,淺色部分表示在InnoDB內部執行的,深色部分是在執行器中執行的

 

兩階段提交
bin log+全量備份數據(假設半個月),可以恢復到本個月內任意一秒的狀態
找到全量備份數據,再重放binlog
如果是增加一個讀庫,也是類似的操作
假設不用兩階段提交,肯定是先寫bin log或者先寫redo log,那麼
1.先寫redo log再寫bin log
redo log已經記錄了,但bin log失敗,系統崩潰後數據可以恢復,但bin log沒記錄,如果要增加從庫就少了記錄
2.先寫bin log再寫redo log
崩潰後數據就無法恢復,事務無效,但bin log記錄後增加從庫數據又有了,導致跟原庫數據不一致

加上prepare後
1.先寫redo log  如果這部失敗了,那事務就無法恢復了,如果成功了則可以恢復
2.再寫bin log,如果bin log失敗,bin log和redo log用一個事務id,如果發現bin log失敗了,則不會恢復redo log,這樣保證了一致性
3.commit之後,redo log和bin log最終一致,無論機器crash還是加從庫都沒問題
對於特殊情況,比如第二步,bin log寫完之後機器crash了,重啓後發現雖然沒有commit,但是redo log和bin log事務id一樣,而且都是寫入成功,那麼就可以恢復,再次commit
 

 

事務

可能出現的事務問題
髒讀 ditry read
不可重複讀 non-repeatable read
幻讀 phantom read

事務的隔離級別
讀未提交 read uncommitted
讀提交   read committed
可重複讀 repeatable read
串行化   serializable

讀未提交,一個事務還沒提交時,它做的變更就能被別的事務看到
讀提交,  一個事務提交之後,他做的更變纔會被其他事務看到
可重複讀,一個事務執行過程中看到的數據,總是跟這個事務在啓動時看到的數據是一致的
串行化,  對於同一行記錄,寫會加寫鎖,讀會加都鎖,當出現讀寫鎖衝突的時候,後訪問的事務要等前面執行完才能繼續執行

對於讀提交,可重複讀 假設有下面一段sql

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);

假設有兩個線程開啓了兩個事務,他們都做同樣的事情

對於上圖來說,四個隔離級別有四種情況
讀未提交,V1的值就是2,事務B雖然沒提交,但A可以看到B修改的值,因此V2,V3都是2
讀提交,  V1是1,V2的值是2,事務B的更新提交後才能被A看到,V3也是2
可重複讀,V1,V2都是1,V3是2,V2是1的原因遵循的是事務在執行期間前後看到的數據一致
串行化,  事務B執行"1改爲2"會被鎖住,直到事務A提交後,事務B纔可以繼續執行,從A的角度看,V1,V2都是1,V3是2

在實現上 數據庫會創建一個“視圖”,訪問的時候以“視圖”的邏輯結果爲準
讀未提交,直接返回記錄的最新值,沒有視圖的概念
讀提交,  這個視圖是在每個SQL語句開始的時候創建
可重複讀,這個視圖是在事務啓動的時候創建,整個事務存在期間都用這個視圖
串行化,  沒有視圖,直接用枷鎖的方式避免並行訪問

通過下面方式查詢當前的事務隔離界別

mysql> show variables like 'transaction_isolation';

可重複讀的使用場景
假設有一個表存了每個月的月底餘額,一個表存了賬單明細
如果要做數據校對,判斷上個月的餘額和當前餘額的差額,是否跟本月的賬單明細一致
那麼在校驗過程中,即使有用戶發生了一筆新的交易,也不影響校對結果

事務隔離的實現
每條記錄在更新的時候都會同時記錄一條回滾操作,記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值
假設一個值從1被按順序改成了2,3,4 那麼回滾日誌中就會有類似下面的記錄

當前值是4,但不同時刻啓動的事務會有不同的read-view
視圖A,B,C裏面這一紀錄分別是1,2,4 同一個記錄在系統中可以存在多個版本,這個就是數據庫的多版本併發控制MVCC
對於read-view A要得到1,就必須將當前的值依次執行圖中鄋回滾操作才能得到
當沒有事務在需要這些回滾日誌時會被刪除,也就是系統裏沒有比這個回滾日誌更早的read-view時
長事務意味着系統裏會存在很老的事務視圖,這裏面就會記錄很多回滾日誌,導致佔用大量的存儲空間

事務的啓動方式
1.顯示的啓動事務,如begin或start transaction,以及commit還有rollback
2.設置autocommit=0,這個命令會將這個現場的自動提交關閉,意味着執行一個select,這個事務就啓動了,而且不會自動提交,這個事務會持續存在直到主動commit或者rollback,或者斷開連接

有的客戶端連接框架在連接成功後會先執行一個set autocommit=0,這導致接下來的查詢都在事務中,如果是長連接,會導致以爲的長事務
所以建議顯示的開啓 事務 set autocommit=1
頁可以設置
commit work and chain
提交事務並自動啓動下一個事務,這樣省去了再次執行begin的開銷,同時從程序開發的角度也明確的知道每個語句是否處於事務中

可以在infomation_schema庫的innodb_trx 表中查詢長事務,sql如下

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

 

索引

常見的索引模型
1.Hash,查詢某個值很方便,衝突了就再散列或者加拉鍊(樹化),但無法做遍歷
2.有序數組,對數時間定位到某個值,支持範圍查詢,但增刪比較麻煩
3.二叉搜索樹,增加/刪除都是對數時間
存儲系統則改爲用B+樹,是多個子節點,現在LSM樹,跳錶也用於存儲結構

InnoDB索引
mysql的的不同引擎的索引方式不同,以InnoDB爲列,他包含主鍵索引和普通索引

主鍵索引也成爲聚集索引clustered index
非主鍵索引的葉子節點內容是主鍵的值,也成爲二級所以你secondary index
參考下面的sql,主鍵查詢的方式,只需要搜索ID這顆B+樹

select * from T where ID=500;  

如果是普通索引,需要先搜索k索引樹,得到ID的值爲500,再到ID索引樹搜索一次,這個過程叫回表

select * from T where ID=500;

索引維護

索引的插入和刪除,都需要重新維護這個B+樹
自增主鍵的好處是追加操作,不涉及挪動其他記錄,不會觸發葉子節點的分裂
如果是業務之邏輯字段做主鍵,則很難保證插入有序,這樣會出發葉子節點分裂
從存儲空間角度考慮,主鍵長度越小,普通葉子節點就越小,普通索引佔用的空間也就越小

大部分情況下用自增主鍵是最好的
如果某個表是類似k-v形式的,只有一個索引,並且該索引是唯一索引,那麼就用這個索引做主鍵

 

覆蓋索引
假設有下面這段sql

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'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

那麼這個sql

select * from T where k between 3 and 5;

他的執行過程如下
1.在k索引樹上找到k=3的記錄,得到ID=300
2.再到ID索引樹查找ID=300對應的R3
3.在k索引樹取下一值k=5,得到ID=500
4.再到ID索引樹查到ID=500對應的R4
5.在k索引樹取下一個值k=6,不滿足條件,循環結束
這個查詢過程中度了k索引樹的3條記錄(步驟1,3,5),回表了兩次(步驟2和4)

如果寫成這樣

select ID from T where k between 3 and 5

這時候只需要查詢ID的值,而ID的值已經在k索引樹上了,因此可以直接提供查詢結果不需要回表,這個查詢需求較覆蓋索引,使用覆蓋索引是一個常見的性能優化手段

假設有個高頻的查詢需求,通過身份證查詢名字
那麼再建立一個(身份證,名字)的聯合索引,可以滿足這個需求,它會用到覆蓋索引,不需要再回表查詢整行數據,減少語句的執行時間
但這增加的冗餘性,也是有代價的,所以需要綜合業務情況去考慮

最左前綴
B+樹可以用最左前綴這個特定來定位記錄
假設聯合索引是(name,age)

當查詢所有名字是"張三"時候,可以快速定位到ID4這個記錄,然後遍歷所有結果
如果是模糊查詢 like '張%' 也能利用上這個索引,查詢到第一個符合條件的記錄是ID3,再向後遍歷

聯合索引定位的原則是,如果通過調整順序,可以少維護一個索引,那麼這個順序往往就需要優先考慮
如果既有聯合查詢,又有基於a,b各自的查詢,查詢條件裏有b是無法使用(a,b)的,只能再添加一個b索引,此時要考慮空間原則,name字段比age字段大,那麼建立(name,age),(age)就比建立(age,name),(name)划算

索引下推
對於不滿足聯合索引中最左前綴中的部分,假設有聯合索引(name,age)現在有一個需求檢索出表中 名字第一個字是張,並且年齡是10歲的所有男孩,那麼sql語句會這麼寫

mysql> select * from tuser where name like '張 %' and age=10 and ismale=1;

在mysql5.6之前只能從ID3開始一個個的回表,找到主鍵上的數據行再對比字段值

InnoDB內部就利用了(name,age)判斷了age是否等於10,對於不等於10的記錄,直接判斷並跳過,所以只需要對ID4,ID5這兩條記錄回表取數據判斷,只需要回表2次

 

全局鎖,表鎖,行鎖

全局鎖是對整個數據庫實例加鎖,mysql提供了一個加全局讀鎖的方式,命令爲
Flush tables with read lock
執行後下面語句會被阻塞

  1. 數據庫更新語句,增刪改
  2. 數據定義語句,建表,修改表結構等
  3. 更新類事務的提交語句

全局鎖的典型使用場景是,做全庫邏輯備份
缺點

  1. 如果在主庫上備份,在此期間業務基本上就停了
  2. 如果在從庫上備份,在此期間不能執行從主庫同步過來的binlog,導致主從延遲

官方自帶的mysqldup使用參數 -single-transaction時,導數據之前會啓動一個事務,確保拿到一致性視圖,由於MMVC支持,這個過程中數據可以正常更新
但一致性讀需要存儲引擎支持,非InnoDB的就不行了

和 set global redonly=true 的區別

  1. 在有些系統中readonly會用來做其他邏輯,如判斷一個庫是主還是備
  2. 如果執行全局鎖失敗了,mysql會自動釋放這個鎖,但readonly則不會

表鎖的語法是

lock tables 。。。 read/write

可以用unlock tables 主動釋放鎖,也可以在客戶端斷開連接時自動釋放
如果A線程中執行

lock tables t1 read,t2 write

則其他寫t1會阻塞,讀寫t2會被阻塞

另一類表鎖是MDL 元數據鎖,MDL不需要顯示使用,在訪問一個表的時候會被自動加上
MDL的作用是保證讀寫的正確性
MDL包括讀和寫鎖
1.讀鎖之間不互斥,可以由多個線程同時對一張表增刪改查
2.讀寫鎖之間,寫鎖之間是互斥的,用來保證變更結構操作的安全性

一個注意的事項,給小表修改字段

A和B都是讀鎖,都可以正常執行
C是修改表結構會被阻塞
C自己阻塞了之後,所有要在表t上申請MDL讀鎖的請求也被會C阻塞
也就是這個表目前完全不可以讀寫了,如果這個表讀寫頻繁,很快會導致數據庫線程爆滿
首先要解決長事務,alter table還要設置等待時間
MariaDB整合了阿里sql,支持不等待更新

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 


行鎖
假設有下面兩個事務

A會鎖住這兩條記錄,B執行的時候會被阻塞,等到A 提交後纔可以繼續執行

InnoDB事務中
行鎖是在需要的時候才加上的,但並不會立即釋放,而要等到事務結束後才釋放,這就是兩階段鎖協議
在事務中,要把可能造成鎖衝突,可能影響併發讀的鎖儘量往後放
假設有一個買電影票的業務
1.客戶A從賬戶餘額中扣除電影票價
2.給影院B賬戶餘額中增加票價
3.記錄一個交易日誌

如果用戶C在買影院B的票,那麼可能衝突的語句就是2了
所以改成3,1,2這樣的順序
可以最大限度的降低鎖造成的併發

死鎖

因爲調整了語句的執行順序,mysql中可能會出現死鎖

解決辦法

  1. 加入超時機制,通過參數 innodb_lock_wait_timeout 來設置
  2. 發起死鎖檢測,主動回滾死鎖鏈中的某一個事務,讓其他事務得以繼續執行,將 innodb_deadlock_detect 設置爲on,表示開啓

第一個參數默認是50秒太長了,但設置的太短可能又會影響正常的讀寫
第二種策略默認是開啓的
死鎖檢測是有額外負擔的
每個新來的被堵住的線程,都要判斷會不會由於自己的加入導致了死鎖,這是一個時間複雜度爲O(n)的操作,如果1000個併發同時更新一行,那麼就是100W這個量級了,會消耗大量的CPU,結果是CPU利用率很高但每秒卻執行不了幾個事務
解決辦法

  1. 確保業務一定不會出現死鎖,可以臨時把死鎖檢測關掉,但這本身也是有風險的
  2. 做併發限制,要在數據庫中間件層做限制,或者修改mysql代碼,在進入引擎之前排隊
  3. 邏輯上做修改,比如影院的賬戶總額等於10條記錄總和,每次更新時隨機選擇一條更新即可,這樣衝突概率變成 1/10,可以減少所鎖等待個數

但這個邏輯需要考慮到可能有退票的情況,一部分行的記錄可能會變成0等特殊情況
 

細說事務隔離機制

細說事務隔離機制
事務的兩種啓動方式
1.begin/start transaction ,一致性視圖在執行第一個快照語句時創建
2.在執行 start transaction with consistent snapshot 創建一致性視圖

mysql中有兩個“視圖”的概念
1.view,用來查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果
2.在InnoDB實現MMVC時用到的一致性讀視圖,用於RC(read committed)和RR(Repeatable
 Read) 兩個隔離級別

InnoDB中每個事務都有唯一的事務ID,在事務開始的時候向InnoDB申請,並且是嚴格遞增的
每行數據也有多個版本,每次事務更新時,都會生成一個新的數據版本,並把事務id賦給這個數據版本的事務id,記爲row tx_id,同時舊的事務版本還會保留

undo log就是那個虛線,通過虛線可以得到
如果當前是V4,通過計算U3,U2就可以得到V2的值

對於視圖的可見性問題,InnoDB定義對每個視圖定義了一個數組
低水位是已經提交的事務,高水位是未提交的事務集合

對於當前的事務,一個數據版本的row trx_id 有以下幾種可能
1.如果落在綠色部分,表示是已提交的事務或者當前事務自己生成的,是可見的
2.落在紅色部分,這個版本是由將來還沒提交的事務生成的不可見
3.落在黃色部分
  a.若在 row trx_id數組中,表示這個版本是還沒提交的事務生成的不可見
  b.不在數組中,表示這個版本是已提交的事務生成的可見

假設有三個事務A B C
開始之前系統裏只有一個活躍的事務ID是99
A B C 的版本號是100 101 102
三個事務開始之前 這一行的row trx_id 是90

C先更新,此時數據的版本事務id是102
B再更新,此時當前版本事務id是101
A去讀,A的可見範圍是[99,100],102和101對A都是不可見的,所以拿到的值還是(1,1)

可以這麼理解,一個數據版本,對於一個事務視圖來說,除了自己的更新總是可見以外,有三種情況
1.版本未提交,不可見
2.版本已提交,但在視圖創建後提交的,不可見
3.版本已提交,在視圖創建前提交的,可見

在用這個邏輯分析上圖,對於A來說
(1,3)還沒提交屬於情況1,不可見
(1,2)提交了,但是在視圖數組A創建後提交的,屬於情況2,不可見
(1,1)是視圖數組創建之前提交的,可見

對於更新邏輯

C先更新成2,B在C的基礎上再更新成3
這裏B不能在視圖上更新,否則之前C的更新就丟失了
因此事務B的更新 set k=k+1 是在(1,2)的基礎上進行的操作
這裏有一條規則
更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲 當前讀

如果A線程的查詢不是普通的select,而是加了鎖的,也會變成當前讀
下面分別是一個讀鎖S鎖,一個寫鎖X鎖

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

再假設事務C更新後不是馬上提交的,也就是如下圖所示

雖然C' 沒有馬上提交,但是(1,2)這個版本已經生產了,並且是當前最新的版本
但因爲C'還沒提交,也就是兩階段鎖協議,先更新再提交釋放鎖,(1,2)這個版本的寫鎖還沒釋放
事務B是當前度,必須加鎖,因此就被鎖住了,必須等C'提交這個鎖才能繼續當前讀

讀提交 和 可重複讀
可重複讀,是在事務開始時創建一致性視圖,之後事務裏的其他查詢都共用這個一致性視圖
讀提交,每個語句執行前都會重新算出一個新的視圖

對於前面的A B C三個事務更新情況,如果是讀提交隔離級別,情況如下

事務A的查詢語句視圖數組是在執行這個語句的時候創建的,時序上(1,2)和(1,3)的生成時間都在創建這個視圖的時刻之前
(1,3)屬於情況1,還沒提交,不可見
(1,2)屬於情況3,提交了,可見
所以A的查詢結果返回的是k=2
 

 

普通索引和唯一索引

查詢過程對比
1.普通索引當滿足某個條件k=5時,還要繼續往下查找
2.唯一索引,滿足條件後就停止檢索
mysql會把一頁的內容16K讀入內存再檢索的
普通索引除非出現在一頁的最後,或者一個表中有很多相同的普通索引
正常來說普通索引就出現在一頁中,很快就能讀完停止,因爲是內存讀取索引普通和唯一差別很小


更新操作
假設要更新的目標頁在內存中
1.唯一索引檢查k=4,找到3-5之間的位置沒有衝突插入這個值返回
2.普通索引找到3-5之間的位置,插入k=4這個記錄返回
所以這兩個操作性能差別很小

如果要更新的目標頁不再內存中
1.唯一索引需要讀取內存判斷是否有衝突,再插入這個值,這裏是隨機I/O
2.普通索引更新記錄到change buffer中,執行結束


change buffer
最初是叫insert buffer,後來支持delete和update,就改爲change buffer
更新某個記錄時,會先存到change buffer中,然後定期刷磁盤,寫入到系統表空間中
change的內容會跟redo log一起合併,寫入到磁盤中(順序寫IO)
change buffer默認是buffer pool的50%,可以通過參數調整
innodb_change_buffer_max_size

change buffer的使用場景
適合寫多讀少的場景,某一個頁更新的越多,收益就越大
寫多讀少的場景,比如賬單類,日誌類系統
如果有一種業務,更新之後又立馬要讀取,就會訪問這個數據頁,會觸發merge過程,這樣隨機訪問I/O的次數不會減少,反而增加了change buffer的維護代價,可以選擇關閉change buffer
如果業務上已經確定不會出現重複,或者沒要求,儘量選擇普通索引

因爲主鍵是唯一的,所以插入操作就不能用chang buffer了
主鍵和數據內容一個B+樹
普通索引和主鍵的值又是一個B+樹
雖然主鍵的B+不能用change buffer了,但是普通索引這個B+樹還是可以用change buffer的


change buffer和redo log
假設有下面的sql

mysql> insert into t(id,k) values(id1,k1),(id2,k2);

k1所在的數據頁在內存中(innob buffer pool)中,k2所在的數據頁不再內存中

整個操作過程如下
1.page 1 在內存中,直接更新內存
2.page 2沒有在內存中,就在內存的change buffer區域,記錄下“往page 2插入一行”這個信息
3.將上述兩個動作記錄到redo log中(上圖中的3和4)
虛線是後臺操作,不影響更新響應時間

假設在這之後又有一個sql
select * from t where k in (k1,k2)
如果讀語句發生在更新語句不久,內存中的數據都還在,那麼此時這兩個操作就和系統表空間ibdata1,redo log(ib_log_fileX)無關了

讀page1的時候,直接從內存返回
讀page2的時候,需要把page2從磁盤讀入內存中,然後應用chang buffer裏面的操作日誌,生成一個正確的版本並返回
可以看到直到讀page 2的時候,這個數據頁纔會被讀入內存

redo log主要節省的是隨機寫磁盤的I/O消耗,轉換爲順序寫
change buffer主要節省的是隨機讀磁盤的I/O消耗

 

索引選擇的問題

。。。

 

前綴索引

假設給字符串增加索引,有兩種方式

mysql> alter table SUser add index index1(email);
或
mysql> alter table SUser add index index2(email(6));

第一種方式建立後,整個B+樹結構如下

第二種方式建立的B+樹結構如下

第二種方式節省了存儲空間,但如果選擇的長度不合理,就會造成性能損失
比如想找到郵箱是
[email protected]的這個人
因爲email(6)存儲的是zhangs
所以輸入zhangsan,zhangsong都滿足條件
但很明顯zhangsong是不對的,會多回表一次
而不用前綴方式,直接存儲整合郵箱就不會有這種問題

使用前綴索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本
使用前綴索引就沒法用覆蓋索引了,因爲mysql沒法判斷是否出現截斷

其他索引方式
1.倒序存儲,然後存儲後面的6位,比如身份證這種的
2.增加一個索引字段,存儲hash值,查詢穩定性好,有額外的計算消耗,和第二種一樣不支持範圍查詢

 

刷髒頁的過程

當內存數據頁跟磁盤數據頁內容不一致的時候,就稱這個內存頁爲“髒頁”,內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲“乾淨頁”

引發數據庫flush的情況
1.InonoDB的redo log寫滿了,這時系統就會停止所有更新操作,把checkpoint往前推進,redo log留出空間可以繼續寫

checkpoint的位置從CP進退到CP',就需要將兩個點之間的日誌,對應的所有髒頁都flush到磁盤上,之後圖中從write pos到CP'之間就是可以再寫入redo log的區域

2.系統內存不足時,當需要新的內存頁,而內存不夠用時,就需要淘汰一些數據頁,空出內存給別的數據頁使用,如果淘汰的是髒頁,就要先將髒頁寫到磁盤

如果刷新髒頁一定會寫盤,就保證了每個數據頁有兩種狀態
a.在內存裏存在,內存裏肯定就是正確的結果,直接返回
b.內存裏沒有數據,讀取文件到內存

3.系統空閒的時候,主動刷新髒頁到磁盤
4.MySQL重啓的時候刷新髒頁

第三種,第四種情況對性能不太關注,主要是第一和第二點
第一種情況會導致整個系統不能更新,從監控上看更新數會跌到0
第二種 內存不夠用,要寫將髒頁寫到磁盤,InnoDB管理內存,緩衝池中的內存頁有三種狀態
a.還沒有使用的
b.使用了並且是乾淨頁
c.使用了並且是髒頁

當要讀入的數據頁沒有在內存時,必須到緩衝池中申請一個數據頁,用LRU淘汰一個,如果是乾淨頁就直接釋放出來複用,如果是髒頁就需要先刷新到磁盤才能複用

刷髒頁是常態,但出現以下兩種情況會明顯影響性能

  1. 一個查詢要淘汰的髒頁個數太多,會導致查詢的響應時間明顯變成
  2. 日誌寫滿,更新全部堵住,寫性能跌爲0

InnoDB 需要知道主機I/O的能力,這樣才能全力刷磁盤
innodb_io_capacity

這個參數就是磁盤的IOPS值,可以通過fio工具來測試
下面是fio的隨機讀寫命令

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 

InnoDB的刷磁盤速度會考慮兩個因素

  1. 髒頁的比列
  2. redo log寫盤的速度

InnoDB會計算這兩個值,然後取最大一個記做R,引擎按照 innodb_io_capacity定義的能力乘以 R% 來控制刷髒頁到大速度
整個流程如下圖

 

InnoDB 需要知道主機I/O的能力,這樣才能全力刷磁盤
innodb_io_capacity

這個參數就是磁盤的IOPS值,可以通過fio工具來測試
下面是fio的隨機讀寫命令
fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 

InnoDB的刷磁盤速度會考慮兩個因素
1.髒頁的比列
2.redo log寫盤的速度
InnoDB會計算這兩個值,然後取最大一個記做R,引擎按照 innodb_io_capacity定義的能力乘以 R% 來控制刷髒頁到大速度
整個流程如下圖


InnoDB在後臺刷髒頁,這個過程需要寫磁盤,會導致mysql性能監控的抖動
平常要多關注髒頁比列,不要繞過他經常接近75%
髒頁的比例是通過

innodb_buffer_poll_pages_dirty / innodb_buffer_pool_pages_total

得到的
還有一個參數,是刷新鄰居頁的,機械硬盤時,寫磁盤的IOPS很低,所以如果有髒頁了儘可能合併到一起刷新
innodb_flush_neighhbors 控制這個行爲,如果當前要刷新,並且檢測到鄰居頁也要刷新,並且鄰居的鄰居的鄰居。。。 最後會引起一連串的刷新
到mysql 8.0這個參數默認就關閉了

 

表數據的存儲方式

表的數據可以放到共享表空間裏,也可以是單獨的文件

innodb_file_per_table 這個參數決定
off表示放到共享表空間,on表示開啓

mysql刪除數據之後,不會清空數據,只是在數據加了一個刪除的標誌
對於下面的這個情況

刪除R4後會標記爲刪除,那麼在300-600之間再插入數據的話,可能會複用這個位置,但是磁盤大小不會縮減
如果刪除的是一個數據頁上的所有記錄,那麼整個數據頁都會被複用,但是數據不會被刪除
所以delete操作會導致文件出現空洞

此外插入也可能會導致數據出現空洞
假設下圖,pageA已經滿了,再插入數據導致分裂

再插入一個550的數據,pageA滿了就不得不分裂,這樣page A的末尾就留下了空間,而且可能不止一個記錄位置
索引的更新,也可以理解爲刪除一箇舊值,再插入一個新值,也可以造成空洞

表重建
使用下面語句重建表
alter table A engine=InnoDB
具體過程如下

這個過程是阻塞的,不能有更新操作
mysql 5.6之後引入了online DDL,也是新建一個臨時表
但是之後對A的更新操作都放到一個日誌文件中
之後再合併這個日誌文件

再插入一個550的數據,pageA滿了就不得不分裂,這樣page A的末尾就留下了空間,而且可能不止一個記錄位置
索引的更新,也可以理解爲刪除一箇舊值,再插入一個新值,也可以造成空洞

表重建
使用下面語句重建表
alter table A engine=InnoDB
具體過程如下

這個過程是阻塞的,不能有更新操作
mysql 5.6之後引入了online DDL,也是新建一個臨時表
但是之後對A的更新操作都放到一個日誌文件中
之後再合併這個日誌文件

alter table的完整寫法包括

alter table t engine=innodb,ALGORITHM=inplace;
alter table t engine=innodb,ALGORITHM=copy;

對於truncate,alter,analyze,optimize幾個的區別

  1. 從mysql的5.6開始,alter table t engine就是online DDL方式
  2. analyze table不重建表,只是對索引信息做重新統計沒有改數據
  3. optime table t等於create+analyze
  4. truncate等於drop+create

 

count(*)執行原理

MyISAM 會保存當前表的總記錄數,所以count(*)直接返回
InnoDB需要按行累加就很慢了

InnoDB沒有像MyISAM那樣保存表總行數,是因爲MMVC的原因,不同的視圖返回多少行是不確定的
假設有三個會話
A先啓動事務並執行一次查詢
B啓動事務,插入一個記錄,再查詢總行數
C先啓動一個單獨插入語句,再查詢總行數

對於MMVC來說,每個事務就沒法判斷到底是多少行了,只能一行一行取出來再判斷
mysql 有一個命令
show table status
這裏面有TABLE_ROWS,這個是採樣的結果,誤差可能達到40%-50%

所以總結下

  1. MyIASM的count(*)很快,但不支持事務
  2. show table status很快,但誤差大
  3. InnoDB的count(*)很準但速度慢


自定義的總行數保存方案
用緩存保存計數,比如Redis,但Redis重啓之後就不對了,需要重新做count(*)
另外即使Redis正常工作,記錄的值可能還是不準
參考下面這個時序圖,先插入數據,再更新

或者是先計數累加,再更新

因爲這是兩個系統,沒有分佈式事務,所以無法保證一致性
可以在mysql中再增加一個表C,專門做計數統計,然後插入和刪除的時候,做事務
這樣就能保證一致性了

count(id)會把每一行的id取出來返回給server,server判斷不爲空再累加
count(1)遍歷整個表,不取值,server對返回的每一行放入1,再累加
count(字段)也是遍歷整個表,判斷不爲null再累加
count(*)是列外,mysql對此專門做了優化

從性能上看
count(*)和count(1)差不多
count(id) 第二
count(字段)最差

 

 

參考

redo/undo log、binlog 的詳解及其區別

《MySQL實戰45講》1~15講

說說MySQL中的Redo log Undo log都在幹啥

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章