《MySQL實戰45講》筆記
第一節: 基礎架構:一條SQL查詢語句是如何執行的?
- MySQL的基本架構示意圖
大體來說,MySQL可以分為Server層和存儲引擎層兩部分。
Server層包括連接器、查詢緩存、分析器、優化器、執行器等,涵蓋MySQL的大多數核心服務 功能,以及所有的內置函數(如日期、時間、數學和加密函數等),所有跨存儲引擎的功能都在 這一層實現,比如存儲過程、觸發器、視圖等。
而存儲引擎層負責數據的存儲和提取。其架構模式是插件式的,支持InnoDB、MyISAM、 Memory等多個存儲引擎。
現在最常用的存儲引擎是InnoDB,它從MySQL 5.5.5版本開始成為了 默認存儲引擎。
-
連接器
連接命令寫法如下,輸入完命令后在交互對話里面輸入密碼。也可以直接在-p后面寫在命令行中
mysql -h$ip -P$port -u$user -p
使用
show processlist
命令查詢當前連接狀態客戶端如果太長時間沒動靜,連接器就會自動將它斷開。這個時間是由參數wait_timeout控制 的,默認值是8小時。
建立連接的過程通常是比較復雜的,盡量減少建立連接的動作,使用長連接。MySQL在執行過程中使用的內存是管理在連接對象里的,只有在連接斷開的時候才會釋放。所以長連接容易導致內存問題。
如何解決長連接導致的內存問題:
- 定期斷開長連接
- 對於MySQL 5.7或更新的版本,在執行較大的操作后,通過執行mysql_reset_connection來重新初始化連接資源。這個過程不需要重連和重新做權限驗證, 但是會將連接恢復到剛剛創建完時的狀態。
-
查詢緩存
MySQL拿到一個查詢請求后,先會在緩存中查找,找不到就會繼續后面的執行階段。執行完成后,結果會存入到查詢緩存中。
由於對表的更新操作會導致緩存清空,對於更新壓力大的數據庫,查詢緩存的命令率比較低。對於靜態表,不更新或者查詢很多更新很少的表,才適合使用緩存查詢。MySQL也提供了這種“按需使用”的方式。你可以將參數query_cache_type設置成DEMAND,這樣對於默認的SQL語句都不使用查詢緩存。而對於你確定要使用查詢緩存的語句,可以用SQL_CACHE顯式指定
select SQL_CACHE * from T where ID=10;
MySQL 8.0版本已經將查詢緩存的整塊功能刪掉了,之后的版本都沒有查詢緩存了
-
分析器
先做"詞法分析",再做"語法分析",判斷輸入的SQL是否滿足MySQL語法
-
優化器
在表里面有多個索引的時候,決定使用哪個索引;或者在一個語句有多表關聯(join) 的時候,決定各個表的連接順序。
-
執行器
先判斷一下你對當前操作的表有沒有執行查詢的權限。有權限,就打開表繼續執行。打開表的時候,執行器就會根據表的引擎定義,去使用這個引 擎提供的接口。
select * from T where ID=10;
比如我們這個例子中的表T中,ID字段沒有索引,那么執行器的執行流程是這樣的:
- 調用InnoDB引擎接口取這個表的第一行,判斷ID值是不是10,如果不是則跳過,如果是則 將這行存在結果集中;
- 調用引擎接口取“下一行”,重復相同的判斷邏輯,直到取到這個表的最后一行。
- 執行器將上述遍歷過程中所有滿足條件的行組成的記錄集作為結果集返回給客戶端。
第二節:日志系統:一條SQL更新語句是如何執行的?
-
重要的日志模塊:redo log
redo log是 InnoDB引擎特有的日志。
MySQL里經常說到的WAL技術,WAL的全稱是WriteAhead Logging,它的關鍵點就是先寫日志,再寫磁盤。
當有一條記錄需要更新的時候,InnoDB引擎就會先把記錄寫到redo log里面,並更新內存,這個時候更新就算完成了。同時,InnoDB引擎會在適當的時候,將這個操作記錄更新到磁盤里面,而這個更新往往是在系統比較空閑的時候做。
InnoDB的redo log是固定大小的,比如可以配置為一組4個文件,每個文件的大小是 1GB,那么這塊“粉板”總共就可以記錄4GB的操作。從頭開始寫,寫到末尾就又回到開頭循環 寫,如下面這個圖所示。
write pos是當前記錄的位置,一邊寫一邊后移,寫到第3號文件末尾后就回到0號文件開頭。
checkpoint是當前要擦除的位置,也是往后推移並且循環的,擦除記錄前要把記錄更新到數據文件。
write pos和checkpoint之間的是“粉板”上還空着的部分,可以用來記錄新的操作。如果write pos追上checkpoint,這時候不能再執行新的更新,得停下來先擦掉一些記錄,把checkpoint推進一下。
有了redo log,InnoDB就可以保證即使數據庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為crash-safe。
-
重要的日志模塊:binlog
Server層的日志,稱為binlog(歸檔日志)
兩種日志有以下三點不同:
- redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用。
- redo log是物理日志,記錄的是“在某個數據頁上做了什么修改”;binlog是邏輯日志,記錄的是這個語句的原始邏輯,比如“給ID=2這一行的c字段加1 ”。
- redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的。“追加寫”是指binlog文件寫到一定大小后會切換到下一個,並不會覆蓋以前的日志。
-
執行更新的過程
創建表: create table T(ID int primary key, c int);
更新: update T set c=c+1 where ID=2;
執行器和InnoDB引擎在執行這個簡單的update語 句時的內部流程。
- 執行器先找引擎取ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果ID=2這一 行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然 后再返回。
- 執行器拿到引擎給的行數據,把這個值加上1,比如原來是N,現在就是N+1,得到新的一行 數據,再調用引擎接口寫入這行新數據。
- 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log里面,此時redo log處 於prepare狀態。然后告知執行器執行完成了,隨時可以提交事務。
- 執行器生成這個操作的binlog,並把binlog寫入磁盤。
- 執行器調用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交(commit)狀態更新完成。
update語句的執行流程圖,圖中淺色框表示是在InnoDB內部執行的,深色框表示是在執行器中執行的
redo log的寫入拆成了兩個步驟:prepare和 commit,這就是"兩階段提交"。
-
兩階段提交
-
為什么日志需要“兩階段提交”
由於redo log和binlog是兩個獨立的邏輯,如果不用兩階段提交,要么就是先寫完redo log再寫binlog,或者采用反過來的順序。我們看看這兩種方式會有什么問題。
仍然用前面的update語句來做例子。假設當前ID=2的行,字段c的值是0,再假設執行update語句過程中在寫完第一個日志后,第二個日志還沒有寫完期間發生了crash,會出現什么情況呢?- 先寫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,與原庫的值不同。
可以看到,如果不使用“兩階段提交”,那么數據庫的狀態就有可能和用它的日志恢復出來的庫的狀態不一致。
簡單說,redo log和binlog都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保 持邏輯上的一致。
- 先寫redo log后寫binlog。假設在redo log寫完,binlog還沒有寫完的時候,MySQL進程異常重啟。由於我們前面說過的,redo log寫完之后,系統即使崩潰,仍然能夠把數據恢復回來,所以恢復后這一行c的值是1。
-
-
總結
物理日志redo log和邏輯日志binlog
redo log用於保證crash-safe能力。innodb_flush_log_at_trx_commit這個參數設置成1的時候, 表示每次事務的redo log都直接持久化到磁盤。這個參數建議你設置成1,這樣可以保證 MySQL異常重啟之后數據不丟失。
sync_binlog這個參數設置成1的時候,表示每次事務的binlog都持久化到磁盤。這個參數也建 議你設置成1,這樣可以保證MySQL異常重啟之后binlog不丟失。
第三節:03.事務隔離:為什么你改了我還看不見?
-
隔離性與隔離級別
事務的4個特性:ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性)
當數據庫上有多個事務同時執行的時候,就可能出現臟讀(dirty read)、不可重復讀(non- repeatable read)、幻讀(phantomread)的問題,為了解決這些問題,就有了“隔離級別”的概 念。
隔離得越嚴實,效率就會越低。因此很多時候,我們都要在二者之間尋找一個平衡點。SQL標准的事務隔離級別包括:讀未提交(read uncommitted)、 讀提交(read committed)、可重復讀(repeatable read)和串行化(serializable )。
讀未提交是指,一個事務還沒提交時,它做的變更就能被別的事務看到。
讀提交是指,一個事務提交之后,它做的變更才會被其他事務看到。
可重復讀是指,一個事務執行過程中看到的數據,總是跟這個事務在啟動時看到的數據是一 致的。當然在可重復讀隔離級別下,未提交變更對其他事務也是不可見的。
串行化,顧名思義是對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖沖突 的時候,后訪問的事務必須等前一個事務執行完成,才能繼續執行。
-
舉例說明這幾種隔離級別
假設數據表T中 只有一列,其中一行的值為1,下面是按照時間順序執行兩個事務的行為。
create table T(c int) engine=InnoDB; insert into T(c) values(1)·
若隔離級別是“讀未提交”, 則V1的值就是2。這時候事務B雖然還沒有提交,但是結果已經被 A看到了。因此,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';
-
-
事務隔離的實現
在MySQL中,實際上每條記錄在更新的時候都會同時記錄一條回滾操作。記錄上的最新值,通 過回滾操作,都可以得到前一個狀態的值。
假設一個值從1被按順序改成了2、3、4,在回滾日志里面就會有類似下面的記錄。
當前值是4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的read-view。如圖中看 到的,在視圖A、B、C里面,這一個記錄的值分別是1、2、4,同一條記錄在系統中可以存在多 個版本,就是數據庫的多版本並發控制(MVCC)。對於read-viewA,要得到1,就必須將當前 值依次執行圖中所有的回滾操作得到。
同時你會發現,即使現在有另外一個事務正在將4改成5,這個事務跟read-viewA、B、C對應的 事務是不會沖突的。
-
回滾日志什么時候刪除?
系統會判斷,當沒有事務再需要用到這些回滾日志時,回滾日志會被刪除。什么時候才不需要了呢?就是當系統里沒有比這個回滾日志更早的read-view的時候。
-
為什么建議盡量不要使用長事務?
長事務意味着系統里面會存在很老的事務視圖。由於這些事務隨時可能訪問數據庫里面的任何數 據,所以這個事務提交之前,數據庫里面它可能用到的回滾記錄都必須保留,這就會導致大量占 用存儲空間。
對回滾段的影響,長事務還占用鎖資源,也可能拖垮整個庫,
-
事務的啟動方式
MySQL的事務啟動方式有以下幾種:
- 顯式啟動事務語句, begin 或 start transaction。配套的提交語句是commit,回滾語句是 rollback。
- set autocommit=0,這個命令會將這個線程的自動提交關掉。意味着如果你只執行一個 select語句,這個事務就啟動了,而且並不會自動提交。這個事務持續存在直到你主動執行 commit 或 rollback 語句,或者斷開連接。
有些客戶端連接框架會默認連接成功后先執行一個set autocommit=0的命令。這就導致接下來的 查詢都在事務中,如果是長連接,就導致了意外的長事務。
建議使用set autocommit=1, 通過顯式語句的方式來啟動事務。
在autocommit為1的情況下,用begin顯式啟動的事務,如果執行commit則提交事務。如果執行 commit work and chain,則是提交事務並自動啟動下一個事務,這樣也省去了再次執行begin語 句的開銷。
在information_schema庫的innodb_trx這個表中查詢長事務
#查找持續時間超過60s的事務 select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
-
如何避免長事務
-
從應用開發端來看:
-
確認是否使用了set autocommit=0,觀察general_log
general log 將所有到達MySQL Server的SQL語句記錄下來。
一般不會開啟開功能,因為log的量會非常龐大。但個別情況下可能會臨時的開一會兒general log以供排障使用。
相關參數一共有3:general_log、log_output、general_log_fileshow variables like 'general_log'; -- 查看日志是否開啟
set global general_log=on; -- 開啟日志功能
show variables like 'general_log_file'; -- 看看日志文件保存位置
set global general_log_file='tmp/general.lg'; -- 設置日志文件保存位置
show variables like 'log_output'; -- 看看日志輸出類型 table或file
set global log_output='table'; -- 設置輸出類型為 table
set global log_output='file'; -- 設置輸出類型為file
-
確認是否有不必要的只讀事務,只讀事務可以去掉
-
通過SETMAX_EXECUTION_TIME命令, 來控制每個語句執行的最長時間,避免單個語句意外執行太長時間。
-
-
從數據庫端來看:
- 監控 information_schema.Innodb_trx表,設置長事務閾值,超過就報警/或者kill;
- Percona的pt-kill這個工具
- 測試階段要求輸出所有的general_log,分析日志行為提前發現問題;
- MySQL 5.6或者更新版本,把innodb_undo_tablespaces(用於設定創建的undo表空間的個數,在mysql_install_db時初始化后,就再也不能被改動了,修改該值會導致MySQL無法啟動)設置成2(或更大的 值)。如果真的出現大事務導致回滾段過大,這樣設置后清理起來更方便。
-
第四節: 深入淺出索引(上)
-
索引常見的三種數據結構:哈希表、有序數組和搜索樹。
-
三種模型的區別:
- 哈希表這種結構適用於只有等值查詢的場景。比如Memcached及其他一些NoSQL引 擎。
- 有序數組,更新數據的時候往中間插入一個記錄就必須得挪動后面所有的記錄,成本太高。等值查詢和范圍查詢場景中的性能就都非常優秀。只適用於靜態存儲引擎。
- 搜索樹比較均衡。
-
InnoDB 的索引模型
在MySQL中,索引是在存儲引擎層實現的,所以並沒有統一的索引標准,
InnoDB使用了B+樹索引模型,每一個索引在InnoDB里面對應一棵B+樹。
舉例說明:
#主鍵列為ID的表,表中有字段k,並且在k上有索引。 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里,主鍵索引也被稱為聚簇索引(clustered index)。
非主鍵索引的葉子節點內容是主鍵的值。在InnoDB里,非主鍵索引也被稱為二級索引 (secondary index)。
從上面的示例可以看出:
- 如果語句是select *fromTwhere ID=500,即主鍵查詢方式,則只需要搜索ID這棵B+樹;
- 如果語句是select *fromTwhere k=5,即普通索引查詢方式,則需要先搜索k索引樹,得到ID 的值為500,再到ID索引樹搜索一次。這個過程稱為回表。
基於非主鍵索引的查詢需要多掃描一棵索引樹
-
索引維護
B+樹為了維護索引有序性,在插入新值的時候需要做必要的維護。以上面這個圖為例,如果插 入新的行ID值為700,則只需要在R5的記錄后面插入一個新記錄。如果新插入的ID值為400,就 相對麻煩了,需要邏輯上挪動后面的數據,空出位置。
而更糟的情況是,如果R5所在的數據頁已經滿了,根據B+樹的算法,這時候需要申請一個新的 數據頁,然后挪動部分數據過去。這個過程稱為頁分裂。在這種情況下,性能自然會受影響。
除了性能外,頁分裂操作還影響數據頁的利用率。原本放在一個頁的數據,現在分到兩個頁中, 整體空間利用率降低大約50%。
當相鄰兩個頁由於刪除了數據,利用率很低之后,會將數據頁做合並。合 並的過程,可以認為是分裂過程的逆過程。
自增主鍵是指自增列上定義的主鍵,在建表語句中一般是這么定義的: NOTNULL PRIMARY KEY AUTO_INCREMENT。
插入新記錄的時候可以不指定ID的值,系統會獲取當前ID最大值加1作為下一條記錄的ID值。 自增主鍵的插入數據模式,正符合了我們前面提到的遞增插入的場景。每次插入一條 新記錄,都是追加操作,都不涉及到挪動其他記錄,也不會觸發葉子節點的分裂。
主鍵長度越小,普通索引的葉子節點就越小,普通索引占用的空間也就越小。 所以,從性能和存儲空間方面考量,自增主鍵往往是更合理的選擇
索引可能因為刪除,或者頁分 裂等原因,導致數據頁有空洞,重建索引的過程會創建一個新的索引,把數據按順序插入,這樣 頁面的利用率最高,也就是索引更緊湊、更省空間。
第五節: 深入淺出索引(下)
-
案例
執行 select *fromTwhere k between 3 and 5,需要執行幾次樹的搜 索操作,會掃描多少行?
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查詢語句的執行流程:
- 在k索引樹上找到k=3的記錄,取得 ID = 300;
- 再到ID索引樹查到ID=300對應的R3;
- 在k索引樹取下一個值k=5,取得ID=500;
- 再回到ID索引樹查到ID=500對應的R4;
- 在k索引樹取下一個值k=6,不滿足條件,循環結束
回到主鍵索引樹搜索的過程,我們稱為回表。這個查詢過程讀了k 索引樹的3條記錄(步驟1、3和5),回表了兩次(步驟2和4)
在這個例子中,由於查詢結果所需要的數據只在主鍵索引上有,所以不得不回表。那么,有沒有 可能經過索引優化,避免回表過程呢?
-
覆蓋索引
如果執行的語句是select ID fromTwhere k between 3 and 5,這時只需要查ID的值,而ID的值 已經在k索引樹上了,因此可以直接提供查詢結果,不需要回表。也就是說,在這個查詢里面, 索引k已經“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。
由於覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用 的性能優化手段。
需要注意的是,在引擎內部使用覆蓋索引在索引k上其實讀了三個記錄,R3~R5(對應的索引k 上的記錄項),但是對於MySQL的Server層來說,它就是找引擎拿到了兩條記錄,因此MySQL 認為掃描行數是2。
-
最左前綴原則
B+樹這種索引結構,可以利用索引的“最左前綴”,來定位記錄。
CREATE TABLE `tuser` ( `id` int(11) NOT NULL, `id_card` varchar(32) DEFAULT NULL, `name` varchar(32) DEFAULT NULL, `age` int(11) DEFAULT NULL, `ismale` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `id_card` (`id_card`), KEY `name_age` (`name`,`age`) ) ENGINE=InnoDB
索引項是按照索引定義里面出現的字段順序排序的。
不只是索引的全部定義,只要滿足最左前綴,就可以利用索引來加速檢索。這個最左 前綴可以是聯合索引的最左N個字段,也可以是字符串索引的最左M個字符
在建立聯合索引的時候,如何安排索 引內的字段順序?
第一原則是,如果通過調整順序,可 以少維護一個索引,那么這個順序往往就是需要優先考慮采用的。
,name字段是比age字段大的 ,創建一個(name,age)的聯合索引和一個(age)的單字段索引。
-
索引下推
#檢索出表中“名字第一個字是張,而且年齡是10歲的所有男孩” select * from tuser where name like '張%' and age=10 and ismale=1
執行步驟:
-
這個語句在搜索索引樹的時候,只能用 “張”,找到第一個滿足 條件的記錄ID3。
-
當然是判斷其他條件是否滿足。
- 在MySQL 5.6之前,只能從ID3開始一個個回表。到主鍵索引上找出數據行,再對比字段值。
- MySQL 5.6 引入的索引下推優化(index condition pushdown), 可以在索引遍歷過程中,對索 引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數。
執行流程圖(每一個虛線箭頭表示回表一次)
在(name,age)索引里面我特意去掉了age的值,這個過程InnoDB並不會去看age的值, 只是按順序把“name第一個字是’張’”的記錄一條條取出來回表。因此,需要回表4次。
InnoDB在(name,age)索引內部就判斷了age是否等於10,對於不等於10的 記錄,直接判斷並跳過。在我們的這個例子中,只需要對ID4、ID5這兩條記錄回表取數據判 斷,就只需要回表2次。
-
第六節: 全局鎖和表鎖 :給表加個字段怎么有這么多阻礙
根據加鎖的范圍,MySQL里面的鎖大致可以分成全局鎖、表級鎖和行鎖三類。
-
全局鎖
全局鎖就是對整個數據庫實例加鎖。MySQL提供了一個加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL)。當你需要讓整個庫處於只讀狀態的時候,可以使用這個命 令,之后其他線程的以下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括 建表、修改表結構等)和更新類事務的提交語句。
全局鎖的典型使用場景是,做全庫邏輯備份
邏輯備份工具是mysqldump。當mysqldump使用參數–single-transaction的時候,導數據之前就會啟動一個事務,來確保拿到一致性視圖。而由於MVCC的支持,這個過程中數據是可以正常更新的。
single-transaction方法只適用於所有的表使用事務引擎的庫。如果有的表使用了不 支持事務的引擎,那么備份就只能通過FTWRL方法。這往往是DBA要求業務開發人員使用 InnoDB替代MyISAM的原因之一。
全庫只讀,為什么不使用set global readonly=true的方式呢?
- 一是,在有些系統中,readonly的值會被用來做其他邏輯,比如用來判斷一個庫是主庫還是備 庫。因此,修改global變量的方式影響面更大,我不建議你使用。
- 二是,在異常處理機制上有差異。如果執行FTWRL命令之后由於客戶端發生異常斷開,那么 MySQL會自動釋放這個全局鎖,整個庫回到可以正常更新的狀態。而將整個庫設置為 readonly之后,如果客戶端發生異常,則數據庫就會一直保持readonly狀態,這樣會導致整個 庫長時間處於不可寫狀態,風險較高。
-
表級鎖
表鎖一般是在數據庫引擎不支持行鎖的時候才會被用到的
MySQL里面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)。
-
表鎖的語法是 lock tables …read/write。與FTWRL類似,可以用unlock tables主動釋放鎖, 也可以在客戶端斷開的時候自動釋放。需要注意,lock tables語法除了會限制別的線程的讀寫 外,也限定了本線程接下來的操作對象。
舉個例子, 如果在某個線程A中執行lock tables t1 read, t2 write; 這個語句,則其他線程寫t1、讀 寫t2的語句都會被阻塞。同時,線程A在執行unlock tables之前,也只能執行讀t1、讀寫t2的操 作。連寫t1都不允許,自然也不能訪問其他表。
-
元數據鎖MDL不需要顯式使用,在訪問一個表的時候會被 自動加上。MDL的作用是,保證讀寫的正確性。
在MySQL 5.5版本中引入了MDL,當對一個表做增刪改查操作的時候,加MDL讀鎖;當 要對表做結構變更操作的時候,加MDL寫鎖。
- 讀鎖之間不互斥,因此你可以有多個線程同時對一張表增刪改查。
- 讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。因此,如果有兩個線 程要同時給一個表加字段,其中一個要等另一個執行完才能開始執行。
-
-
示例
session A先啟動,這時候會對表t加一個MDL讀鎖。由於session B需要的也是 MDL讀鎖,因此可以正常執行。
之后session C會被blocked,是因為session A的MDL讀鎖還沒有釋放,而session C需要MDL寫 鎖,因此只能被阻塞。
如果只有session C自己被阻塞還沒什么關系,但是之后所有要在表t上新申請MDL讀鎖的請求也 會被session C阻塞。前面我們說了,所有對表的增刪改查操作都需要先申請MDL讀鎖,就都被 鎖住,等於這個表現在完全不可讀寫了。
如果某個表上的查詢語句頻繁,而且客戶端有重試機制,也就是說超時后會再起一個新session 再請求的話,這個庫的線程很快就會爆滿。
事務中的MDL鎖,在語句執行開始時申請,但是語句結束后並不會馬上釋 放,而會等到整個事務提交后再釋放。
-
課后問題:
備份一般都會在備庫上執行,你在用–single-transaction方法做邏 輯備份的過程中,如果主庫上的一個小表做了一個DDL,比如給一個表上加了一列。這時候,從 備庫上會看到什么現象呢?
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT; /* other tables */ Q3:SAVEPOINT sp; /* 時刻 1 */ Q4:show create table `t1`; /* 時刻 2 */ Q5:SELECT * FROM `t1`; /* 時刻 3 */ Q6:ROLLBACK TO SAVEPOINT sp; /* 時刻 4 */ /* other tables */
在備份開始的時候,為了確保RR(可重復讀)隔離級別,再設置一次RR隔離級別(Q1);
啟動事務,這里用 WITH CONSISTENT SNAPSHOT確保這個語句執行完就可以得到一個一致性 視圖(Q2);
設置一個保存點,這個很重要(Q3);
showcreate 是為了拿到表結構(Q4),然后正式導數據 (Q5),回滾到SAVEPOINT sp,在這 里的作用是釋放 t1的MDL鎖 (Q6。當然這部分屬於“超綱”,上文正文里面都沒提到。
DDL從主庫傳過來的時間按照效果不同,我打了四個時刻。題目設定為小表,我們假定到達后, 如果開始執行,則很快能夠執行完成。
參考答案如下:
- 如果在Q4語句執行之前到達,現象:沒有影響,備份拿到的是DDL后的表結構。
- 如果在“時刻 2”到達,則表結構被改過,Q5執行的時候,報 Table definition has changed, please retry transaction,現象:mysqldump終止;
- 如果在“時刻2”和“時刻3”之間到達,mysqldump占着t1的MDL讀鎖,binlog被阻塞,現象: 主從延遲,直到Q6執行完成。
- 從“時刻4”開始,mysqldump釋放了MDL讀鎖,現象:沒有影響,備份拿到的是DDL前的表 結構。
第七節: 行鎖功過:怎么減少行鎖對性能的影響?
MySQL的行鎖是在引擎層由各個引擎自己實現的。但並不是所有的引擎都支持行鎖
-
從兩階段鎖說起
在下面的操作序列中,事務B的update語句執行時會是什么現象呢?假設字 段id是表t的主鍵
這個問題的結論取決於事務A在執行完兩條update語句后,持有哪些鎖,以及在什么時候釋放。
實際上事務B的update語句會被阻塞,直到事務A執行commit之后,事務B才 能繼續執行。事務A持有的兩個記錄的行鎖,都是在commit的時候才釋放的。
在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋 放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。
如果你的事務中需要鎖多個行,要把 最可能造成鎖沖突、最可能影響並發度的鎖盡量往后放。
-
死鎖和死鎖檢測
這時候,事務A在等待事務B釋放id=2的行鎖,而事務B在等待事務A釋放id=1的行鎖。 事務A和 事務B在互相等待對方的資源釋放,就是進入了死鎖狀態。當出現死鎖以后,有兩種策略:
- 一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數 innodb_lock_wait_timeout來設置。
- 另一種策略是,發起死鎖檢測,發現死鎖后,主動回滾死鎖鏈條中的某一個事務,讓其他事 務得以繼續執行。將參數innodb_deadlock_detect設置為on,表示開啟這個邏輯。
在InnoDB中,innodb_lock_wait_timeout的默認值是50s