-
當(dāng)前位置:首頁 > 創(chuàng)意學(xué)院 > 技術(shù) > 專題列表 > 正文
快照讀和當(dāng)前讀(快照讀和當(dāng)前讀是mvcc技術(shù)嗎)
大家好!今天讓創(chuàng)意嶺的小編來大家介紹下關(guān)于快照讀和當(dāng)前讀的問題,以下是小編對此問題的歸納整理,讓我們一起來看看吧。
開始之前先推薦一個(gè)非常厲害的Ai人工智能工具,一鍵生成原創(chuàng)文章、方案、文案、工作計(jì)劃、工作報(bào)告、論文、代碼、作文、做題和對話答疑等等
只需要輸入關(guān)鍵詞,就能返回你想要的內(nèi)容,越精準(zhǔn),寫出的就越詳細(xì),有微信小程序端、在線網(wǎng)頁版、PC客戶端
官網(wǎng):https://ai.de1919.com。
創(chuàng)意嶺作為行業(yè)內(nèi)優(yōu)秀的企業(yè),服務(wù)客戶遍布全球各地,如需了解SEO相關(guān)業(yè)務(wù)請撥打電話175-8598-2043,或添加微信:1454722008
本文目錄:
一、MySQL innodb引擎深入講解
表空間(ibd文件),一個(gè)MySQL實(shí)例可以對應(yīng)多個(gè)表空間,用于存儲(chǔ)記錄,索引等數(shù)據(jù)。
段,分為數(shù)據(jù)段、索引段、回滾段,innodb是索引組織表,數(shù)據(jù)段就是B+Tree的葉子節(jié)點(diǎn),索引段為非葉子節(jié)點(diǎn),段用來管理多個(gè)區(qū)。
區(qū),表空間的單元結(jié)構(gòu),每個(gè)區(qū)的大小為1M,默認(rèn)情況下,innodb存儲(chǔ)引擎頁大小為16K,即一個(gè)區(qū)中一共有64個(gè)連續(xù)的頁。
頁,是innodb存儲(chǔ)引擎磁盤管理的最小單元,每個(gè)頁的大小為16K,為了保證頁的連續(xù)性,innodb存儲(chǔ)引擎每次從磁盤申請4~5個(gè)區(qū)。
行,innodb存儲(chǔ)引擎數(shù)據(jù)是按行進(jìn)行存儲(chǔ)的。Trx_id 最后一次事務(wù)操作的id、roll_pointer滾動(dòng)指針。
i nnodb的內(nèi)存結(jié)構(gòu) ,由Buffer Pool、Change Buffer和Log Buffer組成。
Buffer Pool : 緩沖池是主內(nèi)存中的一個(gè)區(qū)域,里面可以緩存磁盤上經(jīng)常操作的真實(shí)數(shù)據(jù),在執(zhí)行增刪改查操作時(shí),先操作緩沖池中的數(shù)據(jù)(若緩沖池么有數(shù)據(jù),則從磁盤加載并緩存),然后再以一定頻率刷新磁盤,從而減少磁盤IO,加快處理速度。
緩沖池以page頁為單位,底層采用鏈表數(shù)據(jù)結(jié)構(gòu)管理page,根據(jù)狀態(tài),將page分為三種類型:
1、free page 即空閑page,未被使用。
2、clean page 被使用page,數(shù)據(jù)沒有被修改過。
3、dirty page 臟頁,被使用page,數(shù)據(jù)被修改過,這個(gè)page當(dāng)中的數(shù)據(jù)和磁盤當(dāng)中的數(shù)據(jù) 不一致。說得簡單點(diǎn)就是緩沖池中的數(shù)據(jù)改了,磁盤中的沒改,因?yàn)檫€沒刷寫到磁盤。
Change Buffer :更改緩沖區(qū)(針對于非唯一二級索引頁),在執(zhí)行DML語句時(shí),如果這些數(shù)據(jù)page沒有在Buffer Pool中,不會(huì)直接操作磁盤,而會(huì)將數(shù)據(jù)變更存在更改緩沖區(qū)Change Buffer中,在未來數(shù)據(jù)被讀取時(shí)。再將數(shù)據(jù)合并恢復(fù)到Buffer Pool中,再將合并后的數(shù)據(jù)刷新到磁盤中。
二級索引通常是非唯一的,并且以相對隨機(jī)的順序插入二級索引頁,同樣,刪除和更新可能會(huì)影響索引樹中不相鄰的二級索引頁。如果每一次都操作磁盤,會(huì)造成大量磁盤IO,有了Change Buffer之后,我們可以在緩沖池中進(jìn)行合并處理,減少磁盤IO。
Adaptive Hash Index: 自適應(yīng)hash索引,用于優(yōu)化對Buffer Pool數(shù)據(jù)的查詢,InnoDB存儲(chǔ)引擎會(huì)監(jiān)控對表上各索引頁的查詢,如果觀察到hash索引可以提升速度,則建立hash索引,稱之為自適應(yīng)hash索引。無需人工干預(yù),系統(tǒng)根據(jù)情況自動(dòng)完成。
參數(shù):innodb_adaptive_hash_index
Log Buffer: 日志緩沖區(qū),用來保存要寫入到磁盤中的log日志數(shù)據(jù)(redo log、undo log),默認(rèn)大小為16M,日志緩沖區(qū)的日志會(huì)定期刷新到磁盤中,如果需要更新,插入或刪除許多行的事務(wù),增加日志緩沖區(qū)的大小可以節(jié)省磁盤IO。
參數(shù): innodb_log_buffer_size 緩沖區(qū)大小
innodb_flush_log_at_trx_commit 日志刷新到磁盤時(shí)機(jī)
innodb_flush_log_at_trx_commit=1 表示日志在每次事務(wù)提交時(shí)寫入并刷新到磁盤
2 表示日志在每次事務(wù)提交后寫入,并每秒刷新到磁盤一次
0 表示每秒將日志寫入并刷新到磁盤一次。
InnoDB 的磁盤結(jié)構(gòu),由系統(tǒng)表空間(ibdata1),獨(dú)立表空間(*.ibd),通用表空間,撤銷表空間(undo tablespaces), 臨時(shí)表空間(Temporary Tablespaces), 雙寫緩沖區(qū)(Doublewrite Buffer files), 重做日志(Redo Log).
系統(tǒng)表空間(ibdata1): 系統(tǒng)表空間是更改緩沖區(qū)的存儲(chǔ)區(qū)域,如果表是在系統(tǒng)表空間而不是每個(gè)表文件或者通用表空間中創(chuàng)建的,它也可能包含表和索引數(shù)據(jù)。
參數(shù)為: innodb_data_file_path
獨(dú)立表空間(*.ibd): 每個(gè)表的文件表空間包含單個(gè)innodb表的數(shù)據(jù)和索引,并存儲(chǔ)在文件系 統(tǒng)上的單個(gè)數(shù)據(jù)文件中。 參數(shù): innodb_file_per_table
通用表空間: 需要通過create tablespace 語法創(chuàng)建,創(chuàng)建表時(shí) 可以指定該表空間。
create tablespace xxx add datafile 'file_name' engine=engine_name
create table table_name .... tablespace xxx
撤銷表空間(undo tablespaces): MySQL實(shí)例在初始化時(shí)會(huì)自動(dòng)創(chuàng)建兩個(gè)默認(rèn)的undo表空間(初始大小16K,undo_001,undo_002),用于存儲(chǔ)undo log 日志
臨時(shí)表空間(Temporary Tablespaces): innodb使用會(huì)話臨時(shí)表空和全局表空間,存儲(chǔ)用 戶創(chuàng)建的臨時(shí)表等數(shù)據(jù)。
雙寫緩沖區(qū)(Doublewrite Buffer files): innodb引擎將數(shù)據(jù)頁從Buffer Pool刷新到磁盤前,先將數(shù)據(jù)頁寫入緩沖區(qū)文件中,便于系統(tǒng)異常時(shí)恢復(fù)數(shù)據(jù)。
重做日志(Redo Log): 是用來實(shí)現(xiàn)事務(wù)的持久性,該日志文件由兩部分組成,重做日志緩沖區(qū)(redo log buffer)以及重做日志文件(redo log),前者是在內(nèi)存中,后者在磁盤中,當(dāng)事務(wù)提交之后會(huì)把修改信息都會(huì)存儲(chǔ)到該日志中,用于在刷新臟頁到磁盤時(shí),發(fā)送錯(cuò)誤時(shí),進(jìn)行數(shù)據(jù)恢復(fù)使用。以循環(huán)方式寫入重做日志文件,涉及兩個(gè)文件ib_logfile0,ib_logfile1。
那內(nèi)存結(jié)構(gòu)中的數(shù)據(jù)是如何刷新到磁盤中的? 在MySQL中有4個(gè)線程負(fù)責(zé)刷新日志到磁盤。
1、Master Thread, mysql核心后臺(tái)線程,負(fù)責(zé)調(diào)度其它線程,還負(fù)責(zé)將緩沖池中的數(shù)據(jù)異 步刷新到磁盤中,保持?jǐn)?shù)據(jù)的一致性,還包括臟頁的刷新,合并插入緩沖、undo頁的回 收。
2、IO Thread,在innodb存儲(chǔ)引擎中大量使用了AIO來處理IO請求,這樣可以極大地提高數(shù) 據(jù)庫的性能,而IO Thead主要負(fù)責(zé)這些IO請求的回調(diào)。
4個(gè)讀線程 Read thread負(fù)責(zé)讀操作
4個(gè)寫線程write thread負(fù)責(zé)寫操作
1個(gè)Log thread線程 負(fù)責(zé)將日志緩沖區(qū)刷新到磁盤
1個(gè)insert buffer線程 負(fù)責(zé)將寫入緩沖區(qū)內(nèi)容刷新到磁盤
3、Purge Thread,主要用于回收事務(wù)已經(jīng)提交了的undo log,在事務(wù)提交之后,undo log 可能不用了,就用它來回收。
4、Page Cleaner Thread, 協(xié)助Master Thread 刷新臟頁到磁盤的線程,它可以減輕主線程 的壓力,減少阻塞。
事務(wù)就是一組操作的集合,它是一個(gè)不可分割的工作單位,事務(wù)會(huì)把所有的操作作為一個(gè)整體一起向系統(tǒng)提交或撤銷操作請求,即這些操作要么同時(shí)成功,要么同時(shí)失效。
事務(wù)的4大特性分為:
如何保證事務(wù)的4大特性,原子性,一致性和持久性是由innodb存儲(chǔ)引擎底層的兩份日志來保證的,分別是redo log和undo log。對于隔離性是由鎖機(jī)制和MVCC(多版本并發(fā)控制)來實(shí)現(xiàn)的。
redo log,稱為重做日志,記錄的是事務(wù)提交時(shí)數(shù)據(jù)頁的物理修改,是用來實(shí)現(xiàn)事務(wù)的持久性。該日志文件由兩部分組成: 重做日志緩沖redo log buffer及重做日志文件redo log file,前者是在內(nèi)存中,后者是在磁盤中,當(dāng)事務(wù)提交之后會(huì)把所有修改信息都存到該日志文件中,用于在刷新臟頁到磁盤,發(fā)送錯(cuò)誤時(shí),進(jìn)行數(shù)據(jù)的恢復(fù)使用,從而保證事務(wù)的持久性。
具體的操作流程是:
1、客戶端發(fā)起事務(wù)操作,包含多條DML語句。首先去innodb中的buffer pool中的數(shù)據(jù)頁去查找有沒有我們要更新的這些數(shù)據(jù),如果沒有則通過后臺(tái)線程從磁盤中加載到buffer pool對應(yīng)的數(shù)據(jù)頁中,然后就可以在緩沖池中進(jìn)行數(shù)據(jù)操作了。
2、此時(shí)緩沖池中的數(shù)據(jù)頁發(fā)生了變更,還沒刷寫到磁盤,這個(gè)數(shù)據(jù)頁稱為臟頁。臟頁不是實(shí)時(shí)刷新到磁盤的,而是根據(jù)你配置的刷寫策略進(jìn)行刷寫到磁盤的(innodb_flush_log_at_trx_commit,0,1,2三個(gè)值)。如果臟頁在往磁盤刷新的時(shí)候出現(xiàn)了故障,會(huì)丟失數(shù)據(jù),導(dǎo)致事務(wù)的持久性得不到保證。為了避免這種現(xiàn)象,當(dāng)對緩沖池中的數(shù)據(jù)進(jìn)行增刪改操作時(shí),會(huì)把增刪改記錄到redo log buffer當(dāng)中,redo log buffer會(huì)把數(shù)據(jù)頁的物理變更持久化到磁盤文件中(ib_logfile0/ib_logfile1)。如果臟頁刷新失敗,就可以通過這兩個(gè)日志文件進(jìn)行恢復(fù)。
undo log,它是用來解決事務(wù)的原子性的,也稱為回滾日志。用于記錄數(shù)據(jù)被修改前的信息,作用包括:提供回滾和MVCC多版本并發(fā)控制。
undo log和redo log的記錄物理日志不一樣,它是邏輯日志??梢哉J(rèn)為當(dāng)delete一條記錄時(shí),undo log中會(huì)記錄一條對應(yīng)的insert記錄,當(dāng)update一條記錄時(shí),它記錄一條對應(yīng)相反的update記錄,當(dāng)執(zhí)行rollback時(shí),就可以從undo log中的邏輯記錄讀取到相應(yīng)的內(nèi)容并進(jìn)行回滾。
undo log銷毀: undo log 在事務(wù)執(zhí)行時(shí)產(chǎn)生,事務(wù)提交時(shí),并不會(huì)立即刪除undo log,因?yàn)檫@些日子可能用于MVCC。
undo log存儲(chǔ): undo log 采用段的方式進(jìn)行管理和記錄,存放在前面介紹的rollback segment回滾段中,內(nèi)部包含1024個(gè)undo log segment。
mvcc(multi-Version Concurrency Control),多版本并發(fā)控制,指維護(hù)一個(gè)數(shù)據(jù)的多個(gè)版本,使得讀寫操作沒有沖突,快照讀為MySQL實(shí)現(xiàn)MVCC提供了一個(gè)非阻塞讀功能,MVCC的具體實(shí)現(xiàn),還需要依賴于數(shù)據(jù)庫記錄中的三個(gè)隱式字段,undo log日志、readView。
read committed 每次select 都生成一個(gè)快照讀
repeatable read 開啟事務(wù)后第一個(gè)select語句才是快照讀的地方
serializable 快照讀會(huì)退化為當(dāng)前讀。
mvcc的實(shí)現(xiàn)原理
DB_TRX_ID: 最近修改事務(wù)ID,記錄插入這條記錄或最后一次修改該記錄的事務(wù)ID
DB_ROLL_PTR: 回滾指針,指向這條記錄的上一個(gè)版本,用于配合undo log,指向上一個(gè) 版本
DB_ROW_ID: 隱藏主鍵,如果表結(jié)構(gòu)沒有指定主鍵,將會(huì)生成該隱藏字段。
m_ids當(dāng)前活躍的事務(wù)ID集合
min_trx_id: 最小活躍事務(wù)id
max_trx_id: 預(yù)分配事務(wù)ID,當(dāng)前最大事務(wù)id+1,因?yàn)槭聞?wù)id是自增的
creator_trx_id: ReadView創(chuàng)建者的事務(wù)ID
版本鏈數(shù)據(jù)訪問規(guī)則:
trx_id: 表示當(dāng)前的事務(wù)ID
1、trx_id == creator_trx_id? 可以訪問讀版本-->成立的話,說明數(shù)據(jù)是當(dāng)前這個(gè)事務(wù)更改的
2、trx_id 成立,說明數(shù)據(jù)已經(jīng)提交了。
3、trx_id>max_trx_id?不可用訪問讀版本-> 成立的話,說明該事務(wù)是在ReadView生成后才開啟的。
4、min_trx_id
二、MySQL之快照讀
快照讀 即: snapshot read ,官方叫法是: Consistent Nonlocking Reads ,即: 一致性非鎖定讀 ,官方的解釋是:
即:
即 快照讀 的問題在于:在同一個(gè)事務(wù)中,能夠讀取到之前提交的數(shù)據(jù)。表現(xiàn)為:
字面意思:在事務(wù)中,為查詢創(chuàng)建的快照,并不適用與 DML 語句。
也就是說:如果事務(wù) A 開始時(shí)創(chuàng)建的快照,查詢不到數(shù)據(jù) col1=1 ,但此時(shí)事務(wù) B 剛剛提交 insert col1=1 和 insert col1=1 ,此時(shí)如果事務(wù) A 執(zhí)行, delete col1=1 ,是能將事務(wù) B 生成的數(shù)據(jù)刪除的。
字面意思:即使事務(wù) A 的快照是在事務(wù) B 提交之前創(chuàng)建的,但事務(wù) A 也只有在事務(wù) A 和事務(wù) B 都提交后,才能看到事務(wù) B 新增的數(shù)據(jù)。
三、事務(wù)/forupdate會(huì)鎖表嗎
如果條件中確定使用了索引,則會(huì)鎖該行,如沒有索引或沒使用到索引,則會(huì)鎖表。
是否使用到索引,利用trace工具判斷,這里不做敘述。
建議用主鍵做索引驗(yàn)證
先打開兩個(gè)連接session
注:session1此時(shí)未提交
session2修改當(dāng)前數(shù)據(jù)被阻塞,因?yàn)樾薷膶儆谔厥庾x這里會(huì)使用當(dāng)前讀,修改阻塞說明session1事務(wù)加了鎖。但此時(shí)不能判斷是行鎖還是表鎖。
將session1提交后,session2隨即成功提交,這里阻塞了20s左右
session2修改其他數(shù)據(jù)正常執(zhí)行,說明鎖的是行鎖,不是表鎖。
session2查詢操作正常,因?yàn)槠胀ㄗx時(shí)由于mysql的mvcc機(jī)制會(huì)使用的是快照度,所以不會(huì)阻塞。
mvcc當(dāng)前讀與快照讀及其相關(guān)原理這里不做敘述
注:session1此時(shí)未提交
session2修改當(dāng)前數(shù)據(jù)被阻塞,因?yàn)樾薷膶儆谔厥庾x這里會(huì)使用當(dāng)前讀,修改阻塞說明session1事務(wù)加了鎖。但此時(shí)不能判斷是行鎖還是表鎖。
將session1提交后,session2隨即成功提交,這里阻塞了20s左右
session2修改其他數(shù)據(jù)被阻塞,說明鎖的是表鎖,不是行鎖。
將session1提交后,session2隨即成功提交,這里阻塞了20s左右
session2查詢操作正常,因?yàn)槠胀ㄗx時(shí)由于mysql的mvcc機(jī)制會(huì)使用的是快照度,所以不會(huì)阻塞。
mvcc當(dāng)前讀與快照讀及其相關(guān)原理這里不做敘述
注:for update只有在begin commit,也就是事務(wù)之間才會(huì)起作用,如果發(fā)現(xiàn)兩個(gè)session都成功對一條數(shù)據(jù)加鎖成功,注意看下是否有沒有開啟事務(wù)。
先打開兩個(gè)連接session
注:session1此時(shí)未提交
由于session1加了鎖,session2查詢加鎖被阻塞,但此時(shí)不能判斷是行鎖還是表鎖。
將session1提交后,session2隨即成功加鎖,這里阻塞了20s左右
session2加鎖其他數(shù)據(jù)正常執(zhí)行,說明鎖的是行鎖,不是表鎖。
session2修改當(dāng)前數(shù)據(jù)被阻塞
session2修改其他數(shù)據(jù)正常執(zhí)行
注:session1此時(shí)未提交
由于session1加了鎖,session2查詢加鎖被阻塞,但此時(shí)不能判斷是行鎖還是表鎖。
將session1提交后,session2隨即加鎖成功,這里阻塞了20s左右
session2加鎖其他數(shù)據(jù)也被阻塞,說明鎖的是表鎖,不是行鎖。
將session1提交后,session2隨即加鎖成功,這里阻塞了20s左右
session2修改當(dāng)前數(shù)據(jù)被阻塞,但此時(shí)不能判斷是行鎖還是表鎖。
將session1提交后,session2隨即修改成功,這里阻塞了20s左右
session2修改其他數(shù)據(jù)同樣被阻塞,說明鎖的是表鎖,不是行鎖。
將session1提交后,session2隨即修改成功,這里阻塞了20s左右
四、事務(wù)的隔離級別 全部都是共享鎖嗎
前言: 我們都知道事務(wù)的幾種性質(zhì),數(shù)據(jù)庫為了維護(hù)這些性質(zhì),尤其是一致性和隔離性,一般使用加鎖這種方式。同時(shí)數(shù)據(jù)庫又是個(gè)高并發(fā)的應(yīng)用,同一時(shí)間會(huì)有大量的并發(fā)訪問,如果加鎖過度,會(huì)極大的降低并發(fā)處理能力。所以對于加鎖的處理,可以說就是數(shù)據(jù)庫對于事務(wù)處理的精髓所在。這里通過分析MySQL中InnoDB引擎的加鎖機(jī)制,來拋磚引玉,讓讀者更好的理解,在事務(wù)處理中數(shù)據(jù)庫到底做了什么。 一次封鎖or兩段鎖? 因?yàn)橛写罅康牟l(fā)訪問,為了預(yù)防死鎖,一般應(yīng)用中推薦使用一次封鎖法,就是在方法的開始階段,已經(jīng)預(yù)先知道會(huì)用到哪些數(shù)據(jù),然后全部鎖住,在方法運(yùn)行之后,再全部解鎖。這種方式可以有效的避免循環(huán)死鎖,但在數(shù)據(jù)庫中卻不適用,因?yàn)樵谑聞?wù)開始階段,數(shù)據(jù)庫并不知道會(huì)用到哪些數(shù)據(jù)。 數(shù)據(jù)庫遵循的是兩段鎖協(xié)議,將事務(wù)分成兩個(gè)階段,加鎖階段和解鎖階段(所以叫兩段鎖) 加鎖階段:在該階段可以進(jìn)行加鎖操作。在對任何數(shù)據(jù)進(jìn)行讀操作之前要申請并獲得S鎖(共享鎖,其它事務(wù)可以繼續(xù)加共享鎖,但不能加排它鎖),在進(jìn)行寫操作之前要申請并獲得X鎖(排它鎖,其它事務(wù)不能再獲得任何鎖)。加鎖不成功,則事務(wù)進(jìn)入等待狀態(tài),直到加鎖成功才繼續(xù)執(zhí)行。 解鎖階段:當(dāng)事務(wù)釋放了一個(gè)封鎖以后,事務(wù)進(jìn)入解鎖階段,在該階段只能進(jìn)行解鎖操作不能再進(jìn)行加鎖操作。 事務(wù) 加鎖/解鎖處理 begin; insert into test ..... 加insert對應(yīng)的鎖 update test set... 加update對應(yīng)的鎖 delete from test .... 加delete對應(yīng)的鎖 commit; 事務(wù)提交時(shí),同時(shí)釋放insert、update、delete對應(yīng)的鎖 這種方式雖然無法避免死鎖,但是兩段鎖協(xié)議可以保證事務(wù)的并發(fā)調(diào)度是串行化(串行化很重要,尤其是在數(shù)據(jù)恢復(fù)和備份的時(shí)候)的。 事務(wù)中的加鎖方式 事務(wù)的四種隔離級別 在數(shù)據(jù)庫操作中,為了有效保證并發(fā)讀取數(shù)據(jù)的正確性,提出的事務(wù)隔離級別。我們的數(shù)據(jù)庫鎖,也是為了構(gòu)建這些隔離級別存在的。 隔離級別 臟讀(Dirty Read) 不可重復(fù)讀(NonRepeatable Read) 幻讀(Phantom Read) 未提交讀(Read uncommitted) 可能 可能 可能 已提交讀(Read committed) 不可能 可能 可能 可重復(fù)讀(Repeatable read) 不可能 不可能 可能 可串行化(Serializable ) 不可能 不可能 不可能 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會(huì)話中未提交事務(wù)修改的數(shù)據(jù) 提交讀(Read Committed):只能讀取到已經(jīng)提交的數(shù)據(jù)。Oracle等多數(shù)數(shù)據(jù)庫默認(rèn)都是該級別 (不重復(fù)讀) 可重復(fù)讀(Repeated Read):可重復(fù)讀。在同一個(gè)事務(wù)內(nèi)的查詢都是事務(wù)開始時(shí)刻一致的,InnoDB默認(rèn)級別。在SQL標(biāo)準(zhǔn)中,該隔離級別消除了不可重復(fù)讀,但是還存在幻象讀 串行讀(Serializable):完全串行化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會(huì)阻塞 Read Uncommitted這種級別,數(shù)據(jù)庫一般都不會(huì)用,而且任何操作都不會(huì)加鎖,這里就不討論了。 MySQL中鎖的種類 MySQL中鎖的種類很多,有常見的表鎖和行鎖,也有新加入的Metadata Lock等等,表鎖是對一整張表加鎖,雖然可分為讀鎖和寫鎖,但畢竟是鎖住整張表,會(huì)導(dǎo)致并發(fā)能力下降,一般是做ddl處理時(shí)使用。 行鎖則是鎖住數(shù)據(jù)行,這種加鎖方法比較復(fù)雜,但是由于只鎖住有限的數(shù)據(jù),對于其它數(shù)據(jù)不加限制,所以并發(fā)能力強(qiáng),MySQL一般都是用行鎖來處理并發(fā)事務(wù)。這里主要討論的也就是行鎖。 Read Committed(讀取提交內(nèi)容) 在RC級別中,數(shù)據(jù)的讀取都是不加鎖的,但是數(shù)據(jù)的寫入、修改和刪除是需要加鎖的。效果如下 MySQL> show create table class_teacher \G\ Table: class_teacher Create Table: CREATE TABLE `class_teacher` ( `id` int(11) NOT NULL AUTO_INCREMENT, `class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `teacher_id` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_teacher_id` (`teacher_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 1 row in set (0.02 sec) MySQL> select * from class_teacher; +----+--------------+------------+ id class_name teacher_id +----+--------------+------------+ 1 初三一班 1 3 初二一班 2 4 初二二班 2 +----+--------------+------------+ 由于MySQL的InnoDB默認(rèn)是使用的RR級別,所以我們先要將該session開啟成RC級別,并且設(shè)置binlog的模式 SET session transaction isolation level read committed; SET SESSION binlog_format = 'ROW'; (或者是MIXED) 事務(wù)A 事務(wù)B begin; begin; update class_teacher set class_name='初三二班' where teacher_id=1; update class_teacher set class_name='初三三班' where teacher_id=1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction commit; 為了防止并發(fā)過程中的修改沖突,事務(wù)A中MySQL給teacher_id=1的數(shù)據(jù)行加鎖,并一直不commit(釋放鎖),那么事務(wù)B也就一直拿不到該行鎖,wait直到超時(shí)。 這時(shí)我們要注意到,teacher_id是有索引的,如果是沒有索引的class_name呢?update class_teacher set teacher_id=3 where class_name = '初三一班'; 那么MySQL會(huì)給整張表的所有數(shù)據(jù)行的加行鎖。這里聽起來有點(diǎn)不可思議,但是當(dāng)sql運(yùn)行的過程中,MySQL并不知道哪些數(shù)據(jù)行是 class_name = '初三一班'的(沒有索引嘛),如果一個(gè)條件無法通過索引快速過濾,存儲(chǔ)引擎層面就會(huì)將所有記錄加鎖后返回,再由MySQL Server層進(jìn)行過濾。 但在實(shí)際使用過程當(dāng)中,MySQL做了一些改進(jìn),在MySQL Server過濾條件,發(fā)現(xiàn)不滿足后,會(huì)調(diào)用unlock_row方法,把不滿足條件的記錄釋放鎖 (違背了二段鎖協(xié)議的約束)。這樣做,保證了最后只會(huì)持有滿足條件記錄上的鎖,但是每條記錄的加鎖操作還是不能省略的。可見即使是MySQL,為了效率也是會(huì)違反規(guī)范的。(參見《高性能MySQL》中文第三版p181) 這種情況同樣適用于MySQL的默認(rèn)隔離級別RR。所以對一個(gè)數(shù)據(jù)量很大的表做批量修改的時(shí)候,如果無法使用相應(yīng)的索引,MySQL Server過濾數(shù)據(jù)的的時(shí)候特別慢,就會(huì)出現(xiàn)雖然沒有修改某些行的數(shù)據(jù),但是它們還是被鎖住了的現(xiàn)象。 Repeatable Read(可重讀) 這是MySQL中InnoDB默認(rèn)的隔離級別。我們姑且分“讀”和“寫”兩個(gè)模塊來講解。 讀 讀就是可重讀,可重讀這個(gè)概念是一事務(wù)的多個(gè)實(shí)例在并發(fā)讀取數(shù)據(jù)時(shí),會(huì)看到同樣的數(shù)據(jù)行,有點(diǎn)抽象,我們來看一下效果。 RC(不可重讀)模式下的展現(xiàn) 事務(wù)A 事務(wù)B begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三二班 1 2 初三一班 1 update class_teacher set class_name='初三三班' where id=1; commit; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三三班 1 2 初三一班 1 讀到了事務(wù)B修改的數(shù)據(jù),和第一次查詢的結(jié)果不一樣,是不可重讀的。 commit; 事務(wù)B修改id=1的數(shù)據(jù)提交之后,事務(wù)A同樣的查詢,后一次和前一次的結(jié)果不一樣,這就是不可重讀(重新讀取產(chǎn)生的結(jié)果不一樣)。這就很可能帶來一些問題,那么我們來看看在RR級別中MySQL的表現(xiàn): 事務(wù)A 事務(wù)B 事務(wù)C begin; begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三二班 1 2 初三一班 1 update class_teacher set class_name='初三三班' where id=1; commit; insert into class_teacher values (null,'初三三班',1); commit; select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id 1 初三二班 1 2 初三一班 1 沒有讀到事務(wù)B修改的數(shù)據(jù),和第一次sql讀取的一樣,是可重復(fù)讀的。 沒有讀到事務(wù)C新添加的數(shù)據(jù)。 commit; 我們注意到,當(dāng)teacher_id=1時(shí),事務(wù)A先做了一次讀取,事務(wù)B中間修改了id=1的數(shù)據(jù),并commit之后,事務(wù)A第二次讀到的數(shù)據(jù)和第一次完全相同。所以說它是可重讀的。那么MySQL是怎么做到的呢?這里姑且賣個(gè)關(guān)子,我們往下看。 不可重復(fù)讀和幻讀的區(qū)別 很多人容易搞混不可重復(fù)讀和幻讀,確實(shí)這兩者有些相似。但不可重復(fù)讀重點(diǎn)在于update和delete,而幻讀的重點(diǎn)在于insert。 如果使用鎖機(jī)制來實(shí)現(xiàn)這兩種隔離級別,在可重復(fù)讀中,該sql第一次讀取到數(shù)據(jù)后,就將這些數(shù)據(jù)加鎖,其它事務(wù)無法修改這些數(shù)據(jù),就可以實(shí)現(xiàn)可重復(fù)讀了。但這種方法卻無法鎖住insert的數(shù)據(jù),所以當(dāng)事務(wù)A先前讀取了數(shù)據(jù),或者修改了全部數(shù)據(jù),事務(wù)B還是可以insert數(shù)據(jù)提交,這時(shí)事務(wù)A就會(huì)發(fā)現(xiàn)莫名其妙多了一條之前沒有的數(shù)據(jù),這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這么做可以有效的避免幻讀、不可重復(fù)讀、臟讀等問題,但會(huì)極大的降低數(shù)據(jù)庫的并發(fā)能力。 所以說不可重復(fù)讀和幻讀最大的區(qū)別,就在于如何通過鎖機(jī)制來解決他們產(chǎn)生的問題。 上文說的,是使用悲觀鎖機(jī)制來處理這兩種問題,但是MySQL、ORACLE、PostgreSQL等成熟的數(shù)據(jù)庫,出于性能考慮,都是使用了以樂觀鎖為理論基礎(chǔ)的MVCC(多版本并發(fā)控制)來避免這兩種問題。 悲觀鎖和樂觀鎖 悲觀鎖 正如其名,它指的是對數(shù)據(jù)被外界(包括本系統(tǒng)當(dāng)前的其他事務(wù),以及來自外部系統(tǒng)的事務(wù)處理)修改持保守態(tài)度,因此,在整個(gè)數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制(也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在本系統(tǒng)中實(shí)現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會(huì)修改數(shù)據(jù))。 在悲觀鎖的情況下,為了保證事務(wù)的隔離性,就需要一致性鎖定讀。讀取數(shù)據(jù)時(shí)給加鎖,其它事務(wù)無法修改這些數(shù)據(jù)。修改刪除數(shù)據(jù)時(shí)也要加鎖,其它事務(wù)無法讀取這些數(shù)據(jù)。 樂觀鎖 相對悲觀鎖而言,樂觀鎖機(jī)制采取了更加寬松的加鎖機(jī)制。悲觀鎖大多數(shù)情況下依靠數(shù)據(jù)庫的鎖機(jī)制實(shí)現(xiàn),以保證操作最大程度的獨(dú)占性。但隨之而來的就是數(shù)據(jù)庫性能的大量開銷,特別是對長事務(wù)而言,這樣的開銷往往無法承受。 而樂觀鎖機(jī)制在一定程度上解決了這個(gè)問題。樂觀鎖,大多是基于數(shù)據(jù)版本( Version )記錄機(jī)制實(shí)現(xiàn)。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個(gè)版本標(biāo)識(shí),在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表增加一個(gè) “version” 字段來實(shí)現(xiàn)。讀取出數(shù)據(jù)時(shí),將此版本號一同讀出,之后更新時(shí),對此版本號加一。此時(shí),將提交數(shù)據(jù)的版本數(shù)據(jù)與數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本信息進(jìn)行比對,如果提交的數(shù)據(jù)版本號大于數(shù)據(jù)庫表當(dāng)前版本號,則予以更新,否則認(rèn)為是過期數(shù)據(jù)。 要說明的是,MVCC的實(shí)現(xiàn)沒有固定的規(guī)范,每個(gè)數(shù)據(jù)庫都會(huì)有不同的實(shí)現(xiàn)方式,這里討論的是InnoDB的MVCC。 MVCC在MySQL的InnoDB中的實(shí)現(xiàn) 在InnoDB中,會(huì)在每行數(shù)據(jù)后添加兩個(gè)額外的隱藏的值來實(shí)現(xiàn)MVCC,這兩個(gè)值一個(gè)記錄這行數(shù)據(jù)何時(shí)被創(chuàng)建,另外一個(gè)記錄這行數(shù)據(jù)何時(shí)過期(或者被刪除)。 在實(shí)際操作中,存儲(chǔ)的并不是時(shí)間,而是事務(wù)的版本號,每開啟一個(gè)新事務(wù),事務(wù)的版本號就會(huì)遞增。 在可重讀Repeatable reads事務(wù)隔離級別下: SELECT時(shí),讀取創(chuàng)建版本號<=當(dāng)前事務(wù)版本號,刪除版本號為空或>當(dāng)前事務(wù)版本號。 INSERT時(shí),保存當(dāng)前事務(wù)版本號為行的創(chuàng)建版本號 DELETE時(shí),保存當(dāng)前事務(wù)版本號為行的刪除版本號 UPDATE時(shí),插入一條新紀(jì)錄,保存當(dāng)前事務(wù)版本號為行創(chuàng)建版本號,同時(shí)保存當(dāng)前事務(wù)版本號到原來刪除的行 通過MVCC,雖然每行記錄都需要額外的存儲(chǔ)空間,更多的行檢查工作以及一些額外的維護(hù)工作,但可以減少鎖的使用,大多數(shù)讀操作都不用加鎖,讀數(shù)據(jù)操作很簡單,性能很好,并且也能保證只會(huì)讀取到符合標(biāo)準(zhǔn)的行,也只鎖住必要行。 我們不管從數(shù)據(jù)庫方面的教課書中學(xué)到,還是從網(wǎng)絡(luò)上看到,大都是上文中事務(wù)的四種隔離級別這一模塊列出的意思,RR級別是可重復(fù)讀的,但無法解決幻讀,而只有在Serializable級別才能解決幻讀。于是我就加了一個(gè)事務(wù)C來展示效果。在事務(wù)C中添加了一條teacher_id=1的數(shù)據(jù)commit,RR級別中應(yīng)該會(huì)有幻讀現(xiàn)象,事務(wù)A在查詢teacher_id=1的數(shù)據(jù)時(shí)會(huì)讀到事務(wù)C新加的數(shù)據(jù)。但是測試后發(fā)現(xiàn),在MySQL中是不存在這種情況的,在事務(wù)C提交后,事務(wù)A還是不會(huì)讀到這條數(shù)據(jù)??梢娫贛ySQL的RR級別中,是解決了幻讀的讀問題的。參見下圖 讀問題解決了,根據(jù)MVCC的定義,并發(fā)提交數(shù)據(jù)時(shí)會(huì)出現(xiàn)沖突,那么沖突時(shí)如何解決呢?我們再來看看InnoDB中RR級別對于寫數(shù)據(jù)的處理。 “讀”與“讀”的區(qū)別 可能有讀者會(huì)疑惑,事務(wù)的隔離級別其實(shí)都是對于讀數(shù)據(jù)的定義,但到了這里,就被拆成了讀和寫兩個(gè)模塊來講解。這主要是因?yàn)镸ySQL中的讀,和事務(wù)隔離級別中的讀,是不一樣的。 我們且看,在RR級別中,通過MVCC機(jī)制,雖然讓數(shù)據(jù)變得可重復(fù)讀,但我們讀到的數(shù)據(jù)可能是歷史數(shù)據(jù),是不及時(shí)的數(shù)據(jù),不是數(shù)據(jù)庫當(dāng)前的數(shù)據(jù)!這在一些對于數(shù)據(jù)的時(shí)效特別敏感的業(yè)務(wù)中,就很可能出問題。 對于這種讀取歷史數(shù)據(jù)的方式,我們叫它快照讀 (snapshot read),而讀取數(shù)據(jù)庫當(dāng)前版本數(shù)據(jù)的方式,叫當(dāng)前讀 (current read)。很顯然,在MVCC中: 快照讀:就是select select * from table ....; 當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,處理的都是當(dāng)前的數(shù)據(jù),需要加鎖。 select * from table where ? lock in share mode; select * from table where ? for update; insert; update ; delete; 事務(wù)的隔離級別實(shí)際上都是定義了當(dāng)前讀的級別,MySQL為了減少鎖處理(包括等待其它鎖)的時(shí)間,提升并發(fā)能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些“當(dāng)前讀”,就需要另外的模塊來解決了。 寫("當(dāng)前讀") 事務(wù)的隔離級別中雖然只定義了讀數(shù)據(jù)的要求,實(shí)際上這也可以說是寫數(shù)據(jù)的要求。上文的“讀”,實(shí)際是講的快照讀;而這里說的“寫”就是當(dāng)前讀了。 為了解決當(dāng)前讀中的幻讀問題,MySQL事務(wù)使用了Next-Key鎖。 Next-Key鎖 Next-Key鎖是行鎖和GAP(間隙鎖)的合并,行鎖上文已經(jīng)介紹了,接下來說下GAP間隙鎖。 行鎖可以防止不同事務(wù)版本的數(shù)據(jù)修改提交時(shí)造成數(shù)據(jù)沖突的情況。但如何避免別的事務(wù)插入數(shù)據(jù)就成了問題。我們可以看看RR級別和RC級別的對比 RC級別: 事務(wù)A 事務(wù)B begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三二班 30 update class_teacher set class_name='初三四班' where teacher_id=30; insert into class_teacher values (null,'初三二班',30); commit; select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三四班 30 10 初三二班 30 RR級別: 事務(wù)A 事務(wù)B begin; begin; select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三二班 30 update class_teacher set class_name='初三四班' where teacher_id=30; insert into class_teacher values (null,'初三二班',30); waiting.... select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id 2 初三四班 30 commit; 事務(wù)Acommit后,事務(wù)B的insert執(zhí)行。 通過對比我們可以發(fā)現(xiàn),在RC級別中,事務(wù)A修改了所有teacher_id=30的數(shù)據(jù),但是當(dāng)事務(wù)Binsert進(jìn)新數(shù)據(jù)后,事務(wù)A發(fā)現(xiàn)莫名其妙多了一行teacher_id=30的數(shù)據(jù),而且沒有被之前的update語句所修改,這就是“當(dāng)前讀”的幻讀。 RR級別中,事務(wù)A在update后加鎖,事務(wù)B無法插入新數(shù)據(jù),這樣事務(wù)A在update前后讀的數(shù)據(jù)保持一致,避免了幻讀。這個(gè)鎖,就是Gap鎖。 MySQL是這么實(shí)現(xiàn)的: 在class_teacher這張表中,teacher_id是個(gè)索引,那么它就會(huì)維護(hù)一套B+樹的數(shù)據(jù)關(guān)系,為了簡化,我們用鏈表結(jié)構(gòu)來表達(dá)(實(shí)際上是個(gè)樹形結(jié)構(gòu),但原理相同) 如圖所示,InnoDB使用的是聚集索引,teacher_id身為二級索引,就要維護(hù)一個(gè)索引字段和主鍵id的樹狀結(jié)構(gòu)(這里用鏈表形式表現(xiàn)),并保持順序排列。 Innodb將這段數(shù)據(jù)分成幾個(gè)個(gè)區(qū)間 (negative infinity, 5], (5,30], (30,positive infinity); update class_teacher set class_name='初三四班' where teacher_id=30; 不僅用行鎖,鎖住了相應(yīng)的數(shù)據(jù)行;同時(shí)也在兩邊的區(qū)間,(5,30]和(30,positive infinity),都加入了gap鎖。這樣事務(wù)B就無法在這個(gè)兩個(gè)區(qū)間insert進(jìn)新數(shù)據(jù)。 受限于這種實(shí)現(xiàn)方式,Innodb很多時(shí)候會(huì)鎖住不需要鎖的區(qū)間。如下所示: 事務(wù)A 事務(wù)B 事務(wù)C begin; begin; begin; select id,class_name,teacher_id from class_teacher; id class_name teacher_id 1 初三一班 5 2 初三二班 30 update class_teacher set class_name='初一一班' where teacher_id=20; insert into class_teacher values (null,'初三五班',10); waiting ..... insert into class_teacher values (null,'初三五班',40); commit; 事務(wù)A commit之后,這條語句才插入成功 commit; commit; update的teacher_id=20是在(5,30]區(qū)間,即使沒有修改任何數(shù)據(jù),Innodb也會(huì)在這個(gè)區(qū)間加gap鎖,而其它區(qū)間不會(huì)影響,事務(wù)C正常插入。 如果使用的是沒有索引的字段,比如update class_teacher set teacher_id=7 where class_name='初三八班(即使沒有匹配到任何數(shù)據(jù))',那么會(huì)給全表加入gap鎖。同時(shí),它不能像上文中行鎖一樣經(jīng)過MySQL Server過濾自動(dòng)解除不滿足條件的鎖,因?yàn)闆]有索引,則這些字段也就沒有排序,也就沒有區(qū)間。除非該事務(wù)提交,否則其它事務(wù)無法插入任何數(shù)據(jù)。 行鎖防止別的事務(wù)修改或刪除,GAP鎖防止別的事務(wù)新增,行鎖和GAP鎖結(jié)合形成的的Next-Key鎖共同解決了RR級別在寫數(shù)據(jù)時(shí)的幻讀問題。 Serializable 這個(gè)級別很簡單,讀加共享鎖,寫加排他鎖,讀寫互斥。使用的悲觀鎖的理論,實(shí)現(xiàn)簡單,數(shù)據(jù)更加安全,但是并發(fā)能力非常差。如果你的業(yè)務(wù)并發(fā)的特別少或者沒有并發(fā),同時(shí)又要求數(shù)據(jù)及時(shí)可靠的話,可以使用這種模式。 這里要吐槽一句,不要看到select就說不會(huì)加鎖了,在Serializable這個(gè)級別,還是會(huì)加鎖的!
以上就是關(guān)于快照讀和當(dāng)前讀相關(guān)問題的回答。希望能幫到你,如有更多相關(guān)問題,您也可以聯(lián)系我們的客服進(jìn)行咨詢,客服也會(huì)為您講解更多精彩的知識(shí)和內(nèi)容。
推薦閱讀:
數(shù)據(jù)庫快照作用(數(shù)據(jù)庫快照作用有哪些)
內(nèi)存快照與磁盤快照(內(nèi)存快照與磁盤快照哪個(gè)好)
華為手機(jī)調(diào)震動(dòng)微信不震動(dòng)(華為手機(jī)調(diào)震動(dòng)微信不震動(dòng)了怎么辦)