一次MySQL面試的故事(虛構(gòu))

本文純屬娛樂

面試官:談?wù)剬κ聞?wù)的理解吧。

我:數(shù)據(jù)庫的事務(wù)是指一組SQL語句組成的數(shù)據(jù)庫邏輯處理單元,在這組SQL的操作中,要么全部執(zhí)行成功,要么全部執(zhí)行失敗。

我:這里的一組SQL操作,舉個簡單又經(jīng)典的例子就是轉(zhuǎn)賬了,事務(wù)A中要進行轉(zhuǎn)賬,那么轉(zhuǎn)出的賬號要扣錢,轉(zhuǎn)入的賬號要加錢,這兩個操作都必須同時執(zhí)行成功,為了確保數(shù)據(jù)的一致性。

面試官: 剛才你提到了數(shù)據(jù)一致性,你知道事務(wù)的特性嗎?說說你的理解。

我: 在Mysql中事務(wù)的四大特性主要包含:原子性(Atomicity)、一致性(Consistent)、隔離性(Isalotion)、持久性(Durable),簡稱為ACID。

我: 原子性是指事務(wù)的原子性操作,對數(shù)據(jù)的修改要么全部執(zhí)行成功,要么全部失敗,實現(xiàn)事務(wù)的原子性,是基于日志的Redo/Undo機制。

我: 一致性是指執(zhí)行事務(wù)前后的狀態(tài)要一致,可以理解為數(shù)據(jù)一致性。隔離性側(cè)重指事務(wù)之間相互隔離,不受影響,這個與事務(wù)設(shè)置的隔離級別有密切的關(guān)系。

我: 持久性則是指在一個事務(wù)提交后,這個事務(wù)的狀態(tài)會被持久化到數(shù)據(jù)庫中,也就是事務(wù)提交,對數(shù)據(jù)的新增、更新將會持久化到數(shù)據(jù)庫中。

我: 在我的理解中,原子性、隔離性、持久性都是為了保障一致性而存在的,一致性也是最終的目的。

面試官: 剛才你說原子性是基于日志的Redo/Undo機制,你能說一說Redo/Undo機制嗎?

我: Redo/Undo機制比較簡單,它們將所有對數(shù)據(jù)的更新操作都寫到日志中。

我: Redo log用來記錄某數(shù)據(jù)塊被修改后的值,可以用來恢復(fù)未寫入 data file 的已成功事務(wù)更新的數(shù)據(jù);Undo log是用來記錄數(shù)據(jù)更新前的值,保證數(shù)據(jù)更新失敗能夠回滾。

我: 假如數(shù)據(jù)庫在執(zhí)行的過程中,不小心崩了,可以通過該日志的方式,回滾之前已經(jīng)執(zhí)行成功的操作,實現(xiàn)事務(wù)的一致性。

面試官: 可以舉一個場景,說一下具體的實現(xiàn)流程嗎?

我: 可以的,假如某個時刻數(shù)據(jù)庫崩潰,在崩潰之前有事務(wù)A和事務(wù)B在執(zhí)行,事務(wù)A已經(jīng)提交,而事務(wù)B還未提交。當(dāng)數(shù)據(jù)庫重啟進行 crash-recovery 時,就會通過Redo log將已經(jīng)提交事務(wù)的更改寫到數(shù)據(jù)文件,而還沒有提交的就通過Undo log進行roll back。

面試官: 之前你還提到事務(wù)的隔離級別,你能說一說嗎?

我:可以的,在Mysql中事務(wù)的隔離級別分為四大等級,讀未提交(READ UNCOMMITTED)、讀提交 (READ COMMITTED)、可重復(fù)讀 (REPEATABLE READ)、串行化 (SERIALIZABLE)。

我: 讀未提交會讀到另一個事務(wù)的未提交的數(shù)據(jù),產(chǎn)生臟讀問題,讀提交則解決了臟讀的,出現(xiàn)了不可重復(fù)讀,即在一個事務(wù)任意時刻讀到的數(shù)據(jù)可能不一樣,可能會受到其它事務(wù)對數(shù)據(jù)修改提交后的影響,一般是對于update的操作。

我: 可重復(fù)讀解決了之前不可重復(fù)讀和臟讀的問題,但是由帶來了幻讀的問題,幻讀一般是針對insert操作。

我: 例如:第一個事務(wù)查詢一個User表id=100發(fā)現(xiàn)不存在該數(shù)據(jù)行,這時第二個事務(wù)又進來了,新增了一條id=100的數(shù)據(jù)行并且提交了事務(wù)。

我: 這時第一個事務(wù)新增一條id=100的數(shù)據(jù)行會報主鍵沖突,第一個事務(wù)再select一下,發(fā)現(xiàn)id=100數(shù)據(jù)行已經(jīng)存在,這就是幻讀。

面試官: 你能演示一下嗎?

我: 首先創(chuàng)建一個User表,最為一個測試表,測試表里面有三個字段,并插入兩條測試數(shù)據(jù)。

CREATE TABLE User (
  id INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(20),
  age INT   DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES (1, 'zhangsan', 23);
INSERT INTO `user` VALUES (2, 'lisi', 20);

我: 在Mysql中可以先查詢一下他的默認隔離級別,可以看出Mysql的默認隔離級別是REPEATABLE-READ。

mysql>select @@tx_isolation;          
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)

我: 先來演示一下讀未提交,先把默認的隔離級別修改為READ UNCOMMITTED。

mysql>set tx_isolation = 'read-uncommitted';
Query OK, 0 rows affected (0.01 sec)

mysql>select @@tx_isolation;          
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)

我: 當(dāng)設(shè)置完隔離級別后對于之前打開的會話,是無效的,要重新打開一個窗口設(shè)置隔離級別才生效。

我: 然后是開啟事務(wù),Mysql中開啟事務(wù)有兩種方式begin/start transaction,最后提交事務(wù)執(zhí)行commit,或者回滾事務(wù)rollback。

我: 在執(zhí)行begin/start transaction命令,它們并不是一個事務(wù)的起點,在執(zhí)行完它們后的第一個sql語句,才表示事務(wù)真正的啟動 。

我: 這里直接打開兩個新的窗口,同時開啟事務(wù),在第一個窗口先update一個id=1的數(shù)據(jù)行name改為'mysia',執(zhí)行成功。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> use test
Database changed

mysql> update user set name = 'mysia' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

我: 然后再第二個窗口執(zhí)行兩次的查詢,分別是窗口一update之前的查詢和update之后的查詢。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> use test
Database changed

mysql>select * from User;
+----+----------+------+
| id | name     | age  |
+----+----------+------+
|  1 | zhangsan |   23 |
|  2 | lisi     |   20 |
+----+----------+------+
2 rows in set (0.00 sec)

mysql>select * from User;
+----+-------+------+
| id | name  | age  |
+----+-------+------+
|  1 | mysia |   23 |
|  2 | lisi  |   20 |
+----+-------+------+
2 rows in set (0.00 sec)

我: 第一個session產(chǎn)生的未提交的事務(wù)的狀態(tài)就會直接影響到第二sesison,也就是臟讀

我: 對于讀提交也是一樣的,開啟事務(wù)后,第一個事務(wù)先執(zhí)行查詢數(shù)據(jù),然后第二個session執(zhí)行update操作,但是還沒有commit,這是第一個session再次select,數(shù)據(jù)并沒有改變,再第二個session執(zhí)行commit之后,第一個session再次select就是改變后的數(shù)據(jù)了。

我: 這樣第一個事務(wù)的查詢結(jié)果就會收到第二事務(wù)的影響,這個也就是產(chǎn)生不可重復(fù)讀的問題。

面試官: 你能畫一下他執(zhí)行的過程圖嗎?你講的我有點亂,我還沒有徹底明白。

隔離級別.png

我: 這個是讀提交的時間軸圖,讀未提交的時間軸圖,原理也一樣的,第二個select的時候數(shù)據(jù)就已經(jīng)改變了。

面試官: 嗯,你接著演示你的可重復(fù)讀吧。

我: 嗯,好的,然后就是可重復(fù)讀,和之前一樣的操作。

我: 將兩個session開啟為REPEATABLE READ,同時開啟事務(wù),在第一個事務(wù)中先select,然后在第二個事務(wù)里面update數(shù)據(jù)行,可以發(fā)現(xiàn)即使第二個事務(wù)已經(jīng)commit,第一個事務(wù)再次select數(shù)據(jù)也還是沒有改變,這就解決了不可重復(fù)讀的問題。

我: 這里有個不同的地方就是在Mysql中,默認的不可重復(fù)讀個隔離級別也解決了幻讀的問題。

我: 第一個事務(wù)中先select一個id=3的數(shù)據(jù)行,這條數(shù)據(jù)行是不存在的,返回Empty set,然后第二個事務(wù)中insert一條id=3的數(shù)據(jù)行并且commit,第一個事務(wù)中再次select的,數(shù)據(jù)也好是沒有id=3的數(shù)據(jù)行。

我: 最后的串行化,樣式步驟也是一樣的,結(jié)果也和Mysql中默認的個可重復(fù)讀隔離級別的結(jié)果一樣,串行化的執(zhí)行流程相當(dāng)于把事務(wù)的執(zhí)行過程變?yōu)轫樞驁?zhí)行。

我: 這4個等級從上到下,隔離的效果是逐漸增強,但是性能卻是越來越差。

面試官: 哦?性能越來越差?為什么會性能越來越差?你能說一說原因嗎?

我: 這個得從Mysq的鎖說起,在Mysql中的鎖可以分為分享鎖/讀鎖(Shared Locks)、排他鎖/寫鎖(Exclusive Locks) 、間隙鎖、行鎖(Record Locks)、表鎖。

我: 在4個隔離級別中加鎖肯定是要消耗性能的,而讀未提交是沒有加任何鎖的,所以對于它來說也就是沒有隔離的效果,所以它的性能也是最好的。

我: 對于串行化加的是一把大鎖,讀的時候加共享鎖,不能寫,寫的時候,加的是排它鎖,阻塞其它事務(wù)的寫入和讀取,若是其它的事務(wù)長時間不能寫入就會直接報超時,所以它的性能也是最差的,對于它來就沒有什么并發(fā)性可言。

我: 對于讀提交和可重復(fù)讀,他們倆的實現(xiàn)是兼顧解決數(shù)據(jù)問題,然后又要有一定的并發(fā)行,所以在實現(xiàn)上鎖機制會比串行化優(yōu)化很多,提高并發(fā)性,所以性能也會比較好。

我: 他們倆的底層實現(xiàn)采用的是MVCC方式進行實現(xiàn)。

面試官: 你能先說一下先這幾個鎖的概念嗎?我不是很懂,說說你的理解。

我: 哦,好的,共享鎖是針對同一份數(shù)據(jù),多個讀操作可以同時進行,簡單來說即讀加鎖,不能寫并且可并行讀;排他鎖針對寫操作,假如當(dāng)前寫操作沒有完成,那么它會阻斷其它的寫鎖和讀鎖,即寫加鎖,其它讀寫都阻塞 。

我: 而行鎖和表鎖,是從鎖的粒度上進行劃分的,行鎖鎖定當(dāng)前數(shù)據(jù)行,鎖的粒度小,加鎖慢,發(fā)生鎖沖突的概率小,并發(fā)度高,行鎖也是MyISAM和InnoDB的區(qū)別之一,InnoDB支持行鎖并且支持事務(wù) 。

我: 而表鎖則鎖的粒度大,加鎖快,開銷小,但是鎖沖突的概率大,并發(fā)度低。

我: 間隙鎖則分為兩種:Gap Locks和Next-Key Locks。Gap Locks會鎖住兩個索引之間的區(qū)間,比如select * from User where id>3 and id<5 for update,就會在區(qū)間(3,5)之間加上Gap Locks。

我: Next-Key Locks是Gap Locks+Record Locks形成閉區(qū)間鎖select * from User where id>=3 and id=<5 for update,就會在區(qū)間[3,5]之間加上Next-Key Locks。

面試官: 那Mysql中什么時候會加鎖呢?

我: 在數(shù)據(jù)庫的增、刪、改、查中,只有增、刪、改才會加上排它鎖,而只是查詢并不會加鎖,只能通過在select語句后顯式加lock in share mode或者for update來加共享鎖或者排它鎖。

面試官: 你在上面提到MVCC,你能說一說原理嗎?

我: 在實現(xiàn)MVCC時用到了一致性視圖,用于支持讀提交和可重復(fù)讀的實現(xiàn)。

我: 在實現(xiàn)可重復(fù)讀的隔離級別,只需要在事務(wù)開始的時候創(chuàng)建一致性視圖,也叫做快照,之后的查詢里都共用這個一致性視圖,后續(xù)的事務(wù)對數(shù)據(jù)的更改是對當(dāng)前事務(wù)是不可見的,這樣就實現(xiàn)了可重復(fù)讀。

我: 而讀提交,每一個語句執(zhí)行前都會重新計算出一個新的視圖,這個也是可重復(fù)讀和讀提交在MVCC實現(xiàn)層面上的區(qū)別。

面試官: 那你知道快照在MVCC底層是怎么工作的嗎?

我: 在InnoDB 中每一個事務(wù)都有一個自己的事務(wù)id,并且是唯一的,遞增的 。

我: 對于Mysql中的每一個數(shù)據(jù)行都有可能存在多個版本,在每次事務(wù)更新數(shù)據(jù)的時候,都會生成一個新的數(shù)據(jù)版本,并且把自己的數(shù)據(jù)id賦值給當(dāng)前版本的row trx_id。

面試官: 你可以畫個圖看看嗎?

MVCC.png

我: 如圖中所示,假如三個事務(wù)更新了同一行數(shù)據(jù),那么就會有對應(yīng)的三個數(shù)據(jù)版本。

我: 實際上版本1、版本2并非實際物理存在的,而圖中的U1和U2實際就是undo log,這v1和v2版本是根據(jù)當(dāng)前v3和undo log計算出來的。

面試官: 那對于一個快照來說,你知道它要遵循什么規(guī)則嗎?

我: 對于一個事務(wù)視圖來說除了對自己更新的總是可見,另外還有三種情況:版本未提交的,都是不可見的;版本已經(jīng)提交,但是是在創(chuàng)建視圖之后提交的也是不可見的;版本已經(jīng)提交,若是在創(chuàng)建視圖之前提交的是可見的。

面試官: 假如兩個事務(wù)執(zhí)行寫操作,又怎么保證并發(fā)呢?

我: 假如事務(wù)1和事務(wù)2都要執(zhí)行update操作,事務(wù)1先update數(shù)據(jù)行的時候,先回獲取行鎖,鎖定數(shù)據(jù),當(dāng)事務(wù)2要進行update操作的時候,也會取獲取該數(shù)據(jù)行的行鎖,但是已經(jīng)被事務(wù)1占有,事務(wù)2只能wait。

我: 若是事務(wù)1長時間沒有釋放鎖,事務(wù)2就會出現(xiàn)超時異常 。

面試官: 這個是在update的where后的條件是在有索引的情況下吧?

我: 是的 。

面試官: 那沒有索引的條件下呢?沒辦法快速定位到數(shù)據(jù)行呢?

我: 若是沒有索引的條件下,就獲取所有行,都加上行鎖,然后Mysql會再次過濾符合條件的的行并釋放鎖,只有符合條件的行才會繼續(xù)持有鎖。

我: 這樣的性能消耗也會比較大。

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容