二階段鎖
對于Innodb中的行鎖,實際上是在需要的時候才加模燥,但并不是不需要了就立即釋放,而是等著事務結(jié)束了才會釋放心肪。其實這句話就是二階段鎖的含義。因此硬鞍,如果再事務中需要鎖多個行戴已,需要把最可能造成鎖沖突、最可能影響并發(fā)度的鎖盡量往后放伐坏,這樣盡可能減少持有鎖的時間握联。
現(xiàn)在舉個例子:需要在美團app上買一張故宮的門票
- 從顧客的賬戶中扣除門票錢
- 給美團的賬戶中增加門票錢
- 記錄一條交易log
從上面的例子中可以看到,現(xiàn)在需要update兩條記錄纯露,insert一條記錄代芜。事務是一定需要的,不然一旦某條sql失敗钞速,會導致數(shù)據(jù)不一致問題嫡秕。那么這三條數(shù)據(jù)如何安排順序呢?
按照上面二階段鎖的定義昆咽,相關(guān)行級鎖應該持有時間最小屠升,所以這種背景下,兩個update一定要放在最后汇在,第三條放在第一。這似乎就已經(jīng)是比較好的解決方案亩鬼。
那下面這個例子:
現(xiàn)在要刪除10000行數(shù)據(jù)阿蝶,有以下幾種方案:
- 執(zhí)行sql:delete from T limit 10000;
- 一個連接中循環(huán)20次 delete from T limit 500;
- 20個連接中執(zhí)行delete from T limit 500;
這幾種方法哪個比較好呢?
- 根據(jù)二階段鎖玷过,占用這些行級鎖的時間很長筑煮,導致其他客戶端等待資源時間較長
- 長事務分成短事務,每次事務占用鎖的時間比較短袋马,其他客戶端等待時間較短秸应,可以提高并發(fā)度
- 人為制造所競爭,加劇并發(fā)量软啼,實際意義不大
二階段提交
二階段提交涉及到一個更新sql語句的處理流程和redo log和binlog
現(xiàn)在看下一條更新sql是如何執(zhí)行的
CREATE TABLE `new_table` (
`id` int(11) NOT NULL,
`num` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
現(xiàn)在要把id=1的這一行+1焰宣,會這么寫
update new_table set num = num + 1 where id=1
相關(guān)執(zhí)行流程
涉及到update操作的時候,會涉及到redo log和binlog
redo log
每次更新的時候盈罐,可以想一想闪唆,其實并不是每次都更立刻更新到磁盤上了,這樣的話IO很頻繁票顾,響應也會很慢,為了解決這個問題奠骄,出現(xiàn)了redo log。
當有一條記錄要更新的時候影锈,會先把記錄寫進redo log里面,并更新內(nèi)存鸭廷,這個時候更新就算完成了熔吗,就可以返回客戶端了。同事桅狠,innodb會在適當?shù)臅r候,將這個操作記錄更新到磁盤里面维雇,而這個往往是系統(tǒng)比較空閑的時候做的晒他。但是如果系統(tǒng)更新的頻率比較高,redo log寫滿了陨仅,該怎么辦?這個時候只能停止更新触徐,先把redo log的內(nèi)容刷到磁盤里面狐赡,把redo log清空
可以想象,把redo log里面的數(shù)據(jù)刷到磁盤這個過程是很痛苦的颖侄,因為磁盤IO比較高,所以這個時候所有的查詢更新操作孝鹊,都會有抖動展蒂,這種情況下也叫做刷臟頁苔咪,相關(guān)刷臟頁就不介紹了柳骄,可以看下面這個介紹
https://gsmtoday.github.io/2019/02/08/flush/
binlog
從上面執(zhí)行流程的圖可以看到,一個sql執(zhí)行過程中馆里,主要分為客戶端、server端、存儲引擎丙者。redo log是存儲引擎innodb特有的日志,binlog是server端特有的日志目锭。所以說纷捞,在使用過程中,如果數(shù)據(jù)庫異常重啟主儡,之前提交的記錄會在redo log中,因為redo log是存儲引擎級別的丰捷,所以記錄不會丟失寂汇,這個也叫做crash-safe
那么為啥會有兩部分日志?myIsam出現(xiàn)的比innodb早停巷,之前在server端是有binlog的,binlog做的是歸檔工作畔勤,臼膏,歸檔工作自然是沒有crash-safe能力,所以后來innodb用了redo log來實現(xiàn)crash-safe能力嚷硫。那么這兩個日志主要的區(qū)別是什么呢?
- redo log是存儲引擎級別的仔掸,是innodb特有的;binlog是server端持有的起暮,是所有引擎都有的功能
- redo log是物理日志,記錄的是在某個數(shù)據(jù)頁做了什么修改筒捺;binlog是邏輯日志纸厉,記錄的是這個語句的原始邏輯,如果是statement格式的話肯尺,記錄的是sql語句躯枢,如果是row格式的話,記錄的是行的內(nèi)容锄蹂,包含更新前更新后的
- redo log是循環(huán)寫的,空間固定(一般是4個g寨昙,1個g為一組)掀亩;binlog是可以追加寫入的,就是說binlog文件寫完會寫入下一個文件槽棍,不會覆蓋之前的記錄
下面介紹一下上面sql語句具體的執(zhí)行流程
- 首先找到id=1這行炼七。因為id是主鍵,所以直接走主鍵索引樹查找到對應的記錄即可
- 看下當前數(shù)據(jù)所在數(shù)據(jù)頁是否在內(nèi)存中豌拙,如果在內(nèi)存中,直接返回給執(zhí)行器按傅,如果沒有再內(nèi)存中胧卤,需要把當前數(shù)據(jù)頁加載到內(nèi)存中枝誊。還要看數(shù)據(jù)頁加載到內(nèi)存后容量是否足夠,如果不夠叶撒,需要刷臟頁
- 執(zhí)行器拿到存儲引擎給的數(shù)據(jù)耐版,把這個值+1,得到新的數(shù)據(jù)再寫會存儲引擎
- 存儲引擎把這部分數(shù)據(jù)更新到內(nèi)存中哪审,同時將這個更新記錄到redo log中(在第幾個數(shù)據(jù)頁把某個數(shù)據(jù)改為某個數(shù)據(jù))虑瀑,此時redo log處于prepare狀態(tài)舌狗,告訴執(zhí)行器執(zhí)行完成扔水,隨時可以提交事務
- 執(zhí)行器生成這個操作的binlog,把binlog寫入磁盤
- 執(zhí)行器調(diào)用引擎的提交事務接口主届,引擎把剛剛寫入的redo log改為commit狀態(tài)
其實前3步應該還比較好理解,后三步事務結(jié)束的時候君丁,分成了兩個狀態(tài)處理了
二階段提交
為什么要把事務的commit分成兩個階段呢将宪?從prepare->commit。這個主要是為了讓binlog和redo log的狀態(tài)是一致的印蔗。
如果不用二階段提交會是什么結(jié)果呢丑勤?是數(shù)據(jù)被存儲引擎更新到內(nèi)存后,是先寫binlog還是先commit redo log呢耙厚?
- 先寫binlog。如果在binlog寫完以后颜曾,server端crash,這就導致redo log還沒寫稠诲,那么如果系統(tǒng)恢復后诡曙,redo log并沒有記錄這行數(shù)據(jù)有改動,但是binlog記錄了這行數(shù)據(jù)+1劝萤,這就導致binlog恢復后多了一個事務慎璧,和原來庫中的值已經(jīng)不同了
- 先commit redo log。redo log commit后胸私,mysql進程異常重啟,binlog寫入失敗阔涉,日志備份就沒這個變化了瑰排。那以后如果使用binlog恢復長期的數(shù)據(jù),那就跟真實的數(shù)據(jù)出現(xiàn)出入了暖侨。
所以可見,如果不用二階段提交函荣,怎么樣都有可能出現(xiàn)數(shù)據(jù)不一致扳肛。那么為啥二階段可以保證?
上面第五步結(jié)束后金拒,系統(tǒng)server端crash沒有通知到存儲引擎,redo log沒有寫入资铡,但是prepare和binlog完整笤休,重啟后會自動commit。也就是說這個時候保存的是修改后的數(shù)據(jù)
上面第四步結(jié)束后店雅,由于沒有寫入binlog贞铣,所以會直接回滾。這樣binlog和redo log就一致了窍奋。