目錄
一、什么是幻讀?
1.1 幻讀的定義
1.2 幻讀的示例
1.3 幻讀產生的原因?
1.4?讀已提交(Read Committed)
1.4.1 確定事務等級
1.4.2 非鎖定讀取
準備
示例
結論
1.4.3 鎖定讀取
準備
示例
分析
結論
1.5?可重復讀(Repeatable Read)
1.5.1 確定事務等級
1.5.2 非鎖定讀取
準備
示例
結論
1.5.3 鎖定讀取
準備
示例
分析
結論
二、怎么解決幻讀?
2.1 提高事務隔離等級
2.2 間隙鎖(Gap Locking)
三、間隙鎖
3.1 定義
3.2 工作原理
無間隙鎖
有間隙鎖
3.3?間隙鎖的類型
范圍查詢
相等查詢
參考資料
一、什么是幻讀?
1.1 幻讀的定義
**幻讀(Phantom Read)**是數據庫事務中的一種并發問題,指的是在一個事務執行過程中,另一個事務插入、刪除或修改了符合查詢條件的記錄,導致前一個事務讀取到不同的數據集,從而產生“幻覺”般的讀操作。
1.2 幻讀的示例
具體來說,事務A執行過程中,事務B插入或者修改了記錄,導致事務A在相同查詢條件下讀取到的結果發生了改變。
假設有用戶表users,包含列username、password、state。
1.事務A,查詢所有狀態啟用的用戶
select count(id) from users where state=1;
2.事務B,插入新用戶,state=1
insert into users(username, password, state) values('test11', 123456, 1);
3.事務A,再次執行相同的sql
select count(id)?from users where state=1;
事務A兩次查詢,得到了不同的結果。
1.3 幻讀產生的原因?
1.對表進行了插入或刪除
2.事務隔離等級
**幻讀(Phantom Read)**通常在較低的事務隔離等級中產生(如 讀已提交(READ COMMITTED)、可重復讀(REPEATABLE READ))。
讀已提交(Read Committed):可以讀取到已提交的記錄,但無法保證查詢過程中數據不被改變,因此會發生幻讀;
可****重復讀(Repeatable Read):可以保證事務中多次查詢的結果一致,但仍然可能出現幻讀,因為在非鎖定讀取時,允許新記錄的插入(即事務 A 查詢的范圍可能會變化)。
兩種隔離等級下,產生幻讀的情況看下文具體示例。
1.4****讀已提交(Read Committed)
1.4.1 確定事務等級
查看當前事務隔離等級
SELECT @@transaction_isolation;
將事務隔離等級設為【讀已提交】
#設置事務隔離等級
SET SESSION TRANSACTION ISOLATION LEVEL {level};
#{level}可選項::
#讀未提交: READ UNCOMMITTED·
#讀已提交: READ COMMITTED
#可重復讀: REPEATABLE READ
#可串行化: SERIALIZABLE
1.4.2 非鎖定讀取
準備
準備一組數據,進行以下測試:事務A執行查詢,事務B插入一條記錄,事務A再次執行查詢。
示例
1.事務A執行查詢
select * from tmp where id>=2;
2.事務B插入數據,并提交
insert into `tmp`(`id`,`value`) values (4, 'dd');
3.事務A再次執行查詢
結論
事務A兩次查詢結果不一致,可以讀取到事務B已提交的記錄,出現【幻讀】
1.4.3 鎖定讀取
準備
準備一組數據,進行以下測試:事務A執行查詢,事務B插入一條記錄,事務A再次執行查詢。
示例
1.事務A進行鎖定讀取
select * from tmp where id >= 4 for update;
2.事務B,在間隙插入數據,并提交
insert into `tmp`(`id`,`value`) values (5, 'dd');
3.事務A再次執行查詢,兩次讀取結果不一致,發現間隙并沒有鎖住
并且事務B插入的行已經落庫。
分析
然后我們看MySQL文檔解釋
意思就是,在【讀已提交】隔離等級下,針對鎖定讀取(共享鎖or排它鎖)、UPDATE語句和DELETE語句,innodb只鎖定索引到的記錄,鎖定記錄的前后都能插入新記錄。也就說“間隙”并沒有被鎖住。
再看【間隙鎖】這邊的解釋,間隙鎖在讀已提交隔離等級下,在查詢和索引掃描被禁用了。
結論
雖然使用了鎖定讀取,但讀已提交(Read Committed)下,幻讀依然存在。
1.5可重復讀(Repeatable Read)
1.5.1 確定事務等級
查看當前事務隔離等級
?SELECT @@transaction_isolation;
1.5.2 非鎖定讀取
準備
準備一組數據,進行以下測試:事務A執行查詢,事務B插入一條記錄,事務A再次執行查詢。
示例
1.事務A執行查詢
select * from tmp where id>=4;
2.事務B插入一條記錄, 并commit
insert into `tmp`( `id`, `value` ) values (5, 'dd');
3.事務A再次執行查詢
select * from tmp where id>=4;
事務B執行commit后,由于事務隔離等級緣故,事務A再次查詢,結果不影響,具備可重復讀特性;事務A結束后,再次查詢多了一條數據(5, ‘dd’)。
結論
在可重復讀(Repeatable Read)隔離等級下,事務內部具備可重復讀特性; 但由于“間隙”并未鎖住,非鎖定讀取會出現“幻讀”。
1.5.3 鎖定讀取
準備
準備一組數據,進行以下測試:事務A執行查詢,事務B插入一條記錄,事務A再次執行查詢。
示例
事務A執行查詢
select * from tmp where id>=4 for update;
事務B插入一條記錄
insert into `tmp`( `id`, `value` ) values (5, 'dd');
這時,發現插入操作被阻塞了。
分析
我們看一下執行的查詢語句
select * from tmp where id>=4 for update;
- Id=4所在的行
- Id=4與 id=6之間的間隙
- Id=6所在的行
- Id=6之后的間隙
這就意味著,在間隙插入記錄會被阻止,直到事務A結束。
共享鎖與示例的排它鎖相同,都是通過阻止其他會話變更,來避免對當前會話的影響。
UPDATE語句、DELETE語句與鎖定讀取(Locking Reads)同理,在執行時,會根據使用的查詢條件進行加鎖:
- 相等(=、in):InnoDB只鎖定找到的索引記錄
- 范圍(BETWEEN、>、<):InnoDB鎖定掃描的索引范圍
要注意的是,對于使用唯一索引的語句,完全沒必要進行加鎖。
注:
我們知道,默認情況下,MySQL是在autocommit開啟的狀態下運行。開啟事務(START TRANSACTION)后,自動提交模式(autocommit)會隱式禁用,所有的純SELECT語句會被轉化成SELECT…LOCK IN SHARE MODE, 但是如果想要鎖住間隙或是鎖住索引記錄的話,需要顯示觸發鎖定行為。
結論
在可重復讀(Repeatable Read)隔離等級下,針對鎖定讀取(共享鎖or排它鎖)、UPDATE語句和DELETE語句,不會出現“幻讀”。
二、怎么解決幻讀?
2.1 提高事務隔離等級
使用可串行化(Serializable),提事務隔離等級來避免。在可串行化隔離等級下,數據庫會對查詢和寫進行加鎖,確保事務的完全順序執行,但是并發效率低下。
2.2 間隙鎖(G****ap Locking)
間隙鎖是加在索引記錄之間間隙的鎖,又或者是在索引區間第一條記錄之前、或最后一條記錄之后。
示例:
select * from users where age between 10 and 30FOR UPDATE;
age(10-30)范圍內所有現有值之間的間隙都被鎖定,防止其他事務將值等于15插入列age中,無論該列中是否已有該值。
三、間隙鎖
3.1 定義
**間隙鎖(Gap Lock)**是一種數據庫鎖定機制,常見于支持行級鎖的數據庫(如 MySQL 的 InnoDB 存儲引擎)中,主要用于解決并發事務中的“幻讀”問題。
3.2 工作原理
假定有數據表
±—±------+
| id | title |
±—±------+
| 1 | aa |
| 2 | bb|
| 3 | cc|
±—±------+
無間隙鎖
事務A查詢id > 2 的記錄并鎖定這條記錄,然后事務B插入一條id=4的新紀錄,事務A再次查詢時會得到不同的結果集。
有間隙鎖
事務A執行查詢 id > 2時,這條查詢會鎖定 id > 2 的記錄、所有行之間的間隙。
3.3****間隙鎖的類型
范圍查詢
當事務執行范圍查詢(如BETWEEN、>、<等)時,數據庫會在查詢范圍內加上間隙鎖。
例如:
select * from `tmp` where `id` > 5 for update;
可重復讀(Repeatable Read)隔離等級下,可能會鎖定 id >5 的所有間隙.
相等查詢
當事務執行相等查詢(如 = )時,數據庫會鎖定對應的索引記錄。如果索引記錄不存在,則會鎖定對應的間隙,防止其他事務插入重復的記錄。
示例:
±—±------+
| id | value |
±—±------+
| 2 | aa |
| 4 | bb |
| 6 | cc |
±—±------+
事務A, 執行查詢, 鎖住id=5的間隙
select * from tmp where id=5 for update;
事務B, 嘗試在id=5插入數據
insert into `tmp`( `id`, `value` ) values ( 5, 'bb' );
發現事務B,在事務A未結束的情況下,一直被阻塞直至超時
參考資料
MySQL數據庫事務隔離等級:The InnoDB Engine: Transaction Isolation Levels
InnoDB一致性非鎖定讀取:The InnoDB Engine | Consistent Nonlocking Reads
InnoDB幻影行(幻讀):The InnoDB Engine | Phantom Rows
InnoDB間隙鎖:The InnoDB Engine | Gap locking