1氢哮、前言
相信工作了一段時間的同學肯定都用過事務,也都聽說過事務的4大特性ACID型檀。ACID表示原子性冗尤、一致性、隔離性和持久性贱除。一個很好的事務處理系統(tǒng)生闲,必須具備這些標準特性:
原子性(Atomicity):一個事務必須被視為一個不可分割的最小工作單元媳溺,整個事務中的所有操作要么全部提交成功月幌,要么全部失敗回滾。
一致性(consistency):數(shù)據(jù)庫總是從一個一致性的狀態(tài)轉(zhuǎn)換到另一個一致性的狀態(tài)悬蔽。(其實原子性和隔離性間接的保證了一致性)
隔離性(isolation):通常來說扯躺,一個事務所做的修改在最終提交以前,對其他事務是不可見的蝎困。
持久性(durability):一旦事務提交录语,則其所做的修改就會永久保存到數(shù)據(jù)庫中。
而我們最常說的隔離性其實有對應的隔離級別禾乘,MySQL規(guī)定的隔離級別有4種澎埠,分別是:
READ UNCOMMITTED(讀未提交):在此級別里,事務的修改始藕,即使沒有提交蒲稳,對其他事務也都是可見的。事務可以讀取未提交的數(shù)據(jù)伍派,也就是會產(chǎn)生臟讀江耀,在實際應用中一般很少使用。
READ COMMITTED(讀已提交):大多數(shù)數(shù)據(jù)庫系統(tǒng)的默認隔離級別都是它诉植,但是MySQL不是祥国。它能夠避免臟讀問題,但是在一個事務里對同一條數(shù)據(jù)的多次查詢可能會得到不同的結(jié)果晾腔,也就是會產(chǎn)生不可重復讀問題舌稀。
REPEATABLE READ(可重復讀):該隔離級別是MySQL默認的隔離級別啊犬,看名字就知道它能夠防止不可重復讀問題,但是在一個事務里對一段數(shù)據(jù)的多次讀取可能會導致不同的結(jié)果壁查,也就是會有幻讀的問題(注:這里說的無法解決是MySQL定義層面椒惨,對于InnoDB引擎則完美的解決了幻讀的問題,如果你正在使用InnoDB引擎潮罪,可忽略)
SERIALIZABLE(可串行化):該隔離級別是級別最高的康谆,它通過鎖來強制事務串行執(zhí)行,避免了前面說的所有問題嫉到。在高并發(fā)下沃暗,可能導致大量的超時和鎖爭用問題。實際應用中也很少用到這個隔離級別何恶,因為RR級別解決了所有問題孽锥。
可以看到隔離級別里最重要的只有兩個隔離級別:RC和RR。那么問題來了细层,我們知道上面說的ACID以及隔離級別的實現(xiàn)原理嗎惜辑?無論是平時工作還是面試,這部分的問題都重中之重疫赎,接下來盛撑,我會拋出幾個問題,大家可以帶著問題來看此文:
ACID問題:
為什么InnoDB能夠保證原子性捧搞?用的什么方式抵卫?
為什么InnoDB能夠保證一致性?用的什么方式胎撇?
為什么InnoDB能夠保證持久性介粘?用的什么方式?
隔離性里隔離級別的問題:
為什么RU級別會發(fā)生臟讀晚树,而其他的隔離級別能夠避免姻采?
為什么RC級別不能重復讀,而RR級別能夠避免爵憎?
為什么InnoDB的RR級別能夠防止幻讀慨亲?
解決這些問題之前,我們要首先知道Redo log纲堵、Undo log以及MVCC都是什么巡雨。
2、Redo log
redo log(重做日志)用來實現(xiàn)事務的持久性席函,即事務ACID中的D铐望。其由兩部分組成,一是內(nèi)存中的重做日志緩沖(redo log buffer),其實易失的正蛙。二是重做日志文件(redo log file)督弓,其是持久的。
在一個事務中的每一次SQL操作之后都會寫入一個redo log到buffer中乒验,在最后COMMIT的時候愚隧,必須先將該事務的所有日志寫入到redo log file進行持久化(這里的寫入是順序?qū)懙模聞盏腃OMMIT操作完成才算完成锻全。
由于重做日志文件打開沒有使用O_DIRECT選項狂塘,因此重做日志緩沖先寫入文件系統(tǒng)緩存。為了確保重做日志寫入磁盤鳄厌,必須進行一次fsync操作荞胡。由于fsync的效率取決于磁盤的性能,因此磁盤的性能決定了事務提交的性能了嚎,也就是數(shù)據(jù)庫的性能泪漂。由此我們可以得出在進行批量操作的時候,不要for循環(huán)里面嵌套事務歪泳。
參數(shù) innodb_flush_log_at_trx_commit
用來控制重做日志刷新到磁盤的策略萝勤,該參數(shù)有3個值:0、1和2呐伞。
- 0:表示事務提交時不進行寫redo log file的操作敌卓,這個操作僅在master thread中完成(master thread每隔1秒進行一次fsync操作)。
- 1:默認值荸哟,表示每次事務提交時進行寫redo log file的操作假哎。
- 2:表示事務提交時將redo log寫入文件,不過僅寫入文件系統(tǒng)的緩存中鞍历,不進行fsync操作。
我們可以看到0和2的設置都比1的效率要高肪虎,但是破壞了數(shù)據(jù)庫的ACID特性劣砍,不建議使用!
對比binlog
在MySQL數(shù)據(jù)庫中還有一種二進制日志(binlog)扇救,從表面上來看它和redo log很相似刑枝,都是記錄了對數(shù)據(jù)庫操作的日志,但是迅腔,它們有著非常大的不同装畅。
首先,redo log是在MySQL的InnoDB引擎層產(chǎn)生沧烈,而binlog則是在MySQL的上層產(chǎn)生掠兄,它不僅針對InnoDB引擎,其他任何引擎對于數(shù)據(jù)庫的更改都會產(chǎn)生binlog。
其次蚂夕,兩種日志記錄的內(nèi)容形式不同迅诬,binlog是一種邏輯日志,其記錄的是對應的SQL語句婿牍。而redo log則是記錄的物理格式日志侈贷,其記錄的是對于每個頁的修改。
此外等脂,兩種日志記錄寫入磁盤的時間點不同俏蛮,binlog只在事務提交完成后一次性寫入,而redo log在上面也說了是在事務進行中不斷被寫入上遥,這表現(xiàn)為日志并不是隨事務提交的順序進行寫入的嫁蛇。
redo log block
在InnoDB引擎中,redo log都是以512字節(jié)進行存儲的(和磁盤扇區(qū)的大小一樣露该,因此redo log寫入可以保證原子性睬棚,不需要double write),也就是重做日志緩存和文件都是以塊的方式進行保存的解幼,稱為redo log block抑党,每個block占512字節(jié)。
重做日志除了日志本身之外撵摆,還由日志塊頭(log block header)及日志塊尾(log block tailer)兩部分組成底靠。
下面我來解釋一下組成Log Block header的4個部分各自的含義:
- LOG_BLOCK_HDR_NO:它主要用來標記所處Redo Log Buffer中Log Block的位置。
- LOG_BLOCK_HDR_DATA_LEN:它表示Log Block所占用的大小特铝。當Log Block被寫滿時暑中,該值為0x200,表示使用全部Log Block空間鲫剿,即占用512字節(jié)鳄逾。
- LOG_BLOCK_FIRST_REC_GROUP:表示Log Block中第一個日志所在的偏移量,如果該值大小和LOG_BLOCK_HDR_DATA_LEN相同灵莲,則表示當前Log Block不包含新的日志雕凹,如果事務的日志大小超過一個Log Block的大小,剩余的將會接著保存到一個新的Log Block中政冻。
- LOG_BLOCK_CHECKPOINT_NO:表示該Log Block最后被寫入時的檢查點第4字節(jié)的值枚抵。
Log Block tailer只包含一個LOG_BLOCK_TRL_NO,它的值和LOG_BLOCK_HDR_NO相同明场,并在函數(shù)log_block_init中被初始化汽摹。
crash recovery
前面提到了redo log是用來實現(xiàn)ACID的持久性的,也就是只要事務提交成功后苦锨,事務內(nèi)的所有修改都會保存到數(shù)據(jù)庫逼泣,哪怕這時候數(shù)據(jù)庫crash了趴泌,也要有辦法來進行恢復。也就是Crash Recovery圾旨。
說到恢復踱讨,我們先來了解一個概念:什么是LSN?
LSN(log sequence number) 用于記錄日志序號砍的,它是一個不斷遞增的 unsigned long long 類型整數(shù)痹筛,占用8字節(jié)。它代表的含義有:
- redo log寫入的總量廓鞠。
- checkpoint的位置帚稠。
- 頁的版本,用來判斷是否需要進行恢復操作床佳。
checkpoint:它是redo log中的一個檢查點滋早,這個點之前的所有數(shù)據(jù)都已經(jīng)刷新回磁盤,當DB crash后砌们,通過對checkpoint之后的redo log進行恢復就可以了杆麸。
我們可以通過命令show engine innodb status來觀察LSN的情況:
---
LOG
---
Log sequence number 33646077360
Log flushed up to 33646077360
Last checkpoint at 336460773600 pending log writes,
0 pending chkp writes
49687445 log i/o's done, 1.25 log i/o's/second
Log sequence number表示當前的LSN,Log flushed up to表示刷新到redo log文件的LSN浪感,Last checkpoint at表示刷新到磁盤的LSN昔头。如果把它們?nèi)齻€簡寫為 A、B影兽、C
的話揭斧,它們的值的大小肯定為 A>=B>=C
。
InnoDB引擎在啟動時不管上次數(shù)據(jù)庫運行時是否正常關(guān)閉峻堰,都會進行恢復操作讹开。因為重做日志記錄的是物理日志,因此恢復的速度比邏輯日志捐名,如二進制日志要快很多旦万。恢復的時候只需要找到redo log的checkpoint進行恢復即可桐筏。
3纸型、Undo log
重做日志記錄了事務的行為,可以很好的通過其對頁進行“重做”操作梅忌。但是事務有時候還需要進行回滾操作,也就是ACID中的A(原子性)除破,這時就需要Undo log了牧氮。因此在數(shù)據(jù)庫進行修改時,InnoDB存儲引擎不但會產(chǎn)生Redo瑰枫,還會產(chǎn)生一定量的Undo踱葛。這樣如果用戶執(zhí)行的事務或語句由于某種原因失敗了丹莲,又或者用戶一條ROLLBACK語句請求回滾,就可以利用這些Undo信息將數(shù)據(jù)庫回滾到修改之前的樣子尸诽。
Undo log是InnoDB MVCC事務特性的重要組成部分甥材。當我們對記錄做了變更操作時就會產(chǎn)生Undo記錄,Undo記錄默認被記錄到系統(tǒng)表空間(ibdata)中性含,但從5.6開始洲赵,也可以使用獨立的Undo 表空間。
Undo記錄中存儲的是老版本數(shù)據(jù)商蕴,當一個舊的事務需要讀取數(shù)據(jù)時叠萍,為了能讀取到老版本的數(shù)據(jù),需要順著undo鏈找到滿足其可見性的記錄绪商。當版本鏈很長時苛谷,通常可以認為這是個比較耗時的操作格郁。
基本文件結(jié)構(gòu)
為了保證事務并發(fā)操作時腹殿,在寫各自的undo log時不產(chǎn)生沖突,InnoDB采用回滾段(Rollback Segment例书,簡稱Rseg)的方式來維護undo log的并發(fā)寫入和持久化锣尉。回滾段實際上是一種 Undo 文件組織方式雾叭,每個回滾段又有多個undo log slot悟耘。具體的文件組織方式如下圖所示:
上圖展示了基本的Undo回滾段布局結(jié)構(gòu),其中:
- rseg0預留在系統(tǒng)表空間ibdata中织狐。
- rseg 1~rseg 32 這32個回滾段存放于臨時表的系統(tǒng)表空間中暂幼,用于臨時表的undo。
- rseg33~rseg 128 則根據(jù)配置(InnoDB >= 1.1默認128移迫,可通過參數(shù)
innodb_undo_logs
設置)存放到獨立undo表空間中(如果沒有打開獨立Undo表空間旺嬉,則存放于ibdata中,獨立表空間可以通過參數(shù)innodb_undo_directory
設置)厨埋,用于普通事務的undo邪媳。
如圖所示,每個回滾段維護了一個段頭頁荡陷,在該page中又劃分了1024個slot(TRX_RSEG_N_SLOTS)雨效,每個slot又對應到一個undo log對象,因此理論上InnoDB最多支持 96 * 1024個普通事務废赞。
Undo log的格式
在InnoDB引擎中徽龟,undo log分為:
- insert undo log
- update undo log
insert undo log是指在insert操作中產(chǎn)生的undo log,因為insert操作的記錄唉地,只對事務本身可見据悔,對其他事務不可見(這是事務隔離性的要求)传透,故該undo log可以在事務提交后直接刪除,不需要進行purge操作极颓。而update undo log記錄的是delete和update操作產(chǎn)生的undo log朱盐。該undo log可能需要提供MVCC機制,因此不能在事務提交時就進行刪除菠隆,提交時放入undo log鏈表兵琳,等待purge線程進行最后的刪除。下面是兩種undo log的結(jié)構(gòu)圖浸赫。
purge
對于一條delete語句 delete from t where a = 1
闰围,如果列a有聚集索引,則不會進行真正的刪除既峡,而只是在主鍵列等于1的記錄delete flag設置為1羡榴,即記錄還是存在在B+樹中。而對于update操作运敢,不是直接對記錄進行更新校仑,而是標識舊記錄為刪除狀態(tài),然后新產(chǎn)生一條記錄传惠。那這些舊版本標識位刪除的記錄何時真正的刪除迄沫?怎么刪除?
其實InnoDB是通過undo日志來進行舊版本的刪除操作的卦方,在InnoDB內(nèi)部羊瘩,這個操作被稱之為purge操作,原來在srv_master_thread主線程中完成盼砍,后來進行優(yōu)化尘吗,開辟了purge線程進行purge操作,并且可以設置purge線程的數(shù)量浇坐。purge操作每10s進行一次睬捶。
為了節(jié)省存儲空間,InnoDB存儲引擎的undo log設計是這樣的:一個頁上允許多個事務的undo log存在近刘。雖然這不代表事務在全局過程中提交的順序擒贸,但是后面的事務產(chǎn)生的undo log總在最后。此外觉渴,InnoDB存儲引擎還有一個history列表介劫,它根據(jù)事務提交的順序叁熔,將undo log進行連接悼尾,如下面的一種情況:
在執(zhí)行purge過程中植康,InnoDB存儲引擎首先從history list中找到第一個需要被清理的記錄宣旱,這里為trx1,清理之后InnoDB存儲引擎會在trx1所在的Undo page中繼續(xù)尋找是否存在可以被清理的記錄鲜戒,這里會找到事務trx3境钟,接著找到trx5植酥,但是發(fā)現(xiàn)trx5被其他事務所引用而不能清理漱挚,故再去history list中取查找翔烁,發(fā)現(xiàn)最尾端的記錄時trx2,接著找到trx2所在的Undo page旨涝,依次把trx6蹬屹、trx4清理,由于Undo page2中所有的記錄都被清理了白华,因此該Undo page可以進行重用慨默。
InnoDB存儲引擎這種先從history list中找undo log,然后再從Undo page中找undo log的設計模式是為了避免大量隨機讀操作弧腥,從而提高purge的效率厦取。
4、多版本控制MVCC
MVCC 多版本并發(fā)控制技術(shù),用于多事務環(huán)境下,對數(shù)據(jù)讀寫在不加讀寫鎖的情況下實現(xiàn)互不干擾,從而實現(xiàn)數(shù)據(jù)庫的隔離性,在事務隔離級別為Read Commit 和 Repeatable Read中使用到管搪,今天我們就用最簡單的方式虾攻,來分析下MVCC具體的原理,先解釋幾個概念更鲁。
InnoDB存儲引擎的行結(jié)構(gòu)
InnoDB表數(shù)據(jù)的組織方式為主鍵聚簇索引霎箍,二級索引中采用的是(索引鍵值, 主鍵鍵值)的組合來唯一確定一條記錄。
InnoDB表數(shù)據(jù)為主鍵聚簇索引,mysql默認為每個索引行添加了4個隱藏的字段,分別是:
- DB_ROW_ID:InnoDB引擎中一個表只能有一個主鍵,用于聚簇索引,如果表沒有定義主鍵會選擇第一個非Null的唯一索引作為主鍵,如果還沒有,生成一個隱藏的DB_ROW_ID作為主鍵構(gòu)造聚簇索引澡为。
- DB_TRX_ID:最近更改該行數(shù)據(jù)的事務ID漂坏。
- DB_ROLL_PTR:undo log的指針,用于記錄之前歷史數(shù)據(jù)在undo log中的位置。
- DELETE BIT:索引刪除標志,如果DB刪除了一條數(shù)據(jù),是優(yōu)先通知索引將該標志位設置為1,然后通過(purge)清除線程去異步刪除真實的數(shù)據(jù)媒至。
整個MVCC的機制都是通過DB_TRX_ID
,DB_ROLL_PTR
這2個隱藏字段來實現(xiàn)的顶别。
事務鏈表
當一個事務開始的時候,會將當前數(shù)據(jù)庫中正在活躍的所有事務(執(zhí)行begin,但是還沒有commit的事務)保存到一個叫trx_sys
的事務鏈表中,事務鏈表中保存的都是未提交的事務,當事務提交之后會從其中刪除。
ReadView
有了前面隱藏列和事務鏈表的基礎塘慕,接下去就可以構(gòu)造MySQL實現(xiàn)MVCC的關(guān)鍵——ReadView筋夏。
ReadView說白了就是一個數(shù)據(jù)結(jié)構(gòu),在事務開始的時候會根據(jù)上面的事務鏈表構(gòu)造一個ReadView,初始化方法如下:
// readview 初始化
// m_low_limit_id = trx_sys->max_trx_id;
// m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;
ReadView::ReadView()
: m_low_limit_id(),
m_up_limit_id(),
m_creator_trx_id(),
m_ids(),
m_low_limit_no()
{
ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list)));
}
總共做了以下幾件事:
- 活躍事務鏈表(
trx_sys
)中事務id最大的值被賦值給m_low_limit_id
图呢。 - 活躍事務鏈表中第一個值(也就是事務id最小)被賦值給
m_up_limit_id
条篷。 -
m_ids
為事務鏈表。
通過該ReadView蛤织,新的事務可以根據(jù)查詢到的所有活躍事務記錄的事務ID來匹配能夠看見該記錄赴叹,從而實現(xiàn)數(shù)據(jù)庫的事務隔離,主要邏輯如下:
- 通過聚簇索引的行結(jié)構(gòu)中DB_TRX_ID隱藏字段可以知道最近被哪個事務ID修改過指蚜。
- 一個新的事務開始時會根據(jù)事務鏈表構(gòu)造一個ReadView乞巧。
- 當前事務根據(jù)ReadView中的數(shù)據(jù)去跟檢索到的每一條數(shù)據(jù)去校驗,看看當前事務是不是能看到這條數(shù)據(jù)。
那么問題來了摊鸡,怎么來判斷可見性呢绽媒?我們來通過源碼一探究竟:
// 判斷數(shù)據(jù)對應的聚簇索引中的事務id在這個readview中是否可見
bool changes_visible(
trx_id_t id, // 記錄的id
const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
// 如果當前記錄id < 事務鏈表的最小值或者等于創(chuàng)建該readview的id就是它自己,那么是可見的
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
// 如果該記錄的事務id大于事務鏈表中的最大值,那么不可見
if (id >= m_low_limit_id) {
return(false);
// 如果事務鏈表是空的,那也是可見的
} else if (m_ids.empty()) {
return(true);
}
const ids_t::value_type* p = m_ids.data();
//判斷是否在ReadView中蚕冬,如果在說明在創(chuàng)建ReadView時 此條記錄還處于活躍狀態(tài)則不應該查詢到,否則說明創(chuàng)建ReadView是此條記錄已經(jīng)是不活躍狀態(tài)則可以查詢到
return(!std::binary_search(p, p + m_ids.size(), id));
}
總結(jié)一下可見性判斷邏輯:
- 當檢索到的數(shù)據(jù)的事務ID小于事務鏈表中的最小值(數(shù)據(jù)行的DB_TRX_ID < m_up_limit_id)表示這個數(shù)據(jù)在當前事務開啟前就已經(jīng)被其他事務修改過了,所以是可見的是辕。
- 當檢索到的數(shù)據(jù)的事務ID表示的是當前事務自己修改的數(shù)據(jù)(數(shù)據(jù)行的DB_TRX_ID = m_creator_trx_id) 時囤热,數(shù)據(jù)可見。
- 當檢索到的數(shù)據(jù)的事務ID大于事務鏈表中的最大值(數(shù)據(jù)行的DB_TRX_ID >= m_low_limit_id) 表示這個數(shù)據(jù)在當前事務開啟后到下一次查詢之間又被其他的事務修改過,那么就是不可見的获三。
- 如果事務鏈表為空,那么也是可見的,也就是當前事務開始的時候,沒有其他任意一個事務在執(zhí)行旁蔼。
- 當檢索到的數(shù)據(jù)的事務ID在事務鏈表中的最小值和最大值之間,從m_low_limit_id到m_up_limit_id進行遍歷疙教,取出DB_ROLL_PTR指針所指向的回滾段的事務ID棺聊,把它賦值給
trx_id_current
,然后從步驟1重新開始判斷贞谓,這樣總能最后找到一個可用的記錄限佩。
RC和RR隔離級別ReadView的實現(xiàn)方式
我們知道,RC隔離級別是能看到其他事務提交后的修改記錄的经宏,也就是不可重復讀犀暑,但是RR隔離級別完美的避免了,但是它們都是使用的MVCC機制烁兰,那又為何有兩種截然不同的結(jié)果呢耐亏?其實我們看一下他們創(chuàng)建ReadView的區(qū)別就知道了。
- 在RC事務隔離級別下,每次語句執(zhí)行都關(guān)閉ReadView,然后重新創(chuàng)建一份ReadView沪斟。
- 在RR下,事務開始后第一個讀操作創(chuàng)建ReadView,一直到事務結(jié)束關(guān)閉广辰。
上面的總結(jié)英文版為:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTEDisolation level, the snapshot is reset to the time of each consistent read operation.
來源自MySQL官網(wǎng):MySQL Glossary-glos_consistent_read
因為RC每次查詢語句都創(chuàng)建一個新的ReadView,所以活躍的事務列表一直在變主之,也就導致如果事務B update提交了后事務A才進行查詢择吊,查詢的結(jié)果就是最新的行,也就是不可重復讀咯槽奕。而RR則一直用的事務開始時創(chuàng)建的ReadView几睛。
5、總結(jié)
還記得開頭提到的問題嗎粤攒?現(xiàn)在應該能夠全部解決了所森。
為什么InnoDB能夠保證原子性A?用的什么方式夯接?
其實這個在上面Undo log中已經(jīng)提及了焕济。在事務里任何對數(shù)據(jù)的修改都會寫一個Undo log,然后進行數(shù)據(jù)的修改盔几,如果出現(xiàn)錯誤或者用戶需要回滾的時候可以利用Undo log的備份數(shù)據(jù)恢復到事務開始之前的狀態(tài)晴弃。
為什么InnoDB能夠保證持久性?用的什么方式?
這個在上面Redo log中已經(jīng)提及了上鞠。在一個事務中的每一次SQL操作之后都會寫入一個redo log到buffer中际邻,在最后COMMIT的時候,必須先將該事務的所有日志寫入到redo log file進行持久化(這里的寫入是順序?qū)懙模┢旃聞盏腃OMMIT操作完成才算完成枯怖。即使COMMIT后數(shù)據(jù)庫有任何的問題,在下次重啟后依然能夠通過redo log的checkpoint進行恢復能曾。也就是上面提到的crash recovery。
為什么InnoDB能夠保證一致性肿轨?用的什么方式寿冕?
在事務處理的ACID屬性中,一致性是最基本的屬性椒袍,其它的三個屬性都為了保證一致性而存在的驼唱。
首先回顧一下一致性的定義。所謂一致性驹暑,指的是數(shù)據(jù)處于一種有意義的狀態(tài)玫恳,這種狀態(tài)是語義上的而不是語法上的。最常見的例子是轉(zhuǎn)帳优俘。例如從帳戶A轉(zhuǎn)一筆錢到帳戶B上京办,如果帳戶A上的錢減少了,而帳戶B上的錢卻沒有增加帆焕,那么我們認為此時數(shù)據(jù)處于不一致的狀態(tài)惭婿。
在數(shù)據(jù)庫實現(xiàn)的場景中,一致性可以分為數(shù)據(jù)庫外部的一致性和數(shù)據(jù)庫內(nèi)部的一致性叶雹。前者由外部應用的編碼來保證财饥,即某個應用在執(zhí)行轉(zhuǎn)帳的數(shù)據(jù)庫操作時,必須在同一個事務內(nèi)部調(diào)用對帳戶A和帳戶B的操作折晦。如果在這個層次出現(xiàn)錯誤钥星,這不是數(shù)據(jù)庫本身能夠解決的,也不屬于我們需要討論的范圍满着。后者由數(shù)據(jù)庫來保證谦炒,即在同一個事務內(nèi)部的一組操作必須全部執(zhí)行成功(或者全部失敗)漓滔。這就是事務處理的原子性编饺。(上面說過了是用Undo log來保證的)
但是,原子性并不能完全保證一致性响驴。在多個事務并行進行的情況下透且,即使保證了每一個事務的原子性,仍然可能導致數(shù)據(jù)不一致的結(jié)果,比如丟失更新問題秽誊。
為了保證并發(fā)情況下的一致性鲸沮,引入了隔離性,即保證每一個事務能夠看到的數(shù)據(jù)總是一致的锅论,就好象其它并發(fā)事務并不存在一樣讼溺。用術(shù)語來說,就是多個事務并發(fā)執(zhí)行后的狀態(tài)最易,和它們串行執(zhí)行后的狀態(tài)是等價的怒坯。
為什么RU級別會發(fā)生臟讀,而其他的隔離級別能夠避免藻懒?
RU級別的操作其實就是對事務內(nèi)的每一條更新語句對應的行記錄加上讀寫鎖來操作剔猿,而不把一個事務當成一個整體來加鎖,所以會導致臟讀嬉荆。但是RC和RR能夠通過MVCC來保證記錄只有在最后COMMIT后才會讓別的事務看到归敬。
為什么RC級別不能重復讀,而RR級別能夠避免鄙早?
這個在上面的MVCC的最后說到了汪茧,在RC事務隔離級別下,每次語句執(zhí)行都關(guān)閉ReadView,然后重新創(chuàng)建一份ReadView。而在RR下,事務開始后第一個讀操作創(chuàng)建ReadView,一直到事務結(jié)束關(guān)閉限番。
為什么InnoDB的RR級別能夠防止幻讀舱污?
這個是因為RR隔離級別使用了Next-key Lock這么個東東,也就是Gap Lock+Record Lock的方式來進行間隙鎖定扳缕。
轉(zhuǎn)帖: https://blog.csdn.net/BruceLeeNumberOne/article/details/81843595