mysql 隔離級別與間隙鎖等


數據庫隔離級

SQL標准中DB隔離級別有:

read uncommitted:可以讀到其它transaction 未提交數據
read committed:可以讀到其它transaction 已提交數據
repeatable read:一個transaction中相同的查詢,每次獲取的結果是一樣的
serialize:所有操作串行

這幾種隔離級別為的是解決並發中的如下問題:

臟讀
即一個transaction 可以讀到另一個的未提交數據。

不可重復讀
即一個transaction 中,兩次相同的查詢會讀到不一樣的結果。

幻讀
一個transaction中,之前不存在的記錄,突然存在了。

read uncommitted 和 serialize 基本沒有數據庫在用。所以我們只關注read committed 和 repeatable read, ORACLE 默認是 rc 級別,mysql默認是rr級別。我們本文的實驗都是mysql rr級別下做的。
這兩種隔離級別對並發沖突的解決程度如下:

隔離級別 臟讀 不可重復讀 幻讀
RC 不存在 存在 存在
RR 不存在 不存在 存在

**要注意的是,sql標准中定義的RR 是存在幻讀的,但實際上mysql 的RR級別不存在幻讀 **

RC級別可以看到其它transaction的提交,所以它是不可重復讀的,同時也會存在幻讀
RR級別不可看到其它transaction的更改,所以它是可重復讀的。但SQL標准定義的RR存在幻讀。(雖然mysql的實現中做到了RR無幻讀)。為什么SQL 標准定義的RR 會有幻讀呢? 首先要進一步清晰不可重復讀和幻讀的區別

幻讀和不可重復讀

幻讀和不可重復讀不太好區分。不可重復讀針對update/delete等操作和已有的數據, 而幻讀針對的是insert類型操作。一次transaction中,之前讀取過的數據被其它transaction 更改提交了,並且在本transaction中能夠看到。這是不可重復讀。一次transaction中,之前沒有的數據,再次操作時卻有了,是幻讀。雖然幻讀也是一種不可重復讀,但還是要把它們區分開討論。這是因為,這兩種問題的解決方案很不一樣。

如果使用鎖機制來實現這兩種隔離級別,在可重復讀中,該sql第一次讀取到數據后,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重復讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。

所以幻讀和不可重復讀最大的區別是如何用鎖來解決他們產生的問題。

MVCC

用鎖解決不可重復讀的問題非常簡單。只需要對transaction中讀取到的數據加行鎖。保證讀取過程中該行不被修改即可。但這樣會造成部分數據的操作變成串行。主流的數據庫一般不通過鎖的方式來實現可重復讀,它們采取的方式叫 MVCC (multi-version concurrent control),我們以mysql 的MVCC 來說明。

mysql 在每一行后面加了幾個字段,來標識每一行的狀態, 如 row_id 記錄行的聚簇索引 key , 創建/更新事務ID, 刪除事務ID,回滾ID , 如:

    ROW_ID, F1, F2, ... ROW_ID, 創建事務ID, 刪除事務ID,回滾ID

INSERT 操作會把創建事務ID 置為自己的transaction ID , 刪除事務ID 和回滾ID 為空

UPDATE 操作會:

    1. 創建該行的回滾記錄
    2. 更新創建事務ID 為自己的ID
    3. 更新回滾ID 指向回滾記錄

DELETE 操作會

1. 創建該行回滾記錄
2. 更新刪除事務ID為自己的transaction ID
3. 更新回滾ID

SELECT 操作會

查找刪除事務ID 為空(沒刪除)或大於自己事務ID 的記錄(自己transaction 開始之后刪除的)
and
創建事務ID 小於等於自己的transaction id (確保不是自己transaction后創建的)

通過這種方式,可以發現mysql 其實實現了可重復讀。並且不必加鎖,因為各事務可以維護和操作不同版本的數據。

也行你認為該方式不僅解決了可重復讀,還解決了幻讀。 確實,假設有transaction A和B ,並且A早於B 開始,那么B中insert的記錄A 是讀不到的,從這個角度說是解決了幻讀。但實際上並不完全對,mysql中的讀其實有兩種,當前讀和快照讀

當前讀和快照讀

RR模式MYSQL 中的SELECT 操作只讀取某一時間點的數據,即transaction開始時刻的數據,盡管后續有transaction 對數據做出了更改,當前transaction 也看不到。這有點類似於 MYSQL 在transaction開始的時刻打了一個快照。所以這種讀叫快照讀。它可重復但不是實時的。

MYSQL 中還有另外一種讀叫當前讀(CURRENT READ). 這種讀只讀取表中當前最新的已提交的數據,可以理解為是一種READ COMMITTED。當前讀一般發生在
UPDATE/DELETE/INSERT 以及 SELECT ... FOR UPDATE 和 LOCK ... IN SHARE MODE中。可以通過實驗驗證一下

transaction A

MariaDB [test]> start transaction;
Query OK, 0 rows affected (0.00 sec)

MariaDB [test]> select * from t1;
+----+--------+
| id | number |
+----+--------+
|  1 |     11 |
|  2 |     22 |
+----+--------+
2 rows in set (0.00 sec)

transaction B 插入一條數據並commit

	MariaDB [test]> select * from t1;
+----+--------+
| id | number |
+----+--------+
|  1 |     11 |
|  2 |     22 |
|  3 |     33 |
+----+--------+

transaction A 先是 select 證明select 是可重復讀,快照讀

MariaDB [test]> select * from t1;
+----+--------+
| id | number |
+----+--------+
|  1 |     11 |
|  2 |     22 |
+----+--------+
2 rows in set (0.01 sec)

然后 transaction A 再delete

MariaDB [test]> delete from t1 where id=3;
Query OK, 1 row affected (0.00 sec)

可以發現,有趣的是雖然SELECT 不到但delete操作顯示成功的刪除了該數據。這說明DELETE 可以看到其它TRANSACTION 提交的數據,是RC。
transaction A 提交后再查詢,也可以發現 數據確實被刪除了

這說明DML操作確實是RC, 為什么這些操作是當前讀我們后面再看,但至少現在可以知道,MVCC 的方式雖然能解決快照讀的不可重復與幻讀問題,但不能解決當前讀的。因為當前讀是Read committed。那么 Mysql 如何解決當前讀的幻讀問題呢? 通過間隙鎖

間隙鎖 和 next-key 鎖

用例子說明一下,假設我們有數據表如下:

其中number上有索引
start transaction;

select * from t4;
+--------+
| number |
+--------+
|      5 |
|     10 |
|     15 |
+--------+

select * from t4 where number=10 for update;
+--------+
| number |
+--------+
|     10 |
+--------+

上述語句用當前讀,讀取number=10 , 這種情況下要避免幻讀,即接下來:

  • insert 操作不會插入到 number=10 t1
  • update操作不能更新 number=10 這行
  • delete操作不能刪除number = 10這行

mysql 所用的方式很簡單,通過row鎖鎖住 number=10的行,阻止update/delete。 通過間隙鎖鎖住10 可能出現的位置

因為 number 有索引,通過索引我們知道 number = 10 可能出現的位置有兩處 5-10 和 10-15 , 所以mysql 會把這兩處鎖住, 從其它transaction 去 insert 數據到 5-10 和10 - 15 的位置會被卡住。這就是間隙鎖。我們驗證一下

MariaDB [test]> start transaction;
Query OK, 0 rows affected (0.00 sec)

MariaDB [test]> insert into t4 values(18);
Query OK, 1 row affected (0.00 sec)

MariaDB [test]> insert into t4 values(6);
^CCtrl-C -- query killed. Continuing normally.
ERROR 1317 (70100): Query execution was interrupted
MariaDB [test]> insert into t4 values(11);
^CCtrl-C -- query killed. Continuing normally.
ERROR 1317 (70100): Query execution was interrupted

除了 18 可以成功,其它兩條被卡住

但要注意的是:mysql 通過間隙鎖來鎖住目標記錄可能出現的位置,如果檢索條件有索引,可以通過索引鎖住目標位置,如果索引是unique 則不用鎖間隙,因為不會出現間隙,如果沒有索引會鎖住全表

間隙鎖加行鎖的方式來防止當前讀的幻讀,在mysql中叫next key鎖

為什么 DML/SELECT FOR UPDATE, SELECT FOR SHARE 是當前讀

SELECT FOR UPDATE/SHARE 是當前讀比較好理解。這兩種讀的目的就是鎖住記錄,不讓他人更改,所以鎖住快照沒有意義

那么DML 為什么是當前讀呢?考慮以下場景

start transaction;

select * from t4;
+--------+
| number |
+--------+
|      5 |
|     10 |
|     15 |
+--------+

select * from t4 where number=10 for update;
+--------+
| number |
+--------+
|     10 |
+--------+

這時有另外transaction 進行DML 操作,如果insert / update / delete 不是當前讀, 那么 SELECT FOR UPDATE的鎖仍然毫無意義..


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM