MVCC多版本并发控制和幻读问题的解决
生活随笔
收集整理的這篇文章主要介紹了
MVCC多版本并发控制和幻读问题的解决
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
首先我們先介紹一下鎖的分類,再進(jìn)入今天的正題。
一、鎖分類:
-
1.從性能上分:樂觀鎖、悲觀鎖。樂觀鎖(用版本號(hào)對(duì)比或CAS機(jī)制)適用于讀比較多的場景,悲觀鎖適用于寫比較多的場景。如果在寫比較多的場景使用樂觀鎖,會(huì)導(dǎo)致對(duì)比次數(shù)過多,影響性能。
-
2.從對(duì)數(shù)據(jù)的粒度上分:表鎖、頁鎖、行鎖。
-
3.從對(duì)數(shù)據(jù)庫的操作上分:讀鎖(悲觀鎖)、寫鎖(悲觀鎖)、意向鎖。
- 讀鎖(共享鎖,S鎖(shared)):針對(duì)同一條數(shù)據(jù)加了讀鎖之后,其他讀操作可以同時(shí)進(jìn)行,不受影響。
比如:select * from A where id = 1 lock in share mode. - 寫鎖(排他鎖,X鎖(exclusive)):針對(duì)同一條數(shù)據(jù)加了寫鎖之后,其他的事務(wù)不能寫和讀。insert、update、delete會(huì)加寫鎖,查詢可以通過加for update加寫鎖。
比如:select * from A where id = 1 for update. - 意向鎖:又稱I鎖,表鎖。主要是為了提高加表鎖的效率,mysql提供的。
- 讀鎖(共享鎖,S鎖(shared)):針對(duì)同一條數(shù)據(jù)加了讀鎖之后,其他讀操作可以同時(shí)進(jìn)行,不受影響。
-
間隙鎖和臨鍵鎖:
間隙鎖的目的主要是為了防止幻讀,以滿足相關(guān)隔離級(jí)別的要求。間隙鎖主要通過兩個(gè)方面實(shí)現(xiàn)這個(gè)目的:一是防止間隙內(nèi)有新數(shù)據(jù)被插入,二是防止已存在的數(shù)據(jù)被更新成間隙內(nèi)的數(shù)據(jù)。 1.間隙鎖:鎖的是兩個(gè)值之間的間隙,在可重復(fù)讀隔離級(jí)別下生效,mysql默認(rèn)是RR級(jí)別,RR級(jí)別下有幻讀問題,間隙鎖就是為了解決幻讀問題出現(xiàn)的。 2.臨鍵鎖:行鎖和間隙鎖的組合。注意:關(guān)于RR級(jí)別行鎖升級(jí)為表鎖的原因:
在RR級(jí)別下,需要解決不可重復(fù)讀和幻讀問題。在遍歷掃描聚集索引記錄時(shí),為了防止掃描過的索引被其他事務(wù)修改(不可重復(fù)讀問題) 或 間隙被其他事務(wù)插入記錄(幻讀問題),從而導(dǎo)致數(shù)據(jù)不一致,索引mysql的解決方案就是把所有掃描過的索引記錄和間隙都鎖上,這里并不是直接將整張表加表鎖,因?yàn)椴灰欢芗由媳礞i,可能會(huì)有其他的事務(wù)鎖住表里的其他記錄。
二、MVCC多版本并發(fā)控制:
首先先介紹幾個(gè)相關(guān)的概念:
-
1.mvcc定義和核心思想:
定義:mvcc是一種并發(fā)控制機(jī)制,用來控制并發(fā)執(zhí)行的事務(wù),保證事務(wù)之間的隔離性。 核心思想:mvcc是通過保存某個(gè)時(shí)間點(diǎn)的數(shù)據(jù)生成快照版本來定義的。mvcc允許同一條記錄有不同的快照版本,不同事務(wù)在查詢時(shí)通過添加一些約束條件,就可以得到對(duì)于某個(gè)時(shí)刻快照版本的數(shù)據(jù)。 mysql在讀已提交和可重復(fù)讀隔離級(jí)別下都實(shí)現(xiàn)了mvcc機(jī)制。 -
2.快照讀和當(dāng)前讀:
1.快照讀:基于mvcc機(jī)制和undo log實(shí)現(xiàn)的,適用于簡單的select語句。 2.當(dāng)前讀:基于臨鍵鎖(行鎖+間隙鎖)實(shí)現(xiàn)的,適用于update、insert、delete、select...for update、select...lock in share mode及加鎖的select語句。 -
3.undo日志版本鏈:
undo日志版本鏈?zhǔn)侵敢恍袛?shù)據(jù)被每個(gè)事務(wù)依次修改過后,在每個(gè)事務(wù)修改完后,會(huì)保留修改前的數(shù)據(jù)到undo log日志中,通過trx_id和roll_pointer字段將undo log日志串連成一個(gè)歷史記錄日志版本鏈。 -
4.MVCC版本對(duì)比規(guī)則:
在可重復(fù)讀隔離級(jí)別下,當(dāng)事務(wù)開啟時(shí),任何查詢的sql在第一次select時(shí)都會(huì)生成一致性視圖read-view,并且這個(gè)視圖在事務(wù)結(jié)束前都不會(huì)改變。這個(gè)視圖是由所有未提交的事務(wù)組成的事務(wù)組,事務(wù)組里面的事務(wù)查詢sql都需要都對(duì)應(yīng)記錄版本鏈的最新記錄逐條和read-view做對(duì)比,最終得到想要的快照結(jié)果。 注:在讀已提交的隔離級(jí)別下,每次select查詢都會(huì)生成一個(gè)一致性視圖。 a.create_trx_id : 當(dāng)前事務(wù)id b.trxs組:當(dāng)前所有未提交事務(wù) c.min_trx_id: 最小未提交事務(wù)id d.max_trx_id:最大未提交事務(wù)id # 版本對(duì)比規(guī)則: 1.當(dāng)trx_id = create_trx_id可見。 2.當(dāng)trx_id < min_trx_id,表示這個(gè)版本由已提交事務(wù)生成的,可見。 3.當(dāng)trx_id > max_trx_id,表示這個(gè)版本是由將來的事務(wù)生成的,可見。 4.當(dāng)min<= trx_id <= max_trx_id,分兩種情況: a.當(dāng)trx_id在這個(gè)trxs組內(nèi),說明這個(gè)版本是未提交的事務(wù),不可見。 b.當(dāng)trx_id不再這個(gè)trxs組內(nèi),說明這個(gè)版本是由已提交事務(wù)生成的,可見。
三、幻讀問題的解決:
首先回顧一下事務(wù)的四大特性和事務(wù)的隔離級(jí)別,以及不同的隔離級(jí)別會(huì)出現(xiàn)什么樣的問題。
事務(wù)的四大特性:原子性、一致性、隔離性、持久性。
事務(wù)的隔離級(jí)別:讀未提交、讀已提交、不可重復(fù)讀、串行化。
讀未提交:會(huì)出現(xiàn)臟讀(當(dāng)前內(nèi)存讀),不可重復(fù)讀,幻讀問題。
讀已提交:不會(huì)出現(xiàn)臟讀問題,但會(huì)出現(xiàn)不可重復(fù)讀,幻讀問題。
不可重復(fù)讀:不會(huì)出現(xiàn)臟讀、不可重復(fù)讀問題,但會(huì)出現(xiàn)幻讀問題(mvcc機(jī)制和鎖可以徹底解決這個(gè)問題)。
串行化:串行化讀取數(shù)據(jù),但是事務(wù)的并發(fā)度就沒有了。
* 臟讀:讀取到其他事務(wù)未提交的數(shù)據(jù)
* 不可重復(fù)讀問題:指的是在同一個(gè)事務(wù)中,多次查詢同一條數(shù)據(jù)(已經(jīng)存在的數(shù)據(jù)),由于其他事務(wù)的修改,導(dǎo)致查詢的結(jié)果不一樣。
* 幻讀:指的是在一個(gè)事務(wù)中,查詢一個(gè)范圍內(nèi)的數(shù)據(jù),一般是count,多次返回的數(shù)量不一樣,查詢到其他事務(wù)新插入的數(shù)據(jù)。
-
結(jié)論:先說一下結(jié)論,在RR隔離級(jí)別下,幻讀問題可以通過mvcc機(jī)制和間隙鎖或臨鍵鎖解決(必需讓查詢語句使用當(dāng)前讀,不能使用快照讀)。
下面舉了一些示例:CREATE TABLE `gap_test` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `number` int(11) NOT NULL COMMENT '數(shù)字', PRIMARY KEY (`id`) USING BTREE, KEY `index_number` (`number`) ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='間隙鎖表' -
1.成功解決幻讀的問題示例:
#間隙鎖示例:(間隙鎖之間是兼容的,高并發(fā)的場景下,不正確的使用可能會(huì)導(dǎo)致死鎖) session 1: begin; select * from gap_test where number = 8 from update; // 記錄不存在,執(zhí)行成功 session 2: begin; select * from gap_test where number = 8 from update; // 記錄不存在,執(zhí)行成功 insert into gap_test value (11,11);//阻塞 insert into gap_test value (7,5);//阻塞 insert into gap_test value (2,5);//執(zhí)行成功 insert into gap_test value (14,11);//執(zhí)行成功 這時(shí)會(huì)對(duì)number=8進(jìn)行檢索,檢索不到記錄,會(huì)向左取最近的值number=5作為左區(qū)間,向右取最近的值number=11作為右區(qū)間,所以session 1鎖的間隙范圍為:number(5,11)。即在(id=6,number=5)和(id=13,number=11)這個(gè)間隙范圍內(nèi)不能插入任何數(shù)據(jù)。 #臨鍵鎖示例: session 1: begin; select * from gap_test where number = 5 from update;//記錄存在,執(zhí)行成功(會(huì)加寫鎖) session 2: begin; select * from gap_test where number = 5 from update;//阻塞(寫鎖互斥) insert into gap_test value (4,4);//阻塞 insert into gap_test value (4,5);//阻塞 insert into gap_test value (8,8);//阻塞 insert into gap_test value (11,11);//阻塞 insert into gap_test value (2,4);//執(zhí)行成功 insert into gap_test value (14,11);//執(zhí)行成功 這時(shí)會(huì)對(duì)number = 5進(jìn)行檢索,檢索到記錄之后,會(huì)對(duì)number =5的記錄加寫鎖,然后向左取最近的值number=4作為左區(qū)間,向右取最近的值number=11作為右區(qū)間,所以session 1鎖的間隙范圍為:number(4,5),number(5,11)。即在(id=3,number=4)和(id=6,number=5)、(id=6,number=5)和(id=13,number=11)之間的間隙范圍內(nèi)不能插入任何數(shù)據(jù)。 -
2.沒有解決幻讀問題的示例:
- 上面的示例產(chǎn)生了幻讀問題。事務(wù)A和事務(wù)B同時(shí)執(zhí)行,事務(wù)A修改了事務(wù)B的新提交的記錄,再查詢時(shí)查到上次沒有查到的記錄,產(chǎn)生了幻讀。要徹底解決幻讀問題,查詢語句需要加鎖,由快照讀變?yōu)楫?dāng)前讀。
-
3.高并發(fā)的場景下,不正確的使用可能會(huì)導(dǎo)致死鎖示例:
- 事務(wù)A和事務(wù)B同時(shí)開啟事務(wù)執(zhí)行查詢語句,這個(gè)間隙鎖的范圍是(30, +oo),事務(wù)B先在間隙鎖范圍內(nèi)插入一條語句,事務(wù)A也在間隙鎖范圍內(nèi)插入一條語句,然后發(fā)生了死鎖。
- 我們執(zhí)行一下SHOW ENGINE INNODB STATUS語句查看死鎖日志:
===================================== 2024-01-12 16:42:35 4af4 INNODB MONITOR OUTPUT ===================================== Per second averages calculated from the last 34 seconds ----------------- BACKGROUND THREAD ----------------- srv_master_thread loops: 661 srv_active, 0 srv_shutdown, 239375 srv_idle srv_master_thread log flush and writes: 240027 ---------- SEMAPHORES ---------- OS WAIT ARRAY INFO: reservation count 156 OS WAIT ARRAY INFO: signal count 155 Mutex spin waits 228, rounds 1256, OS waits 27 RW-shared spins 125, rounds 3674, OS waits 121 RW-excl spins 4, rounds 264, OS waits 4 Spin rounds per wait: 5.51 mutex, 29.39 RW-shared, 66.00 RW-excl ------------------------ ### 發(fā)生死鎖 LATEST DETECTED DEADLOCK ------------------------ 2024-01-12 16:40:30 48e8 *** (1) TRANSACTION: TRANSACTION 1488272, ACTIVE 67 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1 MySQL thread id 39, OS thread handle 0x5c68, query id 2627 ::1 root update insert into gap_test value (40,36) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488272 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) TRANSACTION: TRANSACTION 1488273, ACTIVE 48 sec inserting mysql tables in use 1, locked 1 3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1 MySQL thread id 38, OS thread handle 0x48e8, query id 2628 ::1 root update insert into gap_test value (41,37) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488273 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488273 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (2) ------------ TRANSACTIONS ------------ Trx id counter 1488278 Purge done for trx's n:o < 1488278 undo n:o < 0 state: running but idle History list length 583 LIST OF TRANSACTIONS FOR EACH SESSION: ---TRANSACTION 0, not started MySQL thread id 41, OS thread handle 0x4ac8, query id 2577 ::1 root cleaning up ---TRANSACTION 1488267, not started MySQL thread id 40, OS thread handle 0x4af4, query id 2629 ::1 root init SHOW ENGINE INNODB STATUS ---TRANSACTION 1488273, not started MySQL thread id 38, OS thread handle 0x48e8, query id 2628 ::1 root cleaning up ---TRANSACTION 1488272, ACTIVE 192 sec 4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1 MySQL thread id 39, OS thread handle 0x5c68, query id 2627 ::1 root cleaning up -------- FILE I/O -------- I/O thread 0 state: wait Windows aio (insert buffer thread) I/O thread 1 state: wait Windows aio (log thread) I/O thread 2 state: wait Windows aio (read thread) I/O thread 3 state: wait Windows aio (read thread) I/O thread 4 state: wait Windows aio (read thread) I/O thread 5 state: wait Windows aio (read thread) I/O thread 6 state: wait Windows aio (write thread) I/O thread 7 state: wait Windows aio (write thread) I/O thread 8 state: wait Windows aio (write thread) I/O thread 9 state: wait Windows aio (write thread) Pending normal aio reads: 0 [0, 0, 0, 0] , aio writes: 0 [0, 0, 0, 0] , ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0 Pending flushes (fsync) log: 0; buffer pool: 0 3071 OS file reads, 882 OS file writes, 559 OS fsyncs 0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s ------------------------------------- INSERT BUFFER AND ADAPTIVE HASH INDEX ------------------------------------- Ibuf: size 1, free list len 0, seg size 2, 0 merges merged operations: insert 0, delete mark 0, delete 0 discarded operations: insert 0, delete mark 0, delete 0 Hash table size 276707, node heap has 8 buffer(s) 0.00 hash searches/s, 0.00 non-hash searches/s --- LOG --- Log sequence number 1239834765 Log flushed up to 1239834765 Pages flushed up to 1239834765 Last checkpoint at 1239834765 0 pending log writes, 0 pending chkp writes 251 log i/o's done, 0.00 log i/o's/second ---------------------- BUFFER POOL AND MEMORY ---------------------- Total memory allocated 137428992; in additional pool allocated 0 Dictionary memory allocated 4504559 Buffer pool size 8192 Free buffers 5820 Database pages 2364 Old database pages 888 Modified db pages 0 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young 0, not young 0 0.00 youngs/s, 0.00 non-youngs/s Pages read 2339, created 25, written 525 0.00 reads/s, 0.00 creates/s, 0.00 writes/s No buffer pool page gets since the last printout Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 2364, unzip_LRU len: 0 I/O sum[0]:cur[0], unzip sum[0]:cur[0] -------------- ROW OPERATIONS -------------- 0 queries inside InnoDB, 0 queries in queue 0 read views open inside InnoDB Main thread id 7344, state: sleeping Number of rows inserted 34, updated 25, deleted 0, read 3991433 0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s ---------------------------- END OF INNODB MONITOR OUTPUT ============================
后面一章,我們會(huì)著重分析一下mysql在RR隔離級(jí)別下的加鎖過程,敬請(qǐng)期待!
總結(jié)
以上是生活随笔為你收集整理的MVCC多版本并发控制和幻读问题的解决的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开放 LLM 排行榜: 深入研究 DRO
- 下一篇: Rocketmq学习2——Rocketm