1. 程式人生 > >Innodb隔離級別的實現原理

Innodb隔離級別的實現原理

Mysql簡介

版本號3.232001Mysql的誕生,引入MyISAMInnoDB

版本號4.02003支援更多語法,如UNION和多表DELETE語法,引入查詢快取

版本號5.02006出現企業級Mysql特性:檢視,觸發器,儲存過程和儲存函式。之後Sun收購Mysql5.1版本引入分割槽和基於行的複製備份,以及可插拔的儲存引擎API

版本號5.52010Oracle收購Sun以後,將InnoDB設為預設儲存引擎,增加了其擴充套件性和效能提升

版本號5.62013InnoDB加入全文檢索


Mysql 伺服器架構

Mysql的整體架構如下圖所示,分為三層: 第一層為大部分應用都擁有的client端或者介面端,主要負責呼叫Mysql伺服器的服務。 第二層為Mysql伺服器層,其中包含了所有在呼叫儲存引擎之前所做的預備工作 第三層為儲存引擎層,該層和伺服器層是完全分離,並且由上面可以知道已經實現了儲存引擎可插拔的模式


Mysql模組架構圖

1、Client& Server 互動協議模組

任何C/S 結構的軟體系統,都肯定會有自己獨有的資訊互動協議,MySQL 也不例外。MySQL的Client & Server 互動協議模組部分,實現了客戶端與MySQL 互動過程中的所有協議。當然這些協議都是建立在現有的OS 和網路協議之上的,如TCP/IP 以及Unix Socket。

2、初始化模組

顧名思議,初始化模組就是在MySQL Server 啟動的時候,對整個系統做各種各樣的初始化操作,比如各種buffer,cache 結構的初始化和記憶體空間的申請,各種系統變數的初始化設定,各種儲存引擎的初始化設定,等等。

3、網路互動模組

底層網路互動模組抽象出底層網路互動所使用的介面api,實現底層網路資料的接收與傳送,以方便其他各個模組呼叫,以及對這一部分的維護。所有原始碼都在vio 資料夾下面。

4、連線管理、連線執行緒模組

連線管理模組負責監聽對MySQL Server 的各種請求,接收連線請求,轉發所有連線請求到執行緒管理模組。每一個連線上MySQL Server 的客戶端請求都會被分配(或建立)一個連線執行緒為其單獨服務。而連線執行緒的主要工作就是負責MySQL Server 與客戶端的通訊,接受客戶端的命令請求,傳遞Server 端的結果資訊等。執行緒管理模組則負責管理維護這些連線執行緒。包括執行緒的建立,執行緒的cache 等。

5、使用者模組

使用者模組所實現的功能,主要包括使用者的登入連線許可權控制和使用者的授權管理。他就像MySQL 的大門守衛一樣,決定是否給來訪者“開門”。

6、Query 解析和轉發模組

在MySQL 中我們習慣將所有Client端傳送給Server 端的命令都稱為query,在MySQL Server 裡面,連線執行緒接收到客戶端的一個Query 後,會直接將該query 傳遞給專門負責將各種Query 進行分類然後轉發給各個對應的處理模組,這個模組就是query 解析和轉發模組。其主要工作就是將query 語句進行語義和語法的分析,然後按照不同的操作型別進行分類,然後做出針對性的轉發。

7、QueryCache 模組

Query Cache 模組在MySQL 中是一個非常重要的模組,他的主要功能是將客戶端提交給MySQL 的Select 類query 請求的返回結果集cache 到記憶體中,與該query 的一個hash 值做一個對應。該Query 所取資料的基表發生任何資料的變化之後,MySQL 會自動使該query 的Cache 失效。在讀寫比例非常高的應用系統中,Query Cache 對效能的提高是非常顯著的。當然它對記憶體的消耗也是非常大的。

8、日誌記錄模組

日誌記錄模組主要負責整個系統級別的邏輯層的日誌的記錄,包括error log,binary log,slow query log 等。

9、Query 優化器模組

Query 優化器,顧名思義,就是優化客戶端請求的query,根據客戶端請求的query 語句,和資料庫中的一些統計資訊,在一系列演算法的基礎上進行分析,得出一個最優的策略,告訴後面的程式如何取得這個query 語句的結果。

10、表變更管理模組

表變更管理模組主要是負責完成一些DML 和DDL 的query,如:update,delte,insert,create table,alter table 等語句的處理。

11、表維護模組

表的狀態檢查,錯誤修復,以及優化和分析等工作都是表維護模組需要做的事情。

12、複製模組

複製模組又可分為Master 模組和Slave 模組兩部分, Master 模組主要負責在Replication 環境中讀取Master 端的binary 日誌,以及與Slave 端的I/O 執行緒互動等工作。

Slave 模組比Master 模組所要做的事情稍多一些,在系統中主要體現在兩個執行緒上面。一個是負責從Master請求和接受binary 日誌,並寫入本地relay log 中的I/O 執行緒。另外一個是負責從relay log 中讀取相關日誌事件,然後解析成可以在Slave 端正確執行並得到和Master端完全相同的結果的命令並再交給Slave 執行的SQL 執行緒。

13、系統狀態管理模組

系統狀態管理模組負責在客戶端請求系統狀態的時候,將各種狀態資料返回給使用者,像DBA 常用的各種showstatus 命令,showvariables 命令等,所得到的結果都是由這個模組返回的。

14、訪問控制模組

造訪客人進門了就可以想幹嘛就幹嘛麼?為了安全考慮,肯定不能如此隨意。這時候就需要訪問控制模組實時監控客人的每一個動作,給不同的客人以不同的許可權。訪問控制模組實現的功能就是根據使用者模組中各使用者的授權資訊,以及資料庫自身特有的各種約束,來控制使用者對資料的訪問。使用者模組和訪問控制模組兩者結合起來,組成了MySQL 整個資料庫系統的許可權安全管理的功能。

15、表管理器

這個模組從名字上看來很容易和上面的表變更和表維護模組相混淆,但是其功能與變更及維護模組卻完全不同。大家知道,每一個MySQL 的表都有一個表的定義檔案,也就是*.frm檔案。表管理器的工作主要就是維護這些檔案,以及一個cache,該cache 中的主要內容是各個表的結構資訊。此外它還維護table 級別的鎖管理。

16、儲存引擎介面模組

儲存引擎介面模組可以說是MySQL 資料庫中最有特色的一點了。目前各種資料庫產品中,基本上只有MySQL 可以實現其底層資料儲存引擎的外掛式管理。這個模組實際上只是一個抽象類,但正是因為它成功地將各種資料處理高度抽象化,才成就了今天MySQL 可插拔儲存引擎的特色。

17、核心API

核心API 模組主要是為了提供一些需要非常高效的底層操作功能的優化實現,包括各種底層資料結構的實現,特殊演算法的實現,字串處理,數字處理等,小檔案I/O,格式化輸出,以及最重要的記憶體管理部分。核心API 模組的所有原始碼都集中在mysys和strings資料夾下面,有興趣的讀者可以研究研究。


儲存引擎

可以稱之為一種處理資料的能力,也可以稱之為是一種表的型別。 MyISAM: 擁有較高的插入,查詢速度,但不支援事務 InnoDB :5.5版本後Mysql的預設資料庫,事務型資料庫的首選引擎,支援ACID事務,支援行級鎖定

高併發下的mysql會發生什麼問題?(以下文章僅僅討論InnoDB儲存引擎)

當一個數據庫存在高併發的情況下,如果只進行普通的查操作,那麼我相信通過mysql本身的快取機制和索引機制能夠達到很好的效能效果,但是如果還有其他讀寫操作呢?會發生什麼事情? 一個經典的例子是email,如果一個使用者在閱讀一封郵件的時候,同時另外一個使用者刪除了這一封郵件,最後會發生什麼?結果是不確定的,有可能會報錯退出,有可能會讀取到不一樣的資料。 以此為基礎,引入一個概念,多版本併發控制(Multi-version concurrency control,MVCC),這個概念不僅僅是對於Mysql,包括Oracle、PostgreSQL等其他資料庫系統都實現了MVCC,只是各自的機制不同,它可以保證不阻塞地讀到一致的資料,實現是通過儲存資料在某個時間點的快照來實現的。再引入幾個基礎概念,鎖、事務和隔離級別。

日誌

MySQL Innodb中存在多種日誌,除了錯誤日誌、查詢日誌外,還有很多和資料永續性、一致性有關的日誌。
bin.log是mysql服務層產生的日誌,常用來進行資料恢復、資料庫複製,常見的mysql主從架構,就是採用slave同步master的binlog實現的, 另外通過解析binlog能夠實現mysql到其他資料來源(如ElasticSearch)的資料複製。
redo.log記錄了資料操作在物理層面的修改,mysql中使用了大量快取,快取存在於記憶體中,修改操作時會直接修改記憶體,而不是立刻修改磁碟,當記憶體和磁碟的資料不一致時,稱記憶體中的資料為髒頁(dirty page)。為了保證資料的安全性,事務進行中時會不斷的產生redo log,在事務提交時進行一次flush操作,儲存到磁碟中, redo log是按照順序寫入的,磁碟的順序讀寫的速度遠大於隨機讀寫。當資料庫或主機失效重啟時,會根據redo log進行資料的恢復,如果redo log中有事務提交,則進行事務提交修改資料。這樣實現了事務的原子性、一致性和永續性。
undo.Log除了記錄redo log外,當進行資料修改時還會記錄undo log,undo log用於資料的撤回操作,它記錄了修改的反向操作,比如,插入對應刪除,修改對應修改為原來的資料,通過undo log可以實現事務回滾,並且可以根據undo log回溯到某個特定的版本的資料,實現MVCC。
redo log 和binlog的一致性,為了防止寫完binlog但是redo log的事務還沒提交導致的不一致,innodb 使用了兩階段提交

一種提高共享資源併發性的操作就是讓鎖更有選擇性,儘量只鎖定需要修改的資料而不是所有資料,但是加鎖也是需要消耗各種資源。鎖的各種操作,包括獲得鎖,檢查鎖是否解除,釋放鎖等,都會增加系統開銷,所以想要獲得最高效能的併發鎖策略,是在鎖的開銷和資料的安全性之間尋找一個最優點。 鎖在架構上分為兩層,一種為伺服器層面的鎖,一種為儲存引擎層面的鎖。從功能上分為讀鎖和寫鎖(共享鎖和排它鎖)。 伺服器層面的鎖是在執行Alter Table的某些操作的時候,mysql伺服器會忽略儲存引擎,直接會使用表鎖,執行對應的語句。 儲存引擎層面的鎖是為了最大程度的支援併發處理,在InnoDB,鎖分行鎖、Metadata Lock(事務級表鎖),行鎖的演算法共有三種:Record Lock,Gap Lock,Next-Key Lock Record Lock:單個行記錄的上鎖
Gap Lock:間歇鎖,不包含記錄本身的區間鎖
Next-Key Lock:包含記錄本身的區間鎖
只有在RR隔離級別下才會有gap lock,next-key lock,其中 當where條件為普通索引時為gap lock或者Next-key Lock 當where條件為主鍵索引的時候,Next-key Lock 和Gap Lock的鎖策略降級為行鎖 當where條件不是索引的時候,innodb會給所有資料上鎖,然後返回Mysql server層,然後在Server層過濾掉不符合條件的資料,通過呼叫unlock_row方法解鎖 以下為測試間歇鎖的語句:
create table t(id int,name int,key idx_id(name),primary key(id))engine =innodb;
insert into t values(1,1),(3,3),(5,5),(8,8),(11,11);   
session 1:select * from t where name=8 for update;     
session 2:insert into t(id,name) values(12,6);    
session 2:insert into t(id,name) values(6,6); 

如何在事務中進行行鎖操作? SELECT 語句中加 forupdate 或者 lockinshare mode
或者update、delete 其中還有一種檢驗死鎖的演算法叫做wait-for graph,沒當請求沒有立即反應的時候就會執行。 Metadate Lock主要解決了2個問題,一個是事務隔離問題,比如在可重複隔離級別下,會話A在2次查詢期間,會話B對錶結構做了修改,兩次查詢結果就會不一致,無法滿足可重複讀的要求;另外一個是資料複製的問題,比如會話A執行了多條更新語句期間,另外一個會話B做了表結構變更並且先提交,就會導致slave在重做時,先重做alter,再重做update時就會出現複製錯誤的現象。測試的時候可以在使用show processlist檢視alter 表的session是否存在Waiting for table metadata lock狀態。

事務

我理解的事務為使用者和Mysql伺服器完整的交流,從Start Transaction;開始後的所有sql語句直到commit;結束的一次資料交流,一般mysql中會設定自動提交事務的特點,設定方法為 show variables like 'AUTOCOMMIT'; 事務應該具有4個屬性:原子性、一致性、隔離性、永續性。這四個屬性通常稱為ACID特性。 原子性(atomicity)。一個事務是一個不可分割的工作單位,事務中包括的諸操作要麼都做,要麼都不做。
一致性(consistency)。事務必須是使資料庫從一個一致性狀態變到另一個一致性狀態。一致性與原子性是密切相關的。 隔離性(isolation)。一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。 永續性(durability)。永續性也稱永久性(permanence),指一個事務一旦提交,它對資料庫中資料的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。 其中原子性、永續性通過資料庫的redo.log(重做日誌)來實現,undo.log用來保證事務的一致性、隔離性。
重做日誌:每當有操作執行前,將資料真正更改時,先前相關操作寫入重做日誌。這樣當斷電,或者一些意外,導致後續任務無法完成時,系統恢復後,可以繼續完成這些更改
撤消日誌:當一些更改在執行一半時,發生意外,而無法完成,則可以根據撤消日誌恢復到更改之前的壯態

比如某一時刻資料庫宕機了,有兩個事務,一個事務已經提交,另一個事務正在處理
資料庫重啟的時候就要根據日誌進行前滾及回退,把已提交事務的更改寫到資料檔案,未提交事務的更改恢復到事務開始前的狀態。
關於事務,你可能不知道的地方: 儲存點(savepoint),當開始一個事務的時候,裡面會隱式的包含一個儲存點,也可以在事務過程中使用儲存點,當RollBack的時候會回到上一個儲存點的位置,每個儲存點擁有一個ID。(儲存點只會遞增,例如:從3回滾到2,再記錄下一個儲存點的時候ID為4) 事務還可以進行鏈事務(觸發器連線)、巢狀事務、分散式事務

隔離級別

未提交讀(Read Uncommitted):允許髒讀,也就是可能讀取到其他會話中未提交事務修改的資料,兩個事務互相透明。
提交讀(Read Committed):只能讀取到已經提交的資料,但是可能造成同一個事務中,由於另一個事務提前提交了一個更改事務,導致select到的資料不一致,所以為不可重讀。
可重複讀(Repeatable Read):在同一個事務內的查詢都是事務開始時刻一致的,InnoDB預設級別。在SQL標準中,該隔離級別消除了不可重複讀,但是還存在幻讀。
序列讀(Serializable):完全序列化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞
其他的都好理解,看一下什麼是幻讀: | session one                                  |  session two                                            |

| begin                                             |  begin                                                      |

| select table  where  name ='join'   |                                                                 |

| Empty set (0.00 sec)                     |                                                                 |

|                                                       |  Insert into table (name) values ('join')    |

|                                                       |  Query OK, 1 row affected                      |

| select table  where  name ='join'   |                                                                 |

Empty set (0.00 sec)                     |                                                                 |

| >update table set age=18 where  |                                                                  |

| >name='join' ;                                |                                                                  |

| Query OK, 1 row affected             | commit                                                     |

| commit                                           |                                                                  |

圖中標紅的地方,可以看到明明查不到資料,但是卻update成功了,就和幻象一樣,幻讀之名由此而來。

設定隔離級別 1.檢視當前會話隔離級別
select @@tx_isolation;
2.檢視系統當前隔離級別
select @@global.tx_isolation;
3.設定當前會話隔離級別
SET session TRANSACTION ISOLATION LEVEL repeatable read;
4.設定系統當前隔離級別
SET global TRANSACTION ISOLATION LEVEL repeatable read;

InnoDB下的MVCC強版本控制:

你可將MVCC看成行級別鎖的一種妥協,它在許多情況下避免了使用鎖,同時可以提供更小的開銷。根據實現的不同,它可以 允許非阻塞式讀,在寫操作進行時只鎖定必要的記錄。 在每一行資料中額外儲存兩個隱藏的列:當前行建立時的版本號和刪除時的版本號(可能為空)。這裡的版本號並不是實際的時間值,而是 系統版本號。每開始個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為事務的版本號,用來和查詢每行記 錄的版本號進行比較。 每個事務又有自己的版本號,這樣事務內執行CRUD操作時,就通過版本號的比較來達到資料版本控制的目的。 InnoDB每行資料只在表中保留一份,在更新資料時上行鎖,同時將舊版資料寫入 undo log;表和 undo log 中行資料都記錄著事務ID,在檢索時,只讀取來自當前已提交的

事務的行資料.

MVCC具體的操作如下:

SELECT:InnoDB會根據以下兩個條件檢查每行記錄:
1)InnoDB只查詢版本早於當前事務版本的資料行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,只麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
2)行的刪除版本要麼未定義,要麼大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。
INSERT:InnoDB為新插入的每一行儲存當前系統版本號作為行版本號。
DELETE:InnoDB為刪除的每一行儲存當前系統版本號作為行刪除標識。
UPDATE:InnoDB為插入一行新記錄,儲存當前系統版本號作為行版本號,同時儲存當系統的版本號為原來的行作為刪除標識。

儲存這兩個額外系統版本號,使大多數操作都可以不用加鎖。這樣設計使得計資料操作很簡單,效能很好,並且也能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作。
MVCC只在REPEATABLE READ和READ COMMITED兩個隔離級別下工作,其它兩個隔離級別和MVCC不相容。

可是為什麼RR級別和RC級別看到的資料不一樣呢?我們來看看innodb中MVCC的具體原理是怎麼處理的

隱藏列

在分析MVCC原理之前,先看下InnoDB中資料行的結構:

在InnoDB中,每一行都有2個隱藏列DATA_TRX_IDDATA_ROLL_PTR(如果沒有定義主鍵,則還有個隱藏主鍵列):
DATA_TRX_ID表示最近修改該行資料的事務ID
DATA_ROLL_PTR則表示指向該行回滾段的指標,該行上所有舊的版本,在undo中都通過連結串列的形式組織,而該值,正式指向undo中該行的歷史記錄連結串列
整個MVCC的關鍵就是通過DATA_TRX_ID和DATA_ROLL_PTR這兩個隱藏列來實現的。

事務連結串列

MySQL中的事務在開始到提交這段過程中,都會被儲存到一個叫trx_sys的事務連結串列中,這是一個基本的連結串列結構:

事務連結串列中儲存的都是還未提交的事務,事務一旦被提交,則會被從事務連結串列中摘除。

Read View

有了前面隱藏列和事務連結串列的基礎,接下去就可以構造MySQL實現MVCC的關鍵——ReadView。
ReadView說白了就是一個數據結構,在SQL開始的時候被建立。這個資料結構中包含了3個主要的成員:ReadView{low_trx_id, up_trx_id, trx_ids},在併發情況下,一個事務在啟動時,trx_sys連結串列中存在部分還未提交的事務,那麼哪些改變對當前事務是可見的,哪些又是不可見的,這個需要通過ReadView來進行判定,首先來看下ReadView中的3個成員各自代表的意思:
low_trx_id表示該事務啟動時,當前事務連結串列中最大的事務id編號,也就是最近建立的除自身以外最大事務編號;
up_trx_id表示該事務啟動時,當前事務連結串列中最小的事務id編號,也就是當前系統中建立最早但還未提交的事務;
trx_ids表示所有事務連結串列中事務的id集合。
上述3個成員組成了ReadView中的主要部分,簡單圖示如下:

根據上圖所示,所有資料行上DATA_TRX_ID小於up_trx_id的記錄,說明修改該行的事務在當前事務開啟之前都已經提交完成,所以對當前事務來說,都是可見的。而對於DATA_TRX_ID大於low_trx_id的記錄,說明修改該行記錄的事務在當前事務之後,所以對於當前事務來說是不可見的。
注意,ReadView是與SQL繫結的,而並不是事務,所以即使在同一個事務中,每次SQL啟動時構造的ReadView的up_trx_id和low_trx_id也都是不一樣的,至於DATA_TRX_ID大於low_trx_id本身出現也只有當多個SQL併發的時候,在一個SQL構造完ReadView之後,另外一個SQL修改了資料後又進行了提交,對於這種情況,資料其實是不可見的。
最後,至於位於(up_trx_id, low_trx_id)中間的事務是否可見,這個需要根據不同的事務隔離級別來確定。對於RC的事務隔離級別來說,對於事務執行過程中,已經提交的事務的資料,對當前事務是可見的,也就是說上述圖中,當前事務執行過程中,trx1~4中任意一個事務提交,對當前事務來說都是可見的;而對於RR隔離級別來說,事務啟動時,已經開始的事務連結串列中的事務的所有修改都是不可見的,所以在RR級別下,low_trx_id基本保持與up_trx_id相同的值即可。這裡解釋完也可以瞭解為什麼會出現幻讀。

名詞解釋

DML(Data Manipulation Language)資料操縱語言:

適用範圍:對資料庫中的資料進行一些簡單操作,如insert,delete,update,select等.

DDL(Data Definition Language)資料定義語言:

適用範圍:對資料庫中的某些物件(例如,database,table)進行管理,如Create,Alter和Drop.