MVCC多版本并发控制和幻读问题的解决
生活随笔
收集整理的這篇文章主要介紹了
MVCC多版本并发控制和幻读问题的解决
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
首先我們先介紹一下鎖的分類,再進入今天的正題。
一、鎖分類:
-
1.從性能上分:樂觀鎖、悲觀鎖。樂觀鎖(用版本號對比或CAS機制)適用于讀比較多的場景,悲觀鎖適用于寫比較多的場景。如果在寫比較多的場景使用樂觀鎖,會導致對比次數過多,影響性能。
-
2.從對數據的粒度上分:表鎖、頁鎖、行鎖。
-
3.從對數據庫的操作上分:讀鎖(悲觀鎖)、寫鎖(悲觀鎖)、意向鎖。
- 讀鎖(共享鎖,S鎖(shared)):針對同一條數據加了讀鎖之后,其他讀操作可以同時進行,不受影響。
比如:select * from A where id = 1 lock in share mode. - 寫鎖(排他鎖,X鎖(exclusive)):針對同一條數據加了寫鎖之后,其他的事務不能寫和讀。insert、update、delete會加寫鎖,查詢可以通過加for update加寫鎖。
比如:select * from A where id = 1 for update. - 意向鎖:又稱I鎖,表鎖。主要是為了提高加表鎖的效率,mysql提供的。
- 讀鎖(共享鎖,S鎖(shared)):針對同一條數據加了讀鎖之后,其他讀操作可以同時進行,不受影響。
-
間隙鎖和臨鍵鎖:
間隙鎖的目的主要是為了防止幻讀,以滿足相關隔離級別的要求。間隙鎖主要通過兩個方面實現這個目的:一是防止間隙內有新數據被插入,二是防止已存在的數據被更新成間隙內的數據。 1.間隙鎖:鎖的是兩個值之間的間隙,在可重復讀隔離級別下生效,mysql默認是RR級別,RR級別下有幻讀問題,間隙鎖就是為了解決幻讀問題出現的。 2.臨鍵鎖:行鎖和間隙鎖的組合。注意:關于RR級別行鎖升級為表鎖的原因:
在RR級別下,需要解決不可重復讀和幻讀問題。在遍歷掃描聚集索引記錄時,為了防止掃描過的索引被其他事務修改(不可重復讀問題) 或 間隙被其他事務插入記錄(幻讀問題),從而導致數據不一致,索引mysql的解決方案就是把所有掃描過的索引記錄和間隙都鎖上,這里并不是直接將整張表加表鎖,因為不一定能加上表鎖,可能會有其他的事務鎖住表里的其他記錄。
二、MVCC多版本并發控制:
首先先介紹幾個相關的概念:
-
1.mvcc定義和核心思想:
定義:mvcc是一種并發控制機制,用來控制并發執行的事務,保證事務之間的隔離性。 核心思想:mvcc是通過保存某個時間點的數據生成快照版本來定義的。mvcc允許同一條記錄有不同的快照版本,不同事務在查詢時通過添加一些約束條件,就可以得到對于某個時刻快照版本的數據。 mysql在讀已提交和可重復讀隔離級別下都實現了mvcc機制。 -
2.快照讀和當前讀:
1.快照讀:基于mvcc機制和undo log實現的,適用于簡單的select語句。 2.當前讀:基于臨鍵鎖(行鎖+間隙鎖)實現的,適用于update、insert、delete、select...for update、select...lock in share mode及加鎖的select語句。 -
3.undo日志版本鏈:
undo日志版本鏈是指一行數據被每個事務依次修改過后,在每個事務修改完后,會保留修改前的數據到undo log日志中,通過trx_id和roll_pointer字段將undo log日志串連成一個歷史記錄日志版本鏈。 -
4.MVCC版本對比規則:
在可重復讀隔離級別下,當事務開啟時,任何查詢的sql在第一次select時都會生成一致性視圖read-view,并且這個視圖在事務結束前都不會改變。這個視圖是由所有未提交的事務組成的事務組,事務組里面的事務查詢sql都需要都對應記錄版本鏈的最新記錄逐條和read-view做對比,最終得到想要的快照結果。 注:在讀已提交的隔離級別下,每次select查詢都會生成一個一致性視圖。 a.create_trx_id : 當前事務id b.trxs組:當前所有未提交事務 c.min_trx_id: 最小未提交事務id d.max_trx_id:最大未提交事務id # 版本對比規則: 1.當trx_id = create_trx_id可見。 2.當trx_id < min_trx_id,表示這個版本由已提交事務生成的,可見。 3.當trx_id > max_trx_id,表示這個版本是由將來的事務生成的,可見。 4.當min<= trx_id <= max_trx_id,分兩種情況: a.當trx_id在這個trxs組內,說明這個版本是未提交的事務,不可見。 b.當trx_id不再這個trxs組內,說明這個版本是由已提交事務生成的,可見。
三、幻讀問題的解決:
首先回顧一下事務的四大特性和事務的隔離級別,以及不同的隔離級別會出現什么樣的問題。
事務的四大特性:原子性、一致性、隔離性、持久性。
事務的隔離級別:讀未提交、讀已提交、不可重復讀、串行化。
讀未提交:會出現臟讀(當前內存讀),不可重復讀,幻讀問題。
讀已提交:不會出現臟讀問題,但會出現不可重復讀,幻讀問題。
不可重復讀:不會出現臟讀、不可重復讀問題,但會出現幻讀問題(mvcc機制和鎖可以徹底解決這個問題)。
串行化:串行化讀取數據,但是事務的并發度就沒有了。
* 臟讀:讀取到其他事務未提交的數據
* 不可重復讀問題:指的是在同一個事務中,多次查詢同一條數據(已經存在的數據),由于其他事務的修改,導致查詢的結果不一樣。
* 幻讀:指的是在一個事務中,查詢一個范圍內的數據,一般是count,多次返回的數量不一樣,查詢到其他事務新插入的數據。
-
結論:先說一下結論,在RR隔離級別下,幻讀問題可以通過mvcc機制和間隙鎖或臨鍵鎖解決(必需讓查詢語句使用當前讀,不能使用快照讀)。
下面舉了一些示例:CREATE TABLE `gap_test` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `number` int(11) NOT NULL COMMENT '數字', PRIMARY KEY (`id`) USING BTREE, KEY `index_number` (`number`) ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='間隙鎖表' -
1.成功解決幻讀的問題示例:
#間隙鎖示例:(間隙鎖之間是兼容的,高并發的場景下,不正確的使用可能會導致死鎖) session 1: begin; select * from gap_test where number = 8 from update; // 記錄不存在,執行成功 session 2: begin; select * from gap_test where number = 8 from update; // 記錄不存在,執行成功 insert into gap_test value (11,11);//阻塞 insert into gap_test value (7,5);//阻塞 insert into gap_test value (2,5);//執行成功 insert into gap_test value (14,11);//執行成功 這時會對number=8進行檢索,檢索不到記錄,會向左取最近的值number=5作為左區間,向右取最近的值number=11作為右區間,所以session 1鎖的間隙范圍為:number(5,11)。即在(id=6,number=5)和(id=13,number=11)這個間隙范圍內不能插入任何數據。 #臨鍵鎖示例: session 1: begin; select * from gap_test where number = 5 from update;//記錄存在,執行成功(會加寫鎖) 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);//執行成功 insert into gap_test value (14,11);//執行成功 這時會對number = 5進行檢索,檢索到記錄之后,會對number =5的記錄加寫鎖,然后向左取最近的值number=4作為左區間,向右取最近的值number=11作為右區間,所以session 1鎖的間隙范圍為:number(4,5),number(5,11)。即在(id=3,number=4)和(id=6,number=5)、(id=6,number=5)和(id=13,number=11)之間的間隙范圍內不能插入任何數據。 -
2.沒有解決幻讀問題的示例:
- 上面的示例產生了幻讀問題。事務A和事務B同時執行,事務A修改了事務B的新提交的記錄,再查詢時查到上次沒有查到的記錄,產生了幻讀。要徹底解決幻讀問題,查詢語句需要加鎖,由快照讀變為當前讀。
-
3.高并發的場景下,不正確的使用可能會導致死鎖示例:
- 事務A和事務B同時開啟事務執行查詢語句,這個間隙鎖的范圍是(30, +oo),事務B先在間隙鎖范圍內插入一條語句,事務A也在間隙鎖范圍內插入一條語句,然后發生了死鎖。
- 我們執行一下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 ------------------------ ### 發生死鎖 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 ============================
后面一章,我們會著重分析一下mysql在RR隔離級別下的加鎖過程,敬請期待!
總結
以上是生活随笔為你收集整理的MVCC多版本并发控制和幻读问题的解决的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开放 LLM 排行榜: 深入研究 DRO
- 下一篇: Rocketmq学习2——Rocketm