在今天這篇答疑文章更新前,MySQL實戰這個專欄已經更新了14篇。在這些文章中,你們在評論區留下了不少高質量的留言。如今,每篇文章的評論區都有熱心的同窗幫忙總結文章知識點,也有很多同窗提出了不少高質量的問題,更有一些同窗幫忙解答其餘同窗提出的問題。mysql
在瀏覽這些留言並回復的過程當中,我倍受鼓舞,也盡我所知地幫助你解決問題、和你討論。能夠說,大家的留言活躍了整個專欄的氛圍、提高了整個專欄的質量,謝謝大家。sql
評論區的大多數留言我都直接回復了,對於須要展開說明的問題,我都拿出小本子記了下來。這些被記下來的問題,就是咱們今天這篇答疑文章的素材了。數據庫
到目前爲止,我已經收集了47個問題,很難經過今天這一篇文章所有展開。因此,我就先從中找了幾個聯繫很是緊密的問題,串了起來,但願能夠幫你解決關於日誌和索引的一些疑惑。而其餘問題,咱們就留着後面慢慢展開吧。session
我在第2篇文章《日誌系統:一條SQL更新語句是如何執行的?》中,和你講到binlog(歸檔日誌)和redo log(重作日誌)配合崩潰恢復的時候,用的是反證法,說明了若是沒有兩階段提交,會致使MySQL出現主備數據不一致等問題。併發
在這篇文章下面,不少同窗在問,在兩階段提交的不一樣瞬間,MySQL若是發生異常重啓,是怎麼保證數據完整性的?分佈式
如今,咱們就從這個問題開始吧。性能
我再放一次兩階段提交的圖,方便你學習下面的內容。學習
這裏,我要先和你解釋一個誤會式的問題。有同窗在評論區問到,這個圖不是一個update語句的執行流程嗎,怎麼還會調用commit語句?優化
他產生這個疑問的緣由,是把兩個「commit」的概念混淆了:spa
而咱們這個例子裏面,沒有顯式地開啓事務,所以這個update語句本身就是一個事務,在執行完成後提交事務時,就會用到這個「commit步驟「。
接下來,咱們就一塊兒分析一下在兩階段提交的不一樣時刻,MySQL異常重啓會出現什麼現象。
若是在圖中時刻A的地方,也就是寫入redo log 處於prepare階段以後、寫binlog以前,發生了崩潰(crash),因爲此時binlog還沒寫,redo log也還沒提交,因此崩潰恢復的時候,這個事務會回滾。這時候,binlog還沒寫,因此也不會傳到備庫。到這裏,你們均可以理解。
你們出現問題的地方,主要集中在時刻B,也就是binlog寫完,redo log還沒commit前發生crash,那崩潰恢復的時候MySQL會怎麼處理?
咱們先來看一下崩潰恢復時的判斷規則。
若是redo log裏面的事務是完整的,也就是已經有了commit標識,則直接提交;
若是redo log裏面的事務只有完整的prepare,則判斷對應的事務binlog是否存在並完整:
a. 若是是,則提交事務;
b. 不然,回滾事務。
這裏,時刻B發生crash對應的就是2(a)的狀況,崩潰恢復過程當中事務會被提交。
如今,咱們繼續延展一下這個問題。
回答:一個事務的binlog是有完整格式的:
另外,在MySQL 5.6.2版本之後,還引入了binlog-checksum參數,用來驗證binlog內容的正確性。對於binlog日誌因爲磁盤緣由,可能會在日誌中間出錯的狀況,MySQL能夠經過校驗checksum的結果來發現。因此,MySQL仍是有辦法驗證事務binlog的完整性的。
回答:它們有一個共同的數據字段,叫XID。崩潰恢復的時候,會按順序掃描redo log:
回答:其實,這個問題仍是跟咱們在反證法中說到的數據與備份的一致性有關。在時刻B,也就是binlog寫完之後MySQL發生崩潰,這時候binlog已經寫入了,以後就會被從庫(或者用這個binlog恢復出來的庫)使用。
因此,在主庫上也要提交這個事務。採用這個策略,主庫和備庫的數據就保證了一致性。
回答:其實,兩階段提交是經典的分佈式系統問題,並非MySQL獨有的。
若是必需要舉一個場景,來講明這麼作的必要性的話,那就是事務的持久性問題。
對於InnoDB引擎來講,若是redo log提交完成了,事務就不能回滾(若是這還容許回滾,就可能覆蓋掉別的事務的更新)。而若是redo log直接提交,而後binlog寫入的時候失敗,InnoDB又回滾不了,數據和binlog日誌又不一致了。
兩階段提交就是爲了給全部人一個機會,當每一個人都說「我ok」的時候,再一塊兒提交。
回答:這位同窗的意思是,只保留binlog,而後能夠把提交流程改爲這樣:… -> 「數據更新到內存」 -> 「寫 binlog」 -> 「提交事務」,是否是也能夠提供崩潰恢復的能力?
答案是不能夠。
若是說歷史緣由的話,那就是InnoDB並非MySQL的原生存儲引擎。MySQL的原生引擎是MyISAM,設計之初就有沒有支持崩潰恢復。
InnoDB在做爲MySQL的插件加入MySQL引擎家族以前,就已是一個提供了崩潰恢復和事務支持的引擎了。
InnoDB接入了MySQL後,發現既然binlog沒有崩潰恢復的能力,那就用InnoDB原有的redo log好了。
而若是說實現上的緣由的話,就有不少了。就按照問題中說的,只用binlog來實現崩潰恢復的流程,我畫了一張示意圖,這裏就沒有redo log了。
這樣的流程下,binlog仍是不能支持崩潰恢復的。我說一個不支持的點吧:binlog沒有能力恢復「數據頁」。
若是在圖中標的位置,也就是binlog2寫完了,可是整個事務尚未commit的時候,MySQL發生了crash。
重啓後,引擎內部事務2會回滾,而後應用binlog2能夠補回來;可是對於事務1來講,系統已經認爲提交完成了,不會再應用一次binlog1。
可是,InnoDB引擎使用的是WAL技術,執行事務的時候,寫完內存和日誌,事務就算完成了。若是以後崩潰,要依賴於日誌來恢復數據頁。
也就是說在圖中這個位置發生崩潰的話,事務1也是可能丟失了的,並且是數據頁級的丟失。此時,binlog裏面並無記錄數據頁的更新細節,是補不回來的。
你若是要說,那我優化一下binlog的內容,讓它來記錄數據頁的更改能夠嗎?但,這其實就是又作了一個redo log出來。
因此,至少如今的binlog能力,還不能支持崩潰恢復。
回答:若是隻從崩潰恢復的角度來說是能夠的。你能夠把binlog關掉,這樣就沒有兩階段提交了,但系統依然是crash-safe的。
可是,若是你瞭解一下業界各個公司的使用場景的話,就會發如今正式的生產庫上,binlog都是開着的。由於binlog有着redo log沒法替代的功能。
一個是歸檔。redo log是循環寫,寫到末尾是要回到開頭繼續寫的。這樣歷史日誌無法保留,redo log也就起不到歸檔的做用。
一個就是MySQL系統依賴於binlog。binlog做爲MySQL一開始就有的功能,被用在了不少地方。其中,MySQL系統高可用的基礎,就是binlog複製。
還有不少公司有異構系統(好比一些數據分析系統),這些系統就靠消費MySQL的binlog來更新本身的數據。關掉binlog的話,這些下游系統就無法輸入了。
總之,因爲如今包括MySQL高可用在內的不少系統機制都依賴於binlog,因此「鳩佔鵲巢」redo log還作不到。你看,發展生態是多麼重要。
回答:redo log過小的話,會致使很快就被寫滿,而後不得不強行刷redo log,這樣WAL機制的能力就發揮不出來了。
因此,若是是如今常見的幾個TB的磁盤的話,就不要過小氣了,直接將redo log設置爲4個文件、每一個文件1GB吧。
回答:這個問題其實問得很是好。這裏涉及到了,「redo log裏面究竟是什麼」的問題。
實際上,redo log並無記錄數據頁的完整數據,因此它並無能力本身去更新磁盤數據頁,也就不存在「數據最終落盤,是由redo log更新過去」的狀況。
若是是正常運行的實例的話,數據頁被修改之後,跟磁盤的數據頁不一致,稱爲髒頁。最終數據落盤,就是把內存中的數據頁寫盤。這個過程,甚至與redo log毫無關係。
在崩潰恢復場景中,InnoDB若是判斷到一個數據頁可能在崩潰恢復的時候丟失了更新,就會將它讀到內存,而後讓redo log更新內存內容。更新完成後,內存頁變成髒頁,就回到了第一種狀況的狀態。
回答:這兩個問題能夠一塊兒回答。
在一個事務的更新過程當中,日誌是要寫屢次的。好比下面這個事務:
begin; insert into t1 ... insert into t2 ... commit;
這個事務要往兩個表中插入記錄,插入數據的過程當中,生成的日誌都得先保存起來,但又不能在還沒commit的時候就直接寫到redo log文件裏。
因此,redo log buffer就是一塊內存,用來先存redo日誌的。也就是說,在執行第一個insert的時候,數據的內存被修改了,redo log buffer也寫入了日誌。
可是,真正把日誌寫到redo log文件(文件名是 ib_logfile+數字),是在執行commit語句的時候作的。
(這裏說的是事務執行過程當中不會「主動去刷盤」,以減小沒必要要的IO消耗。可是可能會出現「被動寫入磁盤」,好比內存不夠、其餘事務提交等狀況。這個問題咱們會在後面第22篇文章《MySQL有哪些「飲鴆止渴」的提升性能的方法?》中再詳細展開)。
單獨執行一個更新語句的時候,InnoDB會本身啓動一個事務,在語句執行完成的時候提交。過程跟上面是同樣的,只不過是「壓縮」到了一個語句裏面完成。
以上這些問題,就是把你們提過的關於redo log和binlog的問題串起來,作的一次集中回答。若是你還有問題,能夠在評論區繼續留言補充。
接下來,我再和你分享@ithunter 同窗在第8篇文章《事務究竟是隔離的仍是不隔離的?》的評論區提到的跟索引相關的一個問題。我以爲這個問題挺有趣、也挺實用的,其餘同窗也可能會碰上這樣的場景,在這裏解答和分享一下。
問題是這樣的(我文字上稍微作了點修改,方便你們理解):
業務上有這樣的需求,A、B兩個用戶,若是互相關注,則成爲好友。設計上是有兩張表,一個是like表,一個是friend表,like表有user_id、liker_id兩個字段,我設置爲複合惟一索引即uk_user_id_liker_id。語句執行邏輯是這樣的:
以A關注B爲例:
第一步,先查詢對方有沒有關注本身(B有沒有關注A)
select * from like where user_id = B and liker_id = A;
若是有,則成爲好友
insert into friend;
沒有,則只是單向關注關係
insert into like;
可是若是A、B同時關注對方,會出現不會成爲好友的狀況。由於上面第1步,雙方都沒關注對方。第1步即便使用了排他鎖也不行,由於記錄不存在,行鎖沒法生效。請問這種狀況,在MySQL鎖層面有沒有辦法處理?
首先,我要先贊一下這樣的提問方式。雖然極客時間如今的評論區還不能追加評論,但若是你們可以一次留言就把問題講清楚的話,其實影響也不大。因此,我但願你在留言提問的時候,也能借鑑這種方式。
接下來,我把@ithunter 同窗說的表模擬出來,方便咱們討論。
CREATE TABLE `like` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `liker_id` int(11) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id_liker_id` (`user_id`,`liker_id`) ) ENGINE=InnoDB; CREATE TABLE `friend` ( id` int(11) NOT NULL AUTO_INCREMENT, `friend_1_id` int(11) NOT NULL, `firned_2_id` int(11) NOT NULL, UNIQUE KEY `uk_friend` (`friend_1_id`,`firned_2_id`) PRIMARY KEY (`id`) ) ENGINE=InnoDB;
雖然這個題幹中,並無說到friend表的索引結構。但我猜想friend_1_id和friend_2_id也有索引,爲便於描述,我給加上惟一索引。
順便說明一下,「like」是關鍵字,我通常不建議使用關鍵字做爲庫名、表名、字段名或索引名。
我把他的疑問翻譯一下,在併發場景下,同時有兩我的,設置爲關注對方,就可能致使沒法成功加爲朋友關係。
如今,我用你已經熟悉的時刻順序表的形式,把這兩個事務的執行語句列出來:
因爲一開始A和B之間沒有關注關係,因此兩個事務裏面的select語句查出來的結果都是空。
所以,session 1的邏輯就是「既然B沒有關注A,那就只插入一個單向關注關係」。session 2也一樣是這個邏輯。
這個結果對業務來講就是bug了。由於在業務設定裏面,這兩個邏輯都執行完成之後,是應該在friend表裏面插入一行記錄的。
如提問裏面說的,「第1步即便使用了排他鎖也不行,由於記錄不存在,行鎖沒法生效」。不過,我想到了另一個方法,來解決這個問題。
首先,要給「like」表增長一個字段,好比叫做 relation_ship,並設爲整型,取值一、二、3。
值是1的時候,表示user_id 關注 liker_id;
值是2的時候,表示liker_id 關注 user_id;
值是3的時候,表示互相關注。
而後,當 A關注B的時候,邏輯改爲以下所示的樣子:
應用代碼裏面,比較A和B的大小,若是A<B,就執行下面的邏輯
mysql> begin; /*啓動事務*/ insert into `like`(user_id, liker_id, relation_ship) values(A, B, 1) on duplicate key update relation_ship=relation_ship | 1; select relation_ship from `like` where user_id=A and liker_id=B; /*代碼中判斷返回的 relation_ship, 若是是1,事務結束,執行 commit 若是是3,則執行下面這兩個語句: */ insert ignore into friend(friend_1_id, friend_2_id) values(A,B); commit;
若是A>B,則執行下面的邏輯
mysql> begin; /*啓動事務*/ insert into `like`(user_id, liker_id, relation_ship) values(B, A, 2) on duplicate key update relation_ship=relation_ship | 2; select relation_ship from `like` where user_id=B and liker_id=A; /*代碼中判斷返回的 relation_ship, 若是是2,事務結束,執行 commit 若是是3,則執行下面這兩個語句: */ insert ignore into friend(friend_1_id, friend_2_id) values(B,A); commit;
這個設計裏,讓「like」表裏的數據保證user_id < liker_id,這樣不管是A關注B,仍是B關注A,在操做「like」表的時候,若是反向的關係已經存在,就會出現行鎖衝突。
而後,insert … on duplicate語句,確保了在事務內部,執行了這個SQL語句後,就強行佔住了這個行鎖,以後的select 判斷relation_ship這個邏輯時就確保了是在行鎖保護下的讀操做。
操做符 「|」 是按位或,連同最後一句insert語句裏的ignore,是爲了保證重複調用時的冪等性。
這樣,即便在雙方「同時」執行關注操做,最終數據庫裏的結果,也是like表裏面有一條關於A和B的記錄,並且relation_ship的值是3, 而且friend表裏面也有了A和B的這條記錄。
不知道你會不會吐槽:以前明明還說盡可能不要使用惟一索引,結果這個例子一上來我就建立了兩個。這裏我要再和你說明一下,以前文章咱們討論的,是在「業務開發保證不會插入重複記錄」的狀況下,着重要解決性能問題的時候,才建議儘可能使用普通索引。
而像這個例子裏,按照這個設計,業務根本就是保證「我必定會插入重複數據,數據庫必定要要有惟一性約束」,這時就沒啥好說的了,惟一索引建起來吧。
這是專欄的第一篇答疑文章。
我針對前14篇文章,你們在評論區中的留言,從中摘取了關於日誌和索引的相關問題,串成了今天這篇文章。這裏我也要再和你說一聲,有些我答應在答疑文章中進行擴展的話題,今天這篇文章沒來得及擴展,後續我會再找機會爲你解答。因此,篇幅所限,評論區見吧。
最後,雖然這篇是答疑文章,但課後問題仍是要有的。
咱們建立了一個簡單的表t,並插入一行,而後對這一行作修改。
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL primary key auto_increment, `a` int(11) DEFAULT NULL ) ENGINE=InnoDB; insert into t values(1,2);
這時候,表t裏有惟一的一行數據(1,2)。假設,我如今要執行:
mysql> update t set a=2 where id=1;
你會看到這樣的結果:
結果顯示,匹配(rows matched)了一行,修改(Changed)了0行。
僅從現象上看,MySQL內部在處理這個命令的時候,能夠有如下三種選擇:
更新都是先讀後寫的,MySQL讀出數據,發現a的值原本就是2,不更新,直接返回,執行結束;
MySQL調用了InnoDB引擎提供的「修改成(1,2)」這個接口,可是引擎發現值與原來相同,不更新,直接返回;
InnoDB認真執行了「把這個值修改爲(1,2)"這個操做,該加鎖的加鎖,該更新的更新。
你以爲實際狀況會是以上哪一種呢?你能否用構造實驗的方式,來證實你的結論?進一步地,能夠思考一下,MySQL爲何要選擇這種策略呢?
你能夠把你的驗證方法和思考寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期的問題是,用一個計數表記錄一個業務表的總行數,在往業務表插入數據的時候,須要給計數值加1。
邏輯實現上是啓動一個事務,執行兩個語句:
insert into 數據表;
update 計數表,計數值加1。
從系統併發能力的角度考慮,怎麼安排這兩個語句的順序。
這裏,我直接複製 @阿建 的回答過來供你參考:
併發系統性能的角度考慮,應該先插入操做記錄,再更新計數表。
知識點在《行鎖功過:怎麼減小行鎖對性能的影響?》
由於更新計數表涉及到行鎖的競爭,先插入再更新能最大程度地減小事務之間的鎖等待,提高併發度。
評論區有同窗說,應該把update計數表放後面,由於這個計數表可能保存了多個業務表的計數值。若是把update計數表放到事務的第一個語句,多個業務表同時插入數據的話,等待時間會更長。
這個答案的結論是對的,可是理解不太正確。即便咱們用一個計數表記錄多個業務表的行數,也確定會給表名字段加惟一索引。相似於下面這樣的表結構:
CREATE TABLE `rows_stat` ( `table_name` varchar(64) NOT NULL, `row_count` int(10) unsigned NOT NULL, PRIMARY KEY (`table_name`) ) ENGINE=InnoDB;
在更新計數表的時候,必定會傳入where table_name=$table_name,使用主鍵索引,更新加行鎖只會鎖在一行上。
而在不一樣業務表插入數據,是更新不一樣的行,不會有行鎖。