InnoDB Undo log詳解

1. Undo Log的簡介

在InnoDB的設(shè)計(jì)中,Undo Log主要參與了兩件重要的事:崩潰恢復(fù)(Crash Recovery)和多版本并發(fā)控制(Multi-Version Concurrency Control, MVCC)。Undo Log記錄了數(shù)據(jù)修改前的版本绣版,Undo Log也像用戶數(shù)據(jù)一樣存儲(chǔ)于表空間中放祟,Undo Log也受redo Log提供的原子性保護(hù)叔收。本文將從Undo Log的類型尝江、Undo的存儲(chǔ)啸驯、Undo Log的生成客扎、Undo Log在崩潰恢復(fù)中的使用、Undo Log在多版本并發(fā)中的使用罚斗、Undo Log的清理等方面介紹Undo Log徙鱼。

2. Undo Log的類型

Undo Log的類型總體而言,分為兩大類:Insert Undo和Update Undo针姿。

2.1 Insert Undo

Insert Undo是事務(wù)Insert記錄時(shí)產(chǎn)生的Undo Log袱吆。Insert操作之所以要記錄Undo厌衙,是方便在用戶手動(dòng)rollback或者事務(wù)運(yùn)行中間系統(tǒng)崩潰,需要通過Crash Recovery刪除記錄時(shí)方便杆故。嚴(yán)格來說迅箩,以某種方式標(biāo)記新插入的記錄溉愁,再通過全表掃描刪除這些新插入处铛,但也被回滾的記錄也是可以的,但這樣效率太低了拐揭。

Insert Undo大類下撤蟆,只對(duì)應(yīng)一種Insert格式:TRX_UNDO_INSERT_REC。格式如下:

名稱 含義
Next record offset 在Undo Log頁面中這條記錄開始的地址
TRX_UNDO_INSERT_REC Undo Log類型
Undo no 一個(gè)事務(wù)內(nèi)的遞增編號(hào)
Table id Undo Log對(duì)應(yīng)的表在數(shù)據(jù)字典中的id
主鍵各列<len堂污,value>列表 記錄了主鍵各列的長度家肯,已經(jīng)對(duì)應(yīng)的value。例如如果主鍵只有一個(gè)int列盟猖,其值為10讨衣。那么此處是<4, 10>
Prev Record offset 在Undo Log頁面中這條記錄結(jié)束的地址

需要注意的是:當(dāng)插入一條記錄到表中時(shí),聚簇索引和二級(jí)索引都需要插入相應(yīng)的信息式镐。而Undo日志并不需要針對(duì)不同的頁面記錄兩條反镇,只需要在TRX_UNDO_INSERT_REC類型的Undo Log中記錄完整的主鍵信息即可。在回滾或者Crash Recovery時(shí)需要徹底刪除記錄時(shí)娘汞,有了TRX_UNDO_INSERT_REC中完整的主鍵信息歹茶,即可刪除聚簇索引和二級(jí)索引上的相應(yīng)記錄。從這個(gè)角度來說你弦,Undo Log是邏輯日志惊豺,與作為對(duì)比的邏輯物理日志redo Log不同。redo Log的詳細(xì)介紹見《InnoDB頁面持久化

2.2 Update Undo

除了Insert Undo外的所有Undo Log都屬于Update Undo禽作。詳細(xì)來說包含一下幾類:

2.2.1 TRX_UNDO_DEL_MARK_REC

DELETE一條記錄可以分為三個(gè)階段:

  • 第一階段:僅僅將記錄的記錄頭中的delete_flag修改為true尸昧,做標(biāo)記刪除。記錄格式相關(guān)內(nèi)容詳見《InnoDB行格式解析》旷偿。TRX_UNDO_DEL_MARK_REC類型就是對(duì)記錄delete mark刪除的Undo Log類型烹俗。
  • 第二階段:從記錄被delete mark后,到此事務(wù)提交狸捅,再到其他事務(wù)也再也沒有對(duì)這條記錄的可見性需求前衷蜓。這條delete mark的記錄服務(wù)于MVCC。
  • 第三階段:當(dāng)這條記錄不再被任何事務(wù)需要時(shí)尘喝,其將被Purge線程徹底刪除磁浇,也就是把記錄加入每個(gè)索引頁面的可重用PAGE_FREE鏈表,將此被刪除記錄作為PAGE_FREE頭部朽褪;修改相應(yīng)頁面的用戶記錄數(shù)量PAGE_N_RECS置吓;修改可回收的空間總大小PAGE_GARBAGE等信息无虚。頁面結(jié)構(gòu)相關(guān)內(nèi)容詳見《InnoDB頁面結(jié)構(gòu)解析》。上述第二階段和第三階段在本文后續(xù)章節(jié)還會(huì)有詳細(xì)介紹衍锚。

2.2.2 TRX_UNDO_UPD_EXIST_REC

InnoDB記錄的更新分三類友题,下面分別介紹每類更新產(chǎn)生的Undo Log類型:

  • 不更新主鍵,并且所有字段的長度不變化戴质,那么更新將會(huì)在原地進(jìn)行(in-place Update)度宦。此類修改會(huì)記錄TRX_UNDO_UPD_EXIST_REC類型的Undo Log。
  • 不更新主鍵告匠,但部分字段占用當(dāng)空間有所變化戈抄,不論空間是變小或者變大。此類記錄更新分為兩步:第一步:先徹底刪除舊記錄后专。對(duì)舊記錄的刪除和Purge操作一樣是徹底刪除划鸽,不是delete mark。第二步:插入更新后的新記錄戚哎,新記錄繼承被徹底刪除的舊記錄的Undo Log(roll_ptr與舊記錄相同)裸诽。對(duì)于此類更新可以理解為:為待更新記錄在相同頁面中尋找一個(gè)更合適的位置,由于舊記錄的Undo Log被繼承了型凳,該記錄的所有舊版本都能通過新插入的記錄找到丈冬,所以可以放心地刪除舊記錄。此類更新雖然在頁面上的修改比較復(fù)雜啰脚,但是仍然只會(huì)記錄TRX_UNDO_UPD_EXIST_REC類型的Undo Log殷蛇。
  • 更新主鍵。B+樹的每個(gè)頁面按照主鍵的大小邏輯排序橄浓。如果在原地更新記錄粒梦,將可能導(dǎo)致頁面亂序。因此此類更新的方式是先對(duì)舊記錄進(jìn)行delete mark荸实,然后再插入新的記錄匀们。之所以這里不能徹底刪除舊記錄是因?yàn)椋潞蟮挠涗涀鳛樾掠涗洸迦胱几瑳]有繼承舊記錄的Undo Log(roll_ptr指向Insert Undo)泄朴,而舊記錄在其他事物中可能有可見性需求,因此不能徹底刪除舊記錄露氮,只能進(jìn)行delete mark)祖灰。綜上分析,此類更新會(huì)產(chǎn)生兩條Undo Log:TRX_UNDO_DEL_MARK_REC + TRX_UNDO_INSERT_REC畔规。

2.2.3 TRX_UNDO_UPD_DEL_REC

TRX_UNDO_UPD_DEL_REC從名稱上看是對(duì)delete mark的記錄的修改局扶。但delete mark的記錄一旦提交,就只能被Purge線程徹底刪除,不能被其他事務(wù)修改三妈。因此畜埋,TRX_UNDO_UPD_DEL_REC產(chǎn)生于一種比較特殊的情況:當(dāng)記錄被delete mark之后,同一個(gè)事務(wù)再次插入一條主鍵與被剛被delete mark的記錄相同的記錄畴蒲,并且新舊記錄的各字段占用空間相同時(shí)悠鞍,即可直接復(fù)用被delete mark的記錄的空間,只需要修改被delete mark的記錄的部分列即可模燥。

在一個(gè)事務(wù)中的TRX_UNDO_DEL_MARK_REC + TRX_UNDO_UPD_DEL_REC兩個(gè)操作由于事務(wù)的原子性咖祭,要么都失敗,要么都成功涧窒,因此二者合并在一起可以理解為是一次原地進(jìn)行(in-place Update)心肪。

2.2.4 Update Undo的格式

TRX_UNDO_DEL_MARK_REC锭亏、TRX_UNDO_UPD_EXIST_REC纠吴、TRX_UNDO_UPD_DEL_REC三類Update Undo總體比較相似,都包括table id慧瘤、info bits戴已、old trx_id、old roll_ptr锅减、索引各列值等信息糖儡。但由于TRX_UNDO_DEL_MARK_REC不需要記錄更新列的舊值,它是三類中最簡單的怔匣。

下面以TRX_UNDO_UPD_EXIST_REC為例介紹Update Undo的格式:

名稱 含義
Next Record offset 在Undo Log頁面中這條記錄開始的地址
TRX_UNDO_UPD_EXIST_REC Undo Log類型
Undo no 一個(gè)事務(wù)內(nèi)的遞增編號(hào)
Table id Undo Log對(duì)應(yīng)的表在數(shù)據(jù)字典中的id
Info bits 記錄頭中delete_flag握联、min_rec_flag(B+樹非葉子結(jié)點(diǎn)中每一層最小的記錄會(huì)添加此標(biāo)識(shí))、Record_type信息
Old trx_id 舊記錄的的事務(wù)ID
Old roll_ptr 舊記錄的的回滾段指針
主鍵各列<len每瞒,value>列表 主鍵各列的長度金闽,已經(jīng)對(duì)應(yīng)的value。例如如果主鍵只有一個(gè)int列剿骨,其值為10代芜。那么此處是<4, 10>
n_Update 被更新的列的個(gè)數(shù)
更新列舊值<pos,old_len浓利,old_value>列表 更新列舊值列表
index_col_info len 表示索引各列所占的空間 + index_col_info len本身占用的空間
索引各列<pos挤庇,len,value>列表 索引列表
Prev Record offset 在Undo Log頁面中這條記錄結(jié)束的地址
  • 更新列舊值<pos贷掖,old_len嫡秕,old_value>列表與索引各列<pos,len苹威,value>列表中的pos是列在記錄中的位置昆咽,包含了row_id、trx_id、roll_ptr等隱藏列潮改。如果用戶記錄中沒有主鍵狭郑,那么row_id的pos為0,trx_id的pos為1汇在,roll_ptr的pos為2翰萨,第一個(gè)用戶列的pos為3。
  • 索引各列<pos糕殉,len亩鬼,value>列表主要是為了輔助Purge操作清理二級(jí)索引而記錄的。對(duì)于TRX_UNDO_DEL_MARK_REC而言阿蝶,由于在Purge時(shí)需要清理二級(jí)索引上的記錄雳锋。所以記錄包含在二級(jí)索引中的所有列的信息。對(duì)于TRX_UNDO_UPD_EXIST_REC羡洁、TRX_UNDO_UPD_DEL_REC而言玷过,只有更新的列包含二級(jí)索引的列時(shí)才記錄。否則的話是不會(huì)添加這個(gè)部分的筑煮。

3. Undo Log的存儲(chǔ)

3.1 Rollback segment

用戶可以使用innodb_rollback_segments配置回滾段(Rollback segment)數(shù)量辛蚊。InnoDB默認(rèn)有128個(gè)回滾段,0號(hào)回滾段用于系統(tǒng)表真仲,無論用戶如何配置袋马,都存在于系統(tǒng)表空間(ibdata)中。1-32號(hào)回滾段用于臨時(shí)表空間秸应,存儲(chǔ)于ibtmp1中虑凛。33號(hào)-127號(hào)回滾段也用于普通表,用戶可以通過innodb_undo_tablespaces設(shè)置獨(dú)立Undo表空間的數(shù)量软啼,將33號(hào)-127號(hào)回滾段均勻地分布在Undo獨(dú)立表空間中桑谍,將回滾段設(shè)置到獨(dú)立的Undo表空間中的優(yōu)勢在于在Undo表空間中的文件大到一定程度時(shí),可以將該Undo表空間截?cái)啵╰runcate)成一個(gè)小文件焰宣。而系統(tǒng)表空間存放了系統(tǒng)表等數(shù)據(jù)霉囚,其大小只能不斷的增大,卻不能截?cái)嘭盎H绻鹖nnodb_undo_tablespaces是默認(rèn)值0盈罐,那么33號(hào)-127號(hào)回滾段也將存在于系統(tǒng)表空間中揍很。

系統(tǒng)表空間的第六個(gè)頁面(page no為5)的頁面類型為FSP_TRX_SYS_PAGE_NO麻裁,記錄了InnoDB重要的事務(wù)系統(tǒng)信息躁劣,包括持久化的最大事務(wù)ID具篇,以及128個(gè)回滾段(代碼中稱為RSEG)的地址小染,double write位置等马昙。記錄于FSP_TRX_SYS_PAGE_NO的每個(gè)回滾段地址占8字節(jié)森爽,格式為:

space id 表空間ID
page no 頁面ID

space id和page no各占4字節(jié)颅悉,二者指向的相應(yīng)回滾段的管理頁面,稱為Rollback segment header奠骄。每個(gè)回滾段中有1024個(gè)Undo Slot豆同,每個(gè)Undo Slot是一個(gè)存儲(chǔ)Undo Log的頁面的鏈表,其頁面是通過《InnoDB文件結(jié)構(gòu)解析》介紹過的段(segment)管理的含鳞。由于Insert Undo Log在事務(wù)提交之后影锈,就可以直接刪除,而Update Undo Log在事務(wù)提交之后蝉绷,還需要滿足其他事務(wù)對(duì)這些舊記錄的可見性需求(MVCC)鸭廷,不能立刻刪除,因此InnoDB將Insert Undo Log和Update Undo Log區(qū)分開熔吗,記錄在不同的Undo page鏈表中辆床。

事務(wù)申請一個(gè)新的Undo Slot就需要?jiǎng)?chuàng)建一個(gè)新的段的內(nèi)存結(jié)構(gòu),為了避免頻繁創(chuàng)建和釋放段桅狠,回滾段的內(nèi)存結(jié)構(gòu)trx_rseg_t額外引入了兩個(gè)cache鏈表:Insert Undo cached鏈表和Update Undo cached鏈表讼载,分別用于回收只使用了一個(gè)Undo page,并且Undo page使用的空間小于整個(gè)頁面空間的3/4的Insert Undo Slot或者Update Undo Slot垂攘。需要注意的是:當(dāng)Undo Slot從Insert Undo cached鏈表被復(fù)用時(shí)维雇,新的事務(wù)可以把之前事務(wù)的寫入的Undo Log覆蓋掉,從頭開始寫入新事務(wù)的Undo Log晒他。而Undo Slot從Update Undo cached鏈表被復(fù)用時(shí),舊的事務(wù)的Undo Log還需要服務(wù)于MVCC逸贾,新的事務(wù)不能覆蓋之前事務(wù)寫入的Undo Log陨仅,只能從舊Undo Log之后寫入新事務(wù)的Undo Log。

如果Undo Slot不滿足復(fù)用條件铝侵,Insert Undo Slot將被直接釋放灼伤。記錄了Update Undo Log的Undo Slot會(huì)被掛在本回滾段的History鏈表中(實(shí)際作為History鏈表的Node是Undo Log header中的TRX_UNDO_HISTORY_NODE屬性,此內(nèi)容將在3.3介紹)咪鲜,供MVCC使用狐赡。這些未被釋放的Undo Slot將被Purge線程徹底刪除,關(guān)于Undo Log Purge將在本文后續(xù)介紹疟丙。

接下來颖侄,詳細(xì)了解Rollback segment header的結(jié)構(gòu):

名稱 含義
Fil_header InnoDB頁面的通用文件頭
TRX_RSEG_MAX_SIZE 本Rollback segment中管理的所有Undo Slot 鏈表持有的Undo Log頁面最大值。該屬性占4字節(jié)享郊,默認(rèn)值為4字節(jié)的最大值0xFFFFFFFF览祖,而InnoDB頁面默認(rèn)16KB,也就是說默認(rèn)每個(gè)回滾段能持有64 T的數(shù)據(jù)量炊琉。
TRX_RSEG_HISTORY_SIZE History鏈表占用的頁面數(shù)量
TRX_RSEG_HISTORY History鏈表基節(jié)點(diǎn)
TRX_RSEG_FSEG_HEADER 本回滾段對(duì)應(yīng)段地址信息展蒂,通過段地址信息可以查到段的管理信息(iNode entry)。注意此處記錄的是本回滾段的段地址,不是其中本回滾段中任何一個(gè)Undo Slot的段地址锰悼×荆【每個(gè)回滾段會(huì)使用1+1024個(gè)段】
TRX_RSEG_UNDO_SLOTS 1024個(gè)Undo Slot的集合,占4*1024字節(jié)箕般。Undo Slot如果被占用夹界,則將Undo page鏈表段第一個(gè)頁面的page no填入對(duì)應(yīng)位置,否則填入FIL_NULL隘世。
暫未使用
Fil_trailer InnoDB頁面的通用文件尾

3.2 Undo Slot

每個(gè)Undo Slot都對(duì)應(yīng)一個(gè)頁類型為FIL_PAGE_UNDO_LOG的Undo page的鏈表可柿,Undo page鏈表的頁面的申請使用段(segment)來管理。FIL_PAGE_UNDO_LOG類型的頁面的結(jié)構(gòu)如下:

名稱 含義
Fil_header Innodb頁面的通用文件頭
Undo page header Undo page的獨(dú)特結(jié)構(gòu)
其他內(nèi)容
Fil_trailer InnoDB頁面的通用文件尾

Undo page header丙者,其結(jié)構(gòu)如下:

名稱 含義
TRX_UNDO_PAGE_TYPE 頁面要存儲(chǔ)的Undo Log類型复斥,可選值為TRX_UNDO_INSERT、TRX_UNDO_UPDATE械媒,即Insert Undo Log或者Update Undo Log
TRX_UNDO_PAGE_START 表示在當(dāng)前頁面中是從什么位置開始存儲(chǔ)Undo Log的目锭,或者說表示第一條Undo日志在本頁面中的起始偏移量。之所以有這個(gè)屬性纷捞,是因?yàn)閁ndo Slot是可以復(fù)用的痢虹,如果Update Undo Slot被復(fù)用,那么新的事務(wù)的Undo Log將不是從頭開始往后寫
TRX_UNDO_PAGE_FREE 與上面的TRX_UNDO_PAGE_START對(duì)應(yīng)主儡,表示當(dāng)前頁面中存儲(chǔ)的最后一條Undo日志結(jié)束時(shí)的偏移量奖唯,或者說從這個(gè)位置開始,可以繼續(xù)寫入新的Undo Log
TRX_UNDO_PAGE_NODE 代表一個(gè)List Node結(jié)構(gòu)糜值。詳細(xì)內(nèi)容包括:Prev Node page number丰捷、Prev Node offset、Next Node page number寂汇、Next Node offset病往。通過本Node,可以將Undo page連成鏈表

Undo Slot的第一個(gè)頁面骄瓣,即Undo page鏈表的鏈表頭與其后的Undo page少有不同停巷,其在Undo page header之后,還有一個(gè)Undo Log segment header的結(jié)構(gòu)榕栏。Undo Slot的第一個(gè)頁面的結(jié)構(gòu)如下所示:

名稱 含義
Fil_header InnoDB頁面的通用文件頭
Undo page header Undo page的獨(dú)特結(jié)構(gòu)
Undo Log segment header Undo Slot第一個(gè)頁面的特有結(jié)構(gòu)
其他內(nèi)容
Fil_trailer InnoDB頁面的通用文件尾

其中Undo Log segment header的具體信息如下:

名稱 含義
TRX_UNDO_STATE 本Undo page鏈表所處的狀態(tài)
TRX_UNDO_LAST_LOG 本Undo page鏈表中最后一個(gè)Undo Log header的位置畔勤,Undo Log header的概念將在下一節(jié)介紹
TRX_UNDO_FSEG_HEADER 本Undo page鏈表對(duì)應(yīng)的Undo Slot段地址
TRX_UNDO_PAGE_LIST Undo page鏈表的基節(jié)點(diǎn)

TRX_UNDO_STATE可能的狀態(tài)包括:

  • TRX_UNDO_ACTIVE:活躍狀態(tài)。也就是一個(gè)活躍的事務(wù)正在往這個(gè)段里邊寫入U(xiǎn)ndo日志臼膏。
  • TRX_UNDO_CACHED:被緩存的狀態(tài)硼被。處在該狀態(tài)的Undo頁面鏈表等待著之后被其他事務(wù)復(fù)用。
  • TRX_UNDO_TO_FREE:對(duì)于Insert Undo鏈表來說渗磅,如果在它對(duì)應(yīng)的事務(wù)提交之后嚷硫,該鏈表不能被復(fù)用检访,本Undo Slot將被釋放,那么就會(huì)處于這種狀態(tài)仔掸。
  • TRX_UNDO_TO_PURGE:對(duì)于Update Undo鏈表來說脆贵,如果在它對(duì)應(yīng)的事務(wù)提交之后,該鏈表不能被復(fù)用起暮,那么就會(huì)處于這種狀態(tài)卖氨,服務(wù)于MVCC,等待被Purge负懦。
  • TRX_UNDO_PREPARED:包含處于PREPARE階段的事務(wù)產(chǎn)生的Undo日志筒捺,處于事務(wù)兩階段提交的過程中。

如第一部分Undo Log簡介中所述纸厉,Undo page也受redo Log提供的原子性保護(hù)系吭。考慮到普通表的Undo page修改需要寫redo Log颗品,而臨時(shí)表的Undo page修改不需要寫redo Log肯尺。因此,InnoDB中普通表和臨時(shí)表的記錄改動(dòng)時(shí)產(chǎn)生的Undo Log要分別記錄躯枢。由于Insert操作和Update操作的Undo也是分開記錄的则吟,因此一個(gè)事務(wù)可能需要最多四個(gè)Undo page鏈表,分別記錄臨時(shí)表的Insert Undo page鏈表锄蹂、臨時(shí)表的Update Undo page鏈表氓仲、普通表的Insert Undo page鏈表、普通表的Update Undo page鏈表败匹,因此可能最多要占據(jù)4個(gè)Undo Slot寨昙。

3.3 Undo Log group

InnoDB規(guī)定同一個(gè)事務(wù)向同一個(gè)Undo page中寫入的Undo Log算一個(gè)Undo Log group,在Undo Log group內(nèi)所有Undo Log緊緊相連掀亩,中間沒有任何空隙。在每個(gè)Undo Log group前欢顷,都有一個(gè)Undo Log header槽棍。Undo Log header中的信息很多,如下所示:

名稱 含義
TRX_UNDO_TRX_ID 生成本組Undo日志的事務(wù)id
TRX_UNDO_TRX_NO 事務(wù)提交序號(hào)抬驴,使用此序號(hào)來標(biāo)記事務(wù)的提交順序(先提交的此序號(hào)小炼七,后提交的此序號(hào)大),與MVCC相關(guān)
TRX_UNDO_DEL_MARKS 標(biāo)記本組Undo Log中是否包含由于Delete mark操作產(chǎn)生的Undo日志布持,包含的話除了清理Undo Log豌拙,還需到頁面中徹底刪除記錄
TRX_UNDO_LOG_START 表示本組Undo Log中第一條Undo日志的在頁面中的偏移量
TRX_UNDO_XID_EXISTS 本組Undo Log是否包含XID信息
TRX_UNDO_DICT_TRANS 標(biāo)記本組Undo Log是不是由DDL語句產(chǎn)生的
TRX_UNDO_TABLE_ID 如果TRX_UNDO_DICT_TRANS為真,那么本屬性表示DDL語句操作的表的Table Id
TRX_UNDO_NEXT_LOG 下一組的Undo Log在頁面中開始的偏移量
TRX_UNDO_PREV_LOG 上一組的Undo Log在頁面中開始的偏移量
TRX_UNDO_HISTORY_NODE List Node結(jié)構(gòu)题暖,作為History鏈表的節(jié)點(diǎn)按傅。

3.4 Undo Log存儲(chǔ)總結(jié)

下面以一個(gè)事務(wù)執(zhí)行的過程為契機(jī)捉超,總結(jié)Undo Log的儲(chǔ)存:

事務(wù)剛開啟時(shí),不會(huì)申請回滾段以及其中的Undo Slot唯绍,只有事務(wù)在運(yùn)行過程中運(yùn)行了相應(yīng)操作拼岳,才會(huì)去分配回滾段以及其中相應(yīng)的Undo Slot。

事務(wù)在執(zhí)行過程中况芒,產(chǎn)生數(shù)據(jù)插入或更新操作時(shí)惜纸,需要到系統(tǒng)表空間中page no為5的FSP_TRX_SYS_PAGE_NO頁面中申請回滾段。為了每個(gè)回滾段能均攤負(fù)載绝骚,回滾段采用round-robin(輪流循環(huán))的方式分配給并發(fā)的事務(wù)耐版。當(dāng)僅僅對(duì)普通的記錄做修改時(shí),僅僅需要給事務(wù)分配普通表的回滾段压汪,當(dāng)僅僅對(duì)臨時(shí)表做修改時(shí)粪牲,既會(huì)為事務(wù)分配臨時(shí)表的回滾段,也會(huì)給事務(wù)分配普通表的回滾段蛾魄。

事務(wù)獲得回滾段虑瀑,要分配新的Undo Slot時(shí),會(huì)首先從回滾段的內(nèi)存結(jié)構(gòu)trx_rseg_t中的Insert Undo cached鏈表和Update Undo cached鏈表中找滴须,如果有緩存的Undo Slot舌狗,那么就把這個(gè)緩存的Undo Slot分配給該事務(wù),并復(fù)用舊的Undo Slot的段來分配頁面扔水。如果沒有緩存的Undo Slot可供分配痛侍,那么就要到Rollback segment header頁面中從前往后找未被使用的Undo Slot(值為FIL_NULL)來使用,找到后需要申請Undo Slot對(duì)應(yīng)的段魔市,并從中分配出第一個(gè)Undo Log頁面主届,將page no填入Rollback segment header中對(duì)應(yīng)Slot的位置。極端情況下待德,如果找不到需要分配的Undo Slot君丁,則會(huì)給用戶報(bào)Too many active concurrent transactions的錯(cuò)誤,并回滾本事務(wù)将宪。

一個(gè)事務(wù)最多可能需要4個(gè)Undo Slot绘闷,分別記錄臨時(shí)表的Insert Undo page鏈表、臨時(shí)表的Update Undo page鏈表较坛、普通表的Insert Undo page鏈表印蔗、普通表的Update Undo page鏈表。獲取Undo Slot后丑勤,事務(wù)以組為單位寫入U(xiǎn)ndo Log华嘹,寫完一個(gè)Undo page后,再從Undo Slot的段里申請一個(gè)新頁面法竞,然后把這個(gè)頁面插入到Undo頁面鏈表中耙厚,繼續(xù)往這個(gè)新申請的Undo page中寫Undo Log强挫。

事務(wù)提交以后,如果Undo Slot只有一個(gè)page颜曾,并且頁面的使用空間不足3/4纠拔,則將Undo Slot掛在Insert Undo cached鏈表或者Update Undo cached鏈表中。Insert Undo Slot被復(fù)用后新的Insert Undo Log可以直接覆蓋舊的Insert Undo Log泛豪,Update Undo Slot被復(fù)用后舊的Update Undo Log仍然需要服務(wù)于MVCC稠诲,新的Update Undo Log不能覆蓋舊的Update Undo Log。如果Undo Slot不滿足復(fù)用條件诡曙,那么Insert Undo Slot將被直接釋放臀叙,而Update Undo Slot中的Undo Log header將按照trx_no的順序掛在History list上,服務(wù)于MVCC价卤。

4. Undo Log在事務(wù)回滾的使用(Rollback)

事務(wù)在運(yùn)行過程中劝萤,用戶可能會(huì)主動(dòng)觸發(fā)回滾。下面舉一個(gè)簡單的例子:

create table t (col1 int primary key, col2 char);
Insert into t values (1, 'a');
begin; // 事務(wù)1
Update t set col2 = 'b' where col = 1; // 更新1
Update t set col2 = 'c' where col = 1; // 更新2
rollback;

上述事務(wù)1中將先記錄更新1的Undo Log慎璧,然后記錄更新2的Undo Log床嫌。在回滾時(shí),必須逆向操作才能順利回到事務(wù)1開始的狀態(tài)胸私,即先回滾更新2厌处,再回滾更新1,否則事務(wù)狀態(tài)將出錯(cuò)岁疼。因此阔涉,事務(wù)回滾時(shí),InnoDB會(huì)從最后一條Undo Log開始逆向?qū)ndo Log apply到數(shù)據(jù)頁面中捷绒。具體而言瑰排,InnoDB先通過從Undo Segment Header中記錄TRX_UNDO_LAST_LOG找到當(dāng)前事務(wù)的最后一個(gè)Undo Log header。通過Undo Log header可以找到其所在的Undo page的header暖侨,并在其中找到TRX_UNDO_PAGE_FREE椭住。從TRX_UNDO_PAGE_FREE開始逆向apply Undo Log即可回滾事務(wù)。

5. Undo Log在多版本并發(fā)控制中的使用(MVCC)

在介紹MVCC之前字逗,先簡單介紹一下事務(wù)隔離級(jí)別函荣。在SQL標(biāo)準(zhǔn)中,有四個(gè)隔離級(jí)別:

  • READ UNCOMMITTED:未提交讀扳肛。
  • READ COMMITTED:已提交讀。
  • REPEATABLE READ:可重復(fù)讀乘碑。
  • SERIALIZABLE:可串行化挖息。

SQL標(biāo)準(zhǔn)中,針對(duì)不同的隔離級(jí)別兽肤,并發(fā)事務(wù)可能發(fā)生不同嚴(yán)重程度的問題套腹,具體情況如下:

隔離級(jí)別 臟讀 不可重復(fù)讀 幻讀
READ UNCOMMITTED Possible Possible Possible
READ COMMITTED Not Possible Possible Possible
REPEATABLE READ Not Possible Not Possible Possible
SERIALIZABLE Not Possible Not Possible Not Possible

READ UNCOMMITTED隔離級(jí)別的事務(wù)能直接讀取其他未提交的事務(wù)修改過的記錄绪抛,對(duì)并發(fā)的限制不大。SERIALIZABLE隔離級(jí)別的事務(wù)來說电禀,所有記錄的訪問必須加速幢码,可以改善的空間也不大。為了提高READ COMMITTED和REPEATABLE READ級(jí)別的讀-寫尖飞,寫-讀的并發(fā)能力症副。InnoDB提出了一致性讀的概念,即通過多版本并發(fā)控制(MVCC)達(dá)到記錄在被一個(gè)事務(wù)更新后政基,仍然能被另一個(gè)事務(wù)讀取的效果贞铣。本部分將介紹其原理。

5.1 版本鏈

在《InnoDB行格式解析》中介紹過沮明,InnoDB的行都包括以下兩個(gè)隱藏列:

  • trx_id:每次修改記錄時(shí)辕坝,都會(huì)將修改的事務(wù)ID記錄在此處。
  • roll_ptr:該行更新前的Undo Log的地址荐健,通過roll_ptr指向的Undo Log可以構(gòu)造該行修改前的狀態(tài)酱畅。

所有的roll_ptr屬性能將該記錄的所有歷史版本串連成一個(gè)鏈表,稱為版本鏈江场。

5.2 Readview

對(duì)于Read Committed和Repeatable Read來說纺酸,實(shí)現(xiàn)的關(guān)鍵在于如何判斷事務(wù)的可見性。為此扛稽,InnoDB提出了Readview的概念吁峻。Readview可以認(rèn)為是InnoDB某個(gè)時(shí)刻所有事務(wù)狀態(tài)的快照。在介紹Readview的具體內(nèi)容前在张,先介紹trx_id和trx_no的概念:

trx_id:事務(wù)id是一個(gè)不斷遞增的數(shù)字用含,當(dāng)事務(wù)對(duì)某個(gè)表進(jìn)行了增刪改時(shí),InnoDB就會(huì)為他分配一個(gè)獨(dú)一無二的事務(wù)id帮匾。服務(wù)器在內(nèi)存中會(huì)維護(hù)一個(gè)全局的事務(wù)id啄骇,每次為某個(gè)事務(wù)分配事務(wù)id后,就自增1瘟斜。當(dāng)這個(gè)變量是256的倍數(shù)時(shí)缸夹,該值就會(huì)持久化到系統(tǒng)表空間中page no為5的FSP_TRX_SYS_PAGE_NO頁面中。當(dāng)數(shù)據(jù)庫重啟時(shí)螺句,會(huì)讀取該持久化的事務(wù)id虽惭,并加上512,并繼續(xù)分配事務(wù)ID蛇尚。trx_id可以認(rèn)為是為MVCC判斷可見性服務(wù)的芽唇。

trx_no:trx_id用自增數(shù)字標(biāo)識(shí)了事務(wù)的開始順序,trx_no則是用來標(biāo)識(shí)事務(wù)提交順序的取劫。在一個(gè)事務(wù)提交時(shí)匆笤,會(huì)為這個(gè)事務(wù)生成一個(gè)名為事務(wù)no的值研侣,該值用來表示事務(wù)提交的順序,先提交的事務(wù)no值小炮捧,后提交的事務(wù)的事務(wù)no值大庶诡。事務(wù)提交時(shí),會(huì)把事務(wù)no值填入U(xiǎn)ndo Log header的trx_Undo_trx_no中咆课。trx_no可以理解為判斷Undo Log能否Purge服務(wù)的末誓。關(guān)于trx_no的使用,將在Undo的清理中介紹傀蚌。

了解了trx_id和trx_no的含義后基显,下面來了解Readview的主要內(nèi)容:

名稱 含義
m_ids 表示在生成Readview時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表
min_trx_id 表示在生成Readview時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,也就是m_ids中的最小值善炫。min_trx_id并不是源碼中的名稱撩幽,源碼中的名稱叫m_up_limit_id,此處叫min_trx_id是為了方便理解
max_trx_id 表示生成Readview時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的id值箩艺。max_trx_id并不是m_ids中的最大值窜醉,事務(wù)id是遞增分配的。比方說現(xiàn)在有id為1艺谆,2榨惰,3這三個(gè)事務(wù),之后id為3的事務(wù)提交了静汤。那么一個(gè)新的讀事務(wù)在生成Readview時(shí)琅催,m_ids就包括1和2,min_trx_id的值就是1虫给,max_trx_id的值就是4藤抡。max_trx_id并不是源碼中的名稱,源碼中的名稱叫m_low_limit_id抹估,此處叫max_trx_id是為了方便理解
creator_trx_id 表示生成該ReadView的事務(wù)的事務(wù)id
m_low_limit_no 此Readview創(chuàng)建時(shí)能看到的最大的trx_no

5.3 可見性判斷

有了Readview缠黍,在訪問某條記錄時(shí)只需要按照以下原則判斷記錄的某個(gè)版本是否可見:

  • 如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄药蜻,所以該版本可以被當(dāng)前事務(wù)訪問瓷式。
  • 如果被訪問版本的trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交语泽,所以該版本可以被當(dāng)前事務(wù)訪問贸典。
  • 如果被訪問版本的trx_id屬性值大于ReadView中的max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟踱卵,所以該版本不可以被當(dāng)前事務(wù)訪問瓤漏。
  • 如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在蔬充,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問班利;如果不在饥漫,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問罗标。

如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見的話庸队,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上面的步驟判斷可見性闯割,依此類推彻消,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見的話宙拉,那么就意味著該條記錄對(duì)該事務(wù)完全不可見宾尚,查詢結(jié)果就不包含該記錄。

Read Committed隔離級(jí)別的事務(wù)由于能實(shí)時(shí)看到已提交事務(wù)的信息谢澈,所以它每次查詢前都需要更新自己的Readview煌贴。而Repeatable read隔離級(jí)別的事務(wù)要保證事務(wù)提交前數(shù)據(jù)是可重復(fù)讀取的,所以只會(huì)在第一次執(zhí)行查詢語句時(shí)生成一個(gè)Readview锥忿,之后的查詢就不會(huì)重復(fù)生成了牛郑。

6. Undo Log在崩潰恢復(fù)中的使用(Crash Recovery)

如第一部分Undo Log簡介中所述,Undo Log也受redo Log提供的原子性保護(hù)敬鬓。除了通用的一些MLOG_2BYTES淹朋、MLOG_4BYTES類型之外,Undo本身也有自己對(duì)應(yīng)的redo Log類型:

  • MLOG_UNDO_INIT類型在Undo page初始化的時(shí)候記錄
  • MLOG_UNDO_HDR_REUSE和MLOG_UNDO_HDR_CREATE:在分配Undo Log的時(shí)候钉答,需要重用Undo Log header或需要?jiǎng)?chuàng)建新的Undo Log header的時(shí)候記錄
  • MLOG_UNDO_INSERT:在Undo Log里寫入新的Undo Record時(shí)記錄
  • MLOG_UNDO_ERASE_END:Undo Log跨Undo page時(shí)抹除最后一個(gè)不完整的Undo Record的操作

在redo Log應(yīng)用完成后础芍,初始化完成數(shù)據(jù)詞典子系統(tǒng)后,隨即開始初始化事務(wù)子系統(tǒng)希痴,而回滾段的初始化即在這一步完成者甲。初始化回滾段時(shí),會(huì)根據(jù)每個(gè)Undo Slot是否被使用砌创、Undo Slot的狀態(tài)虏缸、類型等信息來創(chuàng)建內(nèi)存結(jié)構(gòu),并將Undo Slot插入回滾段內(nèi)存結(jié)構(gòu)trx_rseg_t中的Insert_Undo_list或者Update_Undo_list上嫩实。接著根據(jù)每個(gè)回滾段的Insert_Undo_list來恢復(fù)插入操作的事務(wù)刽辙,根據(jù)Update_Undo_list來恢復(fù)更新事務(wù)。如果既存在插入又存在更新甲献,則只恢復(fù)一個(gè)事務(wù)對(duì)象宰缤。另外除了恢復(fù)事務(wù)對(duì)象外,還要恢復(fù)表鎖及讀寫事務(wù)鏈表,從而恢復(fù)到崩潰之前的事務(wù)場景慨灭。

當(dāng)從Undo恢復(fù)崩潰前活躍的事務(wù)對(duì)象后朦乏,會(huì)去開啟一個(gè)后臺(tái)線程來做事務(wù)回滾和清理操作,對(duì)于處于TRX_UNDO_ACTIVE狀態(tài)的事務(wù)直接回滾氧骤,對(duì)于既不TRX_UNDO_ACTIVE也非TRX_UNDO_PREPARED狀態(tài)的事務(wù)呻疹,包括TRX_UNDO_CACHED、TRX_UNDO_TO_FREE和TRX_UNDO_TO_PURGE筹陵,直接則認(rèn)為其是提交的刽锤,直接釋放事務(wù)對(duì)象。完成這一步后朦佩,理論上事務(wù)鏈表上只存在PREPARE狀態(tài)的事務(wù)并思。

隨后進(jìn)入XA Recover階段,MySQL使用內(nèi)部XA语稠,即通過BinLog和InnoDB做XA恢復(fù)宋彼。在初始化完成引擎后,Server層會(huì)開始掃描BinLog文件中記錄的XID颅筋。由于BinLog rotate時(shí)都會(huì)保證前一個(gè)BinLog文件中的事務(wù)已經(jīng)提交并且redo Log已經(jīng)sync到磁盤宙暇,所以只需要掃描最后一個(gè)BinLog文件即可。比較BinLog文件中XID和InnoDB層的事務(wù)XID议泵,如果如果XID已經(jīng)存在于binLog中了占贫,對(duì)應(yīng)的事務(wù)需要提交;否則需要回滾事務(wù)先口。

7. Undo的清理(Purge)

InnoDB在Undo Log中保存了多份歷史版本來實(shí)現(xiàn)MVCC型奥,當(dāng)某個(gè)歷史版本已經(jīng)確認(rèn)不會(huì)被任何現(xiàn)有的和未來的事務(wù)看到的時(shí)候,就應(yīng)該被清理掉碉京。因此就需要有辦法判斷哪些Undo Log不會(huì)再被看到厢汹。

如5.2 Readview部分所述,InnoDB每一個(gè)寫事務(wù)提交時(shí)都會(huì)被分配一個(gè)遞增的編號(hào)trx_no作為事務(wù)的提交序號(hào)谐宙,并記錄在Undo Log header中的TRX_UNDO_TRX_NO中烫葬。每個(gè)讀事務(wù)會(huì)在自己的ReadView中記錄自己開始的時(shí)候看到的最大的trx_no為m_low_limit_no。那么凡蜻,如果一個(gè)事務(wù)的trx_no小于當(dāng)前所有活躍的讀事務(wù)Readview中的這個(gè)m_low_limit_no搭综,說明這個(gè)事務(wù)在所有的讀開始之前已經(jīng)提交了,其修改的新版本是可見的划栓, 因此不再需要通過Undo構(gòu)建之前的版本兑巾,這個(gè)事務(wù)的Undo Log也就可以被清理了。

Undo Log group按提交順序掛在回滾段的History list中忠荞,History list是按照trx_no排序的蒋歌。因此當(dāng)Purge線程執(zhí)行Purge操作時(shí)帅掘,總是取最老的Readview(如果當(dāng)前沒有Readview則現(xiàn)場生成一個(gè))中的m_low_limit_no去與回滾段History list中最小的Undo Log的trx_no對(duì)比,如果Undo Log的trx_no小于 老的Readview的m_low_limit_no堂油,則清除對(duì)應(yīng)的Undo Log修档。清除Undo Log時(shí),如果發(fā)現(xiàn)Undo Log header中的TRX_UNDO_DEL_MARKS為true称诗,說明其中有待徹底刪除的記錄萍悴。還需要到記錄頁面中將記錄徹底刪除。

最老的Readview決定了哪些undo log可以Purge寓免。如果存在一個(gè)Repeatable read隔離級(jí)別的大事務(wù)久未提交,那么事務(wù)開啟時(shí)的Readview將始終保留计维,Purge操作無法進(jìn)行袜香,系統(tǒng)的Undo Log不斷增多,版本鏈不斷加長鲫惶,被標(biāo)記delete mark后等待被徹底刪除的記錄也不斷增加蜈首,系統(tǒng)的性能將受影響。

8 參考文獻(xiàn)

  1. http://mysql.taobao.org/monthly/2015/04/01/
  2. http://mysql.taobao.org/monthly/2021/10/01/
  3. https://relph1119.github.io/mysql-learning-notes/#/mysql/22-%E5%90%8E%E6%82%94%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E-undo%E6%97%A5%E5%BF%97%EF%BC%88%E4%B8%8A%EF%BC%89
  4. https://relph1119.github.io/mysql-learning-notes/#/mysql/23-%E5%90%8E%E6%82%94%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E-undo%E6%97%A5%E5%BF%97%EF%BC%88%E4%B8%8B%EF%BC%89
  5. https://relph1119.github.io/mysql-learning-notes/#/mysql/24-%E4%B8%80%E6%9D%A1%E8%AE%B0%E5%BD%95%E7%9A%84%E5%A4%9A%E5%B9%85%E9%9D%A2%E5%AD%94-%E4%BA%8B%E5%8A%A1%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E4%B8%8EMVCC
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末欠母,一起剝皮案震驚了整個(gè)濱河市欢策,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赏淌,老刑警劉巖踩寇,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異六水,居然都是意外死亡俺孙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門掷贾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來睛榄,“玉大人,你說我怎么就攤上這事想帅〕⊙ィ” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵港准,是天一觀的道長旨剥。 經(jīng)常有香客問我,道長叉趣,這世上最難降的妖魔是什么泞边? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮疗杉,結(jié)果婚禮上阵谚,老公的妹妹穿的比我還像新娘蚕礼。我一直安慰自己,他們只是感情好梢什,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布奠蹬。 她就那樣靜靜地躺著,像睡著了一般嗡午。 火紅的嫁衣襯著肌膚如雪囤躁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天荔睹,我揣著相機(jī)與錄音狸演,去河邊找鬼。 笑死僻他,一個(gè)胖子當(dāng)著我的面吹牛宵距,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吨拗,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼满哪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了劝篷?” 一聲冷哼從身側(cè)響起哨鸭,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎娇妓,沒想到半個(gè)月后像鸡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡峡蟋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年坟桅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蕊蝗。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡仅乓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蓬戚,到底是詐尸還是另有隱情夸楣,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布子漩,位于F島的核電站豫喧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏幢泼。R本人自食惡果不足惜紧显,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缕棵。 院中可真熱鬧孵班,春花似錦涉兽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至虱饿,卻和暖如春拥诡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氮发。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工渴肉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爽冕。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓宾娜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扇售。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容