所在文集:數(shù)據(jù)庫(kù)
如何保證數(shù)據(jù)一致性
通過(guò)并發(fā)控制保證數(shù)據(jù)一致性的常見(jiàn)手段有:
- 鎖(Locking)
- 數(shù)據(jù)多版本(Multi Versioning)
如何使用普通鎖保證一致性凿傅?
- 操作數(shù)據(jù)前,鎖住滞项,實(shí)施互斥狭归,不允許其他的并發(fā)任務(wù)操作;
- 操作完成后文判,釋放鎖过椎,讓其他任務(wù)執(zhí)行;
普通鎖存在什么問(wèn)題戏仓?
簡(jiǎn)單的鎖住太過(guò)粗暴疚宇,連“讀任務(wù)”也無(wú)法并行亡鼠,任務(wù)執(zhí)行過(guò)程本質(zhì)上是串行的。
于是出現(xiàn)了共享鎖與排他鎖:
- 共享鎖(Share Locks敷待,記為S鎖)间涵,讀取數(shù)據(jù)時(shí)加 S 鎖
- 共享鎖之間不互斥,簡(jiǎn)記為:讀讀可以并行
- 排他鎖(eXclusive Locks榜揖,記為X鎖)勾哩,修改數(shù)據(jù)時(shí)加 X 鎖
- 排他鎖與任何鎖互斥,簡(jiǎn)記為:寫(xiě)讀举哟,寫(xiě)寫(xiě)不可以并行
- 可以看到思劳,一旦寫(xiě)數(shù)據(jù)的任務(wù)沒(méi)有完成,數(shù)據(jù)是不能被其他任務(wù)讀取的妨猩,這對(duì)并發(fā)度有較大的影響
有沒(méi)有可能潜叛,進(jìn)一步提高并發(fā)呢?
即使寫(xiě)任務(wù)沒(méi)有完成壶硅,其他讀任務(wù)也可能并發(fā)威兜,這就引出了數(shù)據(jù)多版本。
數(shù)據(jù)多版本是一種能夠進(jìn)一步提高并發(fā)的方法庐椒,它的核心原理是:
- 寫(xiě)任務(wù)發(fā)生時(shí)椒舵,將數(shù)據(jù)克隆一份,以版本號(hào)區(qū)分扼睬;
- 寫(xiě)任務(wù)操作新克隆的數(shù)據(jù)逮栅,直至提交;
- 并發(fā)讀任務(wù)可以繼續(xù)讀取舊版本的數(shù)據(jù)窗宇,不至于阻塞;
如上圖:
- 最開(kāi)始數(shù)據(jù)的版本是 V0特纤;
- T1 時(shí)刻發(fā)起了一個(gè)寫(xiě)任務(wù)军俊,這是把數(shù)據(jù)克隆了一份,進(jìn)行修改炭剪,版本變?yōu)?V1静尼,但任務(wù)還未完成捉貌;
- T2 時(shí)刻并發(fā)了一個(gè)讀任務(wù),依然可以讀 V0 版本的數(shù)據(jù)镰官;
- T3 時(shí)刻又并發(fā)了一個(gè)讀任務(wù),依然不會(huì)阻塞吗货;
可以看到泳唠,數(shù)據(jù)多版本,通過(guò)“讀取舊版本數(shù)據(jù)”能夠極大提高任務(wù)的并發(fā)度宙搬。
提高并發(fā)的演進(jìn)思路笨腥,就在如此:
- 普通鎖拓哺,本質(zhì)是串行執(zhí)行
- 讀寫(xiě)鎖,可以實(shí)現(xiàn)讀讀并發(fā)
- 數(shù)據(jù)多版本脖母,可以實(shí)現(xiàn)讀寫(xiě)并發(fā)
undo log
日志文件士鸥,記錄數(shù)據(jù)被 修改前 的值,用來(lái)進(jìn)行 rollback回滾谆级。
如某個(gè)事務(wù) T1烤礁,將 X 的值由 5 修改為 10,則 undo log 寫(xiě)入 <T1肥照,X脚仔,5>
對(duì)于 insert 操作,undo log 記錄新數(shù)據(jù)的 PK(ROW_ID)
建峭,回滾時(shí)直接刪除玻侥;
為什么要有 undo log?
數(shù)據(jù)庫(kù)事務(wù)未提交時(shí)亿蒸,會(huì)將事務(wù)修改數(shù)據(jù)的鏡像(即修改前的舊版本)存放到 undo log 里凑兰,當(dāng)事務(wù)回滾時(shí),或者數(shù)據(jù)庫(kù)奔潰時(shí)边锁,可以利用 undo log姑食,即舊版本數(shù)據(jù),撤銷(xiāo)未提交事務(wù)對(duì)數(shù)據(jù)庫(kù)產(chǎn)生的影響茅坛。
一句話音半,undo日志用于保障,未提交事務(wù)不會(huì)對(duì)數(shù)據(jù)庫(kù)的ACID特性產(chǎn)生影響贡蓖。
磁盤(pán)上 不存在 單獨(dú)的 undo log 文件曹鸠,所有的 undo log 均存放在主 ibd 數(shù)據(jù)文件中(表空間)。
記錄先寫(xiě)入到 undo buffer斥铺,但當(dāng)緩沖滿(mǎn)的時(shí)候彻桃,undo buffer 中的內(nèi)容會(huì)也會(huì)被刷新到磁盤(pán)。
redo log
日志文件晾蜘,記錄數(shù)據(jù)被 修改后 的值邻眷,回來(lái)恢復(fù)未寫(xiě)入到磁盤(pán)文件的已成功事務(wù)更新的數(shù)據(jù)。
如某個(gè)事務(wù) T1剔交,將 X 的值由 5 修改為 10肆饶,則 redo log 寫(xiě)入 <T1,X岖常,10>
為什么要有 redo log驯镊?
數(shù)據(jù)庫(kù)事務(wù)提交后,必須將更新后的數(shù)據(jù)刷到磁盤(pán)上,以保證 ACID 特性阿宅。磁盤(pán)隨機(jī)寫(xiě)性能較低候衍,如果每次都刷盤(pán),會(huì)極大影響數(shù)據(jù)庫(kù)的吞吐量洒放。
優(yōu)化方式是蛉鹿,將修改行為先寫(xiě)到 redo log 里,再定期將數(shù)據(jù)刷到磁盤(pán)上往湿,這樣能極大提高性能妖异。
假如某一時(shí)刻,數(shù)據(jù)庫(kù)崩潰领追,還沒(méi)來(lái)得及刷盤(pán)的數(shù)據(jù)他膳,在數(shù)據(jù)庫(kù)重啟后,會(huì)重做 redo log 日志里的內(nèi)容绒窑,以保證已提交事務(wù)對(duì)數(shù)據(jù)產(chǎn)生的影響都刷到磁盤(pán)上棕孙。
一句話,redo log 用于保障些膨,已提交事務(wù)的 ACID 特性蟀俊。
磁盤(pán)上 存在 單獨(dú)的 redo log 文件。
記錄先寫(xiě)入到 redo buffer订雾,但當(dāng)緩沖滿(mǎn)的時(shí)候肢预,redo buffer 中的內(nèi)容會(huì)也會(huì)被刷新到磁盤(pán)。
rollback segment 回滾段
回滾段洼哎。在 InnoDB 中烫映,undo log 被劃分為多個(gè)段,具體某行的 undo log 就保存在某個(gè)段中噩峦,稱(chēng)為回滾段锭沟。
事務(wù)的過(guò)程
假設(shè) User 表有兩個(gè)字段 <name, salary>
。
InnoDB 為每行記錄都實(shí)現(xiàn)了三個(gè)隱藏字段:
- 6 字節(jié)识补,單調(diào)遞增的行 ID(
DB_ROW_ID
) - 6 字節(jié)冈钦,記錄每一行最近一次修改它的事務(wù) ID(
DB_TRX_ID
) - 7 字節(jié),回滾指針(
DB_ROLL_PTR
)李请,記錄指向回滾段 undo log 的指針
因此實(shí)際的 User 表有五個(gè)字段 <name, salary, ID, DB_TRX_ID, DB_ROLL_PTR>
。
假設(shè)某一行的初始值為:<Tom, 10000, 1, NULL, NULL>
假設(shè)某一個(gè)事務(wù)里執(zhí)行了 update users set salary = 20000 where name = 'Tom'
厉熟,存儲(chǔ)引擎會(huì)依次進(jìn)行如下操作:
- 用 排他鎖 鎖定該行导盅。
- 記錄 redo log,即被 修改后 的值
<Tom, 20000>
- 記錄 undo log揍瑟,即被 修改前 的值
<Tom, 10000, 1, NULL, NULL>
- 修改當(dāng)前數(shù)據(jù)表中的值白翻,變?yōu)?
<Tom, 20000, 1, 01, DB_ROLL_PTR>
,其中DB_ROLL_PTR
指向之前 undo log 中的那一行
若 提交了 事務(wù)
只需要更改事務(wù)狀態(tài)為 COMMIT 即可,不需做其他額外的工作滤馍。
若 回滾了 事務(wù)
需要根據(jù)當(dāng)前 DB_ROLL_PTR
回滾指針 從 undo log 中找出事務(wù)修改前的版本岛琼,并恢復(fù)。
一個(gè)具體的示例
數(shù)據(jù)表 t(id PK, name)
數(shù)據(jù)為:
1, shenjian
2, zhangsan
3, lisi
此時(shí)沒(méi)有事務(wù)未提交巢株,故回滾段是空的槐瑞。
接著啟動(dòng)了一個(gè)事務(wù),并且事務(wù)處于未提交的狀態(tài)阁苞。
start trx;
delete (1, shenjian);
update set(3, lisi) to (3, xxx);
insert (4, wangwu);
可以看到:
- 被刪除前的
(1, shenjian)
作為舊版本數(shù)據(jù)困檩,進(jìn)入了回滾段; - 被修改前的
(3, lisi)
作為舊版本數(shù)據(jù)那槽,進(jìn)入了回滾段悼沿; - 被插入的數(shù)據(jù)
PK(4)
進(jìn)入了回滾段;
接下來(lái)骚灸,假如事務(wù) rollback糟趾,此時(shí)可以通過(guò)回滾段里的 undo log 回滾。
可以看到:
- 被刪除的舊數(shù)據(jù)恢復(fù)了甚牲;
- 被修改的舊數(shù)據(jù)也恢復(fù)了义郑;
- 被插入的數(shù)據(jù),刪除了鳖藕;
InnoDB 是基于多版本并發(fā)控制的存儲(chǔ)引擎
InnoDB 是高并發(fā)互聯(lián)網(wǎng)場(chǎng)景最為推薦的存儲(chǔ)引擎魔慷,根本原因,就是其多版本并發(fā)控制(Multi Version Concurrency Control, MVCC)著恩。行鎖院尔,并發(fā),事務(wù)回滾等多種特性都和 MVCC 相關(guān)喉誊。
MVCC就是通過(guò)“讀取舊版本數(shù)據(jù)”來(lái)降低并發(fā)事務(wù)的鎖沖突邀摆,提高任務(wù)的并發(fā)度。
InnoDB為何能夠做到這么高的并發(fā)伍茄?
回滾段里的數(shù)據(jù)栋盹,其實(shí)是歷史數(shù)據(jù)的快照(snapshot),這些數(shù)據(jù)是不會(huì)被修改敷矫,select
可以肆無(wú)忌憚的并發(fā)讀取他們例获。
快照讀(Snapshot Read),這種一致性不加鎖的讀(Consistent Nonlocking Read)曹仗,就是 InnoDB 并發(fā)如此之高的核心原因之一榨汤。
這里的一致性是指,事務(wù)讀取到的數(shù)據(jù)怎茫,要么是事務(wù)開(kāi)始前就已經(jīng)存在的數(shù)據(jù)(當(dāng)然收壕,是其他已提交事務(wù)產(chǎn)生的),要么是事務(wù)自身插入或者修改的數(shù)據(jù)蜜宪。
什么樣的 select
是快照讀虫埂?
除非顯示加鎖,普通的select語(yǔ)句都是快照讀圃验,例如:
select * from t where id>2;
這里的顯示加鎖,非快照讀是指:
select * from t where id>2 lock in share mode;
select * from t where id>2 for update;
總結(jié):
- 常見(jiàn)并發(fā)控制保證數(shù)據(jù)一致性的方法有鎖损谦,數(shù)據(jù)多版本;
- 普通鎖串行闯参,讀寫(xiě)鎖讀讀并行悲立,數(shù)據(jù)多版本讀寫(xiě)并行鹿寨;
- redo log 保證已提交事務(wù)的 ACID 特性脚草,設(shè)計(jì)思路是,通過(guò)順序?qū)懱娲S機(jī)寫(xiě)原献,提高并發(fā)馏慨;
- undo log 用來(lái)回滾未提交的事務(wù),它存儲(chǔ)在回滾段里姑隅;
- InnoDB 是基于 MVCC 的存儲(chǔ)引擎写隶,它利用了存儲(chǔ)在回滾段里的 undo log,即數(shù)據(jù)的舊版本讲仰,提高并發(fā)慕趴;
- InnoDB 之所以并發(fā)高,快照讀不加鎖鄙陡;
- InnoDB 所有普通
select
都是快照讀冕房;
引用:
Mysql InnoDB MVCC 實(shí)現(xiàn)原理
InnoDB并發(fā)如此高,原因竟然在這趁矾?