還不懂mysql的undo log和mvcc?算我輸!
undo log有兩個作用:提供回滾和MVCC。
undo log是邏輯日志。
undo log存在於一個特殊的段中,存在於表空間中,和主鍵id組織的數據存在一個文件中,畢竟每行數據都有個指向undo log的指針。
當執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾。
有時候應用到行版本控制的時候,也是通過undo log來實現的:當讀取的某一行被其他事務鎖定時
它可以從undo log中分析出該行記錄以前的數據是什么,從而提供該行版本信息,讓用戶實現非鎖定一致性讀取。
undo log的存儲方式
innodb存儲引擎對undo的管理采用段的方式。rollback segment稱為回滾段,每個回滾段中有1024個undo log segment。
在以前老版本,只支持1個rollback segment,MySQL5.5可以支持128個rollback segment,即支持128*1024個undo操作,還可以通過變量 innodb_undo_logs自定義多少個rollback segment,默認值為128。
undo log默認存放在共享表空間中。
默認rollback segment全部寫在一個文件中,但可以通過設置變量 innodb_undo_tablespaces 平均分配到多少個文件中。
delete/update操作的內部機制
當事務提交的時候,innodb不會立即刪除undo log,因為后續還可能會用到undo log,如隔離級別為repeatable read時,事務讀取的都是開啟事務時的最新提交行版本,只要該事務不結束,該行版本就不能刪除,即undo log不能刪除。
但是在事務提交的時候,會將該事務對應的undo log放入到刪除列表中,未來通過purge來刪除。
並且提交事務時,還會判斷undo log分配的頁是否可以重用,如果可以重用,則會分配給后面來的事務,避免為每個獨立的事務分配獨立的undo log頁而浪費存儲空間和性能。
通過undo log記錄delete和update操作的結果發現:
delete操作實際上不會直接刪除,而是將delete對象打上delete flag,標記為刪除,最終的刪除操作是purge線程完成的。
update分為兩種情況:update的列是否是主鍵列。
- 如果不是主鍵列,在undo log中直接反向記錄是如何update的。即update是直接進行的。
- 如果是主鍵列,update分兩部執行:先刪除該行,再插入一行目標行。
insert undo log和update undo log為啥要分開,為啥提交之后insert undo log可以直接刪除了,update undo log需要等待purge?
如果某個事務ID=100新增了一條記錄,那么在這個事務版本之前這個記錄是不存在的
- 這條數據要么是事務100提交的,然后就存在這條數據了
- 事務100沒有提交,這條數據是nul
這條數據本身就是一個版本,要么是不存在,讀取不到,要么就是存在,可以讀取
數據是否存在,在RC和RR級別看事務有沒有提交。
undo log 分成兩種格式
一種給insert操作。記錄中不含回滾指針,不含舊值。insert之前是沒有的。
一種給update/delete操作。有回滾指針,有舊值。
對於INSERT_UNDO,調用函數trx_undo_page_report_insert進行插入,記錄格式大致如下圖所示:
對於UPDATE_UNDO,調用函數trx_undo_page_report_modify
進行插入,UPDATE UNDO的記錄格式大概如下圖所示:
什么是MVCC
MVCC在MySQL InnoDB中的實現主要是為了提高數據庫並發性能,用更好的方式去處理讀-寫沖突,
做到即使有讀寫沖突時,也能做到不加鎖,非阻塞並發讀
准確的說,MVCC多版本並發控制指的是 “維持一個數據的多個版本,使得讀寫操作沒有沖突” 這么一個概念。僅僅是一個理想概念
當前讀和快照讀
當前讀
像select lock in share mode(共享鎖
), select for update ; update, insert ,delete(排他鎖
)這些操作都是一種當前讀
它讀取的是記錄的最新版本,讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖
快照讀
像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;
快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當前讀;
提高並發性能,快照讀是基於多版本並發控制,即MVCC,在很多情況下,避免了加鎖操作,降低了開銷;
既然是基於多版本,即快照讀讀到的不一定是數據的最新版本,而有可能是之前的歷史版本
MVCC就是為了實現讀-寫沖突不加鎖,而這個讀指的就是快照讀
, 而非當前讀
當前讀實際上是一種加鎖的操作,是悲觀鎖的實現
當前讀,快照讀和MVCC的關系
(1)MVCC多版本並發控制指的是 “維持一個數據的多個版本,使得讀寫操作沒有沖突” 這么一個概念。僅僅是一個理想概念,
而在MySQL中,實現這么一個MVCC理想概念,我們就需要MySQL提供具體的功能去實現它
(2)快照讀就是MySQL實現MVCC的其中一個具體非阻塞讀功能。
(3)當前讀就是悲觀鎖的具體功能實現
要說的再細致一些,快照讀本身也是一個抽象概念,再深入研究。MVCC模型在MySQL中的具體實現則是由 3個隱式字段
,undo日志
,Read View
等去完成的,具體可以看下面的MVCC實現原理
MVCC能解決什么問題
數據庫並發場景有三種,分別為:
讀-讀
:不存在任何問題,也不需要並發控制讀-寫
:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀寫-寫
:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失
MVCC帶來的好處
用來解決讀-寫沖突
的無鎖並發控制,為每個修改保存一個版本,讀操作只讀事務開始前的的快照。
(1)在並發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫並發讀寫的性能
(2)還可以解決臟讀,幻讀,不可重復讀等事務隔離問題,但不能解決更新丟失問題
不滿意只讓數據庫采用悲觀鎖這樣性能不佳的形式去解決讀-寫沖突問題,而提出了MVCC,所以我們可以形成兩個組合:
MVCC + 悲觀鎖
MVCC解決讀寫沖突,悲觀鎖解決寫寫沖突
MVCC + 樂觀鎖
MVCC解決讀寫沖突,樂觀鎖解決寫寫沖突
這種組合的方式就可以最大程度的提高數據庫並發性能,並解決讀寫沖突,和寫寫沖突導致的問題
MVCC的實現原理
MVCC的實現依賴記錄中的 3個隱式字段
,undo日志
,Read View
。
取出DB_TRX_ID
(即當前事務ID),與系統當前其他活躍事務的ID去對比(由Read View維護),
不符合可見性,那就通過DB_ROLL_PTR
回滾指針去取出Undo Log
中的DB_TRX_ID
再比較,直到找到滿足特定條件的DB_TRX_ID
,
那么這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新版本
隱式字段
每行記錄除了我們自定義的字段外,還有數據庫隱式定義的DB_TRX_ID,
DB_ROLL_PTR,
DB_ROW_ID
等字段
DB_TRX_ID
6byte,記錄創建這條記錄 或者 最后一次修改該記錄的事務ID
DB_ROLL_PTR
7byte,回滾指針,用於配合undo日志,指向這條記錄的上一個版本(存儲於rollback segment里)用於配合undo日志
DB_ROW_ID
6byte,隱含的自增ID(隱藏主鍵),如果數據表沒有主鍵,InnoDB會自動以DB_ROW_ID
產生一個聚簇索引
實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了
undo日志
undo log主要分為兩種:
insert undo log
代表事務在insert
新記錄時產生的undo log
, 只在事務回滾時需要,並且在事務提交后可以被立即丟棄
update undo log
事務在進行update
或delete
時產生的undo log
; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日志時,對應的日志才會被purge
線程統一清除
purge
從前面的分析可以看出,為了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下老記錄的deleted_bit,並不真正將過時的記錄刪除。
為了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄。為了不影響MVCC的正常工作,purge線程自己也維護了一個read view(這個read view相當於系統中最老活躍事務的read view);如果某個記錄的deleted_bit為true,並且DB_TRX_ID相對於purge線程的read view可見,那么這條記錄一定是可以被安全清除的。
Read View(讀視圖)
事務進行快照讀的讀視圖(Read View),用來做可見性判斷。
(1)當執行快照讀的時候,對該記錄創建一個Read View
讀視圖,記錄當前活躍事務的ID。
(2)用來判斷當前事務能夠看到哪個版本的數據,可能是當前最新的數據,也可能是該行記錄的undo log
里面的數據。
實現
當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,最新的事務,ID值越大
trx_list:列表,記錄Read View生成時刻系統正活躍的事務ID
up_limit_id:記錄trx_list列表中事務ID最小的ID
low_limit_id:系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1
(1)首先判斷 DB_TRX_ID < up_limit_id,
如果小於,則當前事務能看到DB_TRX_ID
所在的記錄,如果大於等於進入下一個判斷
(2)然后判斷 DB_TRX_ID >= low_limit_id
,如果大於等於,則DB_TRX_ID
所在的記錄在Read View
生成后才出現的,那對當前事務肯定不可見,如果小於則進入下一個判斷
(3)判斷DB_TRX_ID
是否在活躍事務之中,trx_list.contains(DB_TRX_ID)
- 如果在,則代表
Read View
生成時刻,這個事務還在活躍,還沒有Commit,修改的數據對當前事務不可見;
- 如果不在,這個事務在
Read View
生成之前就已經Commit了,修改的結果對當前事務可見
select count(*) 總行數
在 MyISAM 存儲引擎中,把表的總行數存儲在磁盤上,直接返回總數據。
在 InnoDB 存儲引擎中,沒有將總行數存儲在磁盤上,會先把數據讀出來,一行一行的累加,最后返回總數量。
在默認隔離級別可重復讀的情況下,通過多版本並發控制(MVCC)來實現,每一行記錄都需要判斷自己是否對這個會話可見,因此在統
計總數量時,InnoDB 只好把數據一行一行的讀取出來判斷,只有當前會話可見的才納入統計中。
所以同一時刻不同會話查詢到的數量就不一樣。