說(shuō)說(shuō)一致性讀實(shí)現(xiàn)原理?
本文轉(zhuǎn)載自微信公眾號(hào)「三太子敖丙」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系三太子敖丙公眾號(hào)。
這個(gè)問(wèn)題是我當(dāng)初在面天貓的時(shí)候,2面的面試官問(wèn)我的,我之前已經(jīng)寫(xiě)過(guò)mvcc的文章了,但是在看到我筆記的里的這個(gè)問(wèn)題的時(shí)候我準(zhǔn)備單獨(dú)理一遍,所以就有了這個(gè)文章。
現(xiàn)在,主流關(guān)系型數(shù)據(jù)庫(kù)產(chǎn)品基本都實(shí)現(xiàn)了MVCC的特性,快照在MVCC中起著重要的作用,代表某一時(shí)刻數(shù)據(jù)的版本,它是實(shí)現(xiàn)一致性讀的基礎(chǔ)。在更新操作沒(méi)提交前,數(shù)據(jù)的前鏡像存儲(chǔ)在Undo中,利用Undo可以實(shí)現(xiàn)一致性讀,事務(wù)回滾以及異?;謴?fù)等操作,下面就聊聊MySQL事務(wù),MVCC,快照及一致讀的原理與實(shí)現(xiàn)。
MySQL中的事務(wù)
事務(wù)在RDBMS系統(tǒng)中概念基本都是一樣的,是由一組DML語(yǔ)句構(gòu)的工作單元,要么全部成功,要么全部失敗。
開(kāi)發(fā)過(guò)程中,比較關(guān)心長(zhǎng)事務(wù),即包含DML語(yǔ)句多的工作單元,事務(wù)太長(zhǎng)會(huì)導(dǎo)致一些錯(cuò)誤,例如可能由于事務(wù)數(shù)據(jù)包大小超過(guò)參數(shù)max_allowed_packet設(shè)置會(huì)導(dǎo)致程序報(bào)錯(cuò),也可能有事務(wù)中某個(gè)SQL對(duì)應(yīng)接口報(bào)錯(cuò),導(dǎo)致整個(gè)服務(wù)調(diào)用失敗,在程序設(shè)計(jì)時(shí),應(yīng)該考慮避免長(zhǎng)事務(wù)帶來(lái)的業(yè)務(wù)影響。
事務(wù)的ACID
image-20201114221841801
原子性是事務(wù)隔離的基礎(chǔ),隔離性和持久性是手段,最終目的是為了保持?jǐn)?shù)據(jù)的一致性。
事務(wù)的并發(fā)問(wèn)題
- 臟讀:事務(wù)A讀取了事務(wù)B未提交的數(shù)據(jù)。
- 不可重復(fù)度:事務(wù)A多次讀取同一份數(shù)據(jù),事務(wù)B在此過(guò)程中對(duì)數(shù)據(jù)修改并提交,導(dǎo)致事務(wù)A多次讀取同一份數(shù)據(jù)的結(jié)果不一致。
- 幻讀:事務(wù)A修改數(shù)據(jù)的同時(shí),事務(wù)B插入了一條數(shù)據(jù),當(dāng)事務(wù)A提交后發(fā)現(xiàn)還有數(shù)據(jù)沒(méi)被修改,產(chǎn)生了幻覺(jué)。
不可重復(fù)讀側(cè)重于update操作,幻讀側(cè)重于insert或delete。解決不可重復(fù)讀的問(wèn)題只需鎖住滿(mǎn)足條件的行,解決幻讀需要鎖表。
事務(wù)隔離級(jí)別
事務(wù)隔離是數(shù)據(jù)庫(kù)處理的基礎(chǔ)之一,隔離級(jí)別在多個(gè)事務(wù)同時(shí)進(jìn)行更改和執(zhí)行查詢(xún)時(shí),對(duì)性能與結(jié)果的可靠性、一致性和可再現(xiàn)性之間的平衡進(jìn)行調(diào)整,InnoDB利用不同的鎖策略支持不同隔離級(jí)別。MySQL中有四種隔離級(jí)別,分別是讀未提交(READ UNCOMMITTED),讀已提交(READ COMMITTED),可重復(fù)讀(REPEATABLE READ)以及串行化(SERIALIZABLE)。
隔離級(jí)別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
READ UNCOMMITTED | Yes | Yes | Yes |
READ COMMITTED | No | Yes | Yes |
REPEATABLE READ | No | No | Yes |
SERIALIZABLE | No | No | No |
InnoDB并發(fā)控制
MVCC特性
InnonDB是一個(gè)支持行鎖的存儲(chǔ)引擎,為了提供更好支持的并發(fā),使用了非鎖定讀,不需要等待訪(fǎng)問(wèn)數(shù)據(jù)上的鎖釋放,而是讀取行的一個(gè)快照,該方法是通過(guò)InnonDB MVCC特性實(shí)現(xiàn)的。
MVCC是Multi-Version Concurrency Control的簡(jiǎn)稱(chēng),即多版本并發(fā)控制,作用是讓事務(wù)在并行發(fā)生時(shí),在一定隔離級(jí)別前提下,可以保證在某個(gè)事務(wù)中能實(shí)現(xiàn)一致性讀,也就是該事務(wù)啟動(dòng)時(shí)根據(jù)某個(gè)條件讀取到的數(shù)據(jù),直到事務(wù)結(jié)束時(shí),再次執(zhí)行相同條件,還是讀到同一份數(shù)據(jù),不會(huì)發(fā)生變化。
MVCC的好處
讀不加鎖,讀寫(xiě)不沖突。在讀多寫(xiě)少的OLTP應(yīng)用中,讀寫(xiě)不沖突是非常重要的,可以增加系統(tǒng)的并發(fā)性能。
在MVCC中,有兩種讀操作:快照度和當(dāng)前讀。
MVCC快照
MVCC內(nèi)部使用的一致性讀快照稱(chēng)為Read View,在不同的隔離級(jí)別下,事務(wù)啟動(dòng)時(shí)或者SQL語(yǔ)句開(kāi)始時(shí),看到的數(shù)據(jù)快照版本可能也不同,在RR、RC隔離級(jí)別下會(huì)用到 Read view。
InnoDB 里面每個(gè)事務(wù)有一個(gè)唯一的事務(wù)ID,稱(chēng)為T(mén)ransaction ID,它是在事務(wù)開(kāi)始的時(shí)候向InnoDB的事務(wù)系統(tǒng)申請(qǐng)的,是按申請(qǐng)順序嚴(yán)格遞增的。而每行數(shù)據(jù)都有多個(gè)版本。每次事務(wù)更新數(shù)據(jù)的時(shí)候,都會(huì)生成一個(gè)新的數(shù)據(jù)版本Read View,并且把Transaction ID賦值給這個(gè)數(shù)據(jù)版本的事務(wù) ID,標(biāo)記為 row_trx_id。同時(shí)舊的數(shù)據(jù)版本要保留,并且在新的數(shù)據(jù)版本中,能夠有信息可以直接拿到它,數(shù)據(jù)表中的一行記錄,其實(shí)可能有多個(gè)數(shù)據(jù)版本 ,每個(gè)版本有自己的 row_trx_id。
InnoDB行格式
目前InnoDB默認(rèn)的行格式Dynamic,是Compat格式的增強(qiáng)版,記錄頭結(jié)構(gòu)信息占用5個(gè)字節(jié),事務(wù)ID和回滾指針?lè)謩e占用6和7個(gè)字節(jié),行格式如下:
記錄頭結(jié)構(gòu)
項(xiàng)目 | 大小(bit) | 描述 |
---|---|---|
() | 1 | Unknown |
() | 1 | Unknown |
deleted_flag | 1 | 數(shù)據(jù)行刪除標(biāo)記 |
min_rec_flag | 1 | =1如果該記錄被預(yù)先被定義為最小的記錄 |
n_owned | 4 | 擁有的記錄數(shù) |
heap_no | 13 | 索引堆中該條記錄的排序位置 |
record_type | 3 | 記錄類(lèi)型;000:普通,001:B+樹(shù)葉子節(jié)點(diǎn),010:偽列Infinum,011:Supernum,1xx:保留 |
next_record | 16 | page中下一條記錄的相對(duì)位置 |
Transaction ID | 48 | 記錄中的事務(wù)ID,固定6個(gè)字節(jié) |
Rollback Pointer | 56 | 回滾指針,固定7個(gè)字節(jié) |
數(shù)據(jù)行存儲(chǔ)
- #創(chuàng)建表
- mysql> create table store_users (id int not null auto_increment primary key comment '主鍵id',name varchar(20) not null default '' comment '姓名');
- # 查看表狀態(tài)信息
- mysql> show table status like 'store_users'\G
- Row_format: Dynamic #默認(rèn)行格式為Dynamic
- Rows: 0 #行數(shù)
- Avg_row_length: 0 #平均行長(zhǎng)度
- Data_length: 16384 #初始化段大小16K
- #開(kāi)啟事務(wù),插入數(shù)據(jù)
- mysql> begin;
- mysql> insert into store_users values(null, 'aaaaa'),(null, 'bbbbb');
- #查看InnoDB分配的事務(wù)ID
- mysql> select trx_id from information_schema.innodb_trx\G
- trx_id: 8407246 #事務(wù)ID
分析表的行頭信息以及隱藏的事務(wù)ID和回滾指針。
- # 用Linux下的工具h(yuǎn)exdump進(jìn)行分析
- $ hexdump -C -v /usr/local/var/mysql/test/store_users.ibd > store_users.txt
- $ vi store_users.txt
- 00010060 02 00 1b 69 6e 66 69 6d 75 6d 00 03 00 0b 00 00 |...infimum......|
- 00010070 73 75 70 72 65 6d 75 6d 05 00 00 10 00 1c 80 00 |supremum........|
- 00010080 00 01 00 00 00 80 48 ce 83 00 00 01 d8 01 10 61 |......H........a| #Record Header信息
- 00010090 61 61 61 61 05 00 00 18 ff d6 80 00 00 02 00 00 |aaaa............|
- 000100a0 00 80 48 ce 83 00 00 01 d8 01 1d 62 62 62 62 62 |..H........bbbbb|
- 000100b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
- 10表示變長(zhǎng)字段長(zhǎng)度,只有一個(gè)varchar(20)沒(méi)有超過(guò)256字節(jié),且沒(méi)有NULL值。
- 00代表NULL標(biāo)志位,第一行沒(méi)有為NULL數(shù)據(jù)。
- 字符a的十六進(jìn)制是61,即61 61 61 61 61代表的是字段值aaaaa
- 00 00 00 80 48 ce 6個(gè)字節(jié)就是Transaction ID,轉(zhuǎn)換成十進(jìn)制8407246,正是上面information_schema.innodb_trx.trx_id列的值,trx_id: 8407246 。
- 83 00 00 01 d8 01 10 7個(gè)字節(jié)是Rollback Pointer。
- 1c 80 00 00 01 是5個(gè)字節(jié),代表Record Header信息。
隔離級(jí)別與快照
REPEATABLE READ
默認(rèn)的隔離級(jí)別,一致讀快照(Read View)是在第一次SELECT發(fā)起時(shí)建立,之后不會(huì)再發(fā)生變化。如果在同一個(gè)事務(wù)中發(fā)出多個(gè)非 鎖定SELECT語(yǔ)句,那么這些SELECT語(yǔ)句在事務(wù)提交前返回的結(jié)果是一致的。
在RR下快照Read View不是事務(wù)發(fā)起時(shí)創(chuàng)建,而是在第一個(gè)SELECT發(fā)起后創(chuàng)建。
READ COMMITTED
在READ COMMITTED讀已提交下,一致讀快照(Read View)是在每次SELECT后都會(huì)生成最新的Read View,即每次SELECT都能讀取到已COMMIT的數(shù)據(jù),就會(huì)存在不可重復(fù)讀、幻讀 現(xiàn)象。
Undo回滾段
當(dāng)開(kāi)啟事務(wù)執(zhí)行更新語(yǔ)句(insert/update/deeldte),會(huì)經(jīng)過(guò)Server層的處理生成執(zhí)行計(jì)劃,然后調(diào)用存儲(chǔ)引擎層接口去讀寫(xiě)數(shù)據(jù),用戶(hù)沒(méi)有觸發(fā)COMMIT或ROLLBACK之前,這些Uncommitted Data的數(shù)據(jù)稱(chēng)為前鏡像(Post Image),數(shù)據(jù)存儲(chǔ)在Undo Log,以便用戶(hù)回滾或者M(jìn)ySQL Server Crash的恢復(fù),同時(shí)Undo Log是循環(huán)覆蓋使用。
- #開(kāi)啟事務(wù),更新賬戶(hù)余額,不提交事務(wù)。
- mysql> start transaction;
- mysql> update account set balance = 100000 where account_no = 10001;
- Rows matched: 1 Changed: 1 Warnings: 0
上面在RR隔離級(jí)別下,開(kāi)啟一個(gè)事務(wù),做update更新操作,不提交事務(wù),通過(guò)show engine innodb status\G查看undo情況。
- Trx id counter 8407258
- Purge done for trx's n:o < 8407257 undo n:o < 0 state: running but idle
- History list length 33
- ......
- ---TRANSACTION 8407257, ACTIVE 154 sec
- 2 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
Trx id counter 8407258當(dāng)前的事務(wù)ID,undo log entries 1使用了的undo entries,ACTIVE 154 sec事務(wù)持續(xù)時(shí)間,事務(wù)commit后,會(huì)調(diào)用Purge Thread把undo中的老數(shù)據(jù)清理掉。
回滾記錄
insert:反向操作是delete,undo里記錄的是delete相關(guān)信息,存儲(chǔ)主鍵id即可。
udpate:反向操作是update,undo里記錄的是update前的相關(guān)數(shù)據(jù)。
delete:反向操作是insert,undo里記錄的是insert values(…..)相關(guān)的記錄。
從這里可以知道,更新操作占用Undo空間的大小排序如下:
delete > update > insert
所以不建議物理delete刪除數(shù)據(jù),會(huì)產(chǎn)生大量的Undo Log,Undo快被寫(xiě)滿(mǎn)就會(huì)發(fā)生切換,在次期間會(huì)有大量的IO操作,導(dǎo)致業(yè)務(wù)的DML都會(huì)變得很慢。
一致性讀
MySQL官方文檔對(duì)一致讀的描述:
讀操作基于某個(gè)時(shí)間點(diǎn)得到一份那時(shí)的數(shù)據(jù)快照,而不管同時(shí)其他事務(wù)對(duì)數(shù)據(jù)的修改。查詢(xún)過(guò)程中,若其他事務(wù)修改了數(shù)據(jù),那么就需要從 undo log中獲取舊版本的數(shù)據(jù)。這么做可以有效避免因?yàn)樾枰渔i(來(lái)阻止其他事務(wù)同時(shí)對(duì)這些數(shù)據(jù)的修改)而導(dǎo)致事務(wù)并行度下降的問(wèn)題。
在可重復(fù)讀(REPEATABLE READ,簡(jiǎn)稱(chēng)RR)隔離級(jí)別下,數(shù)據(jù)快照版本是在第一個(gè)讀請(qǐng)求發(fā)起時(shí)創(chuàng)建的。在讀已提交(READ COMMITTED,簡(jiǎn)稱(chēng)RC)隔離級(jí)別下,則是在每次讀請(qǐng)求時(shí)都會(huì)重新創(chuàng)建一份快照。
一致性讀是InnoDB在RR和RC下處理SELECT請(qǐng)求的默認(rèn)模式。由于一致性讀不會(huì)在它請(qǐng)求的表上加鎖,其他事務(wù)可以同時(shí)修改數(shù)據(jù)不受影響。
一行數(shù)據(jù)有多個(gè)版本,每個(gè)數(shù)據(jù)版本有自己的trx_id,每個(gè)事務(wù)或者查詢(xún)通過(guò)trx_id生成自己的一致性視圖。普通select語(yǔ)句是一致性讀,一致性讀會(huì)根據(jù)row trx_id和一致性視圖確定數(shù)據(jù)版本的可見(jiàn)性,圖中UR1,UR2就是undo,存儲(chǔ)在Undo Log中,每次查詢(xún)時(shí)根據(jù)當(dāng)前data page和 Undo page構(gòu)造出一致性數(shù)據(jù)頁(yè)(Consistent Read Page),通過(guò)讀取CR Page將數(shù)據(jù)返回給用戶(hù)。
總結(jié)
介紹了MySQL事務(wù),快照,MVCC以及Undo,雖然這些東西比較抽象,但是搞清楚這些東西是一件很有意義的事,能夠幫助我們更好的理解和使用MySQL,也可以把這種設(shè)計(jì)思想用在自己業(yè)務(wù)系統(tǒng)中。其中Undo在MySQL中的作用很重要,它是MVCC能夠快速創(chuàng)建快照基礎(chǔ),支撐系統(tǒng)的高并發(fā)。
好啦,以上就是本期的全部?jī)?nèi)容了,我是敖丙,你知道的越多,你不知道的越多,我們下期見(jiàn)。