MySQL 四種事務隔離級別 + 鎖

一、事務的基本要素(ACID)

  1、原子性(Atomicity):事務開始後所有操作,要麼全部做完,要麼全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。

   2、一致性(Consistency):事務開始前和結束後,數據庫的完整性約束沒有被破壞 。比如A向B轉賬,不可能A扣了錢,B卻沒收到。

   3、隔離性(Isolation):同一時間,只允許一個事務請求同一數據,不同的事務之間彼此沒有任何干擾。比如A正在從一張銀行卡中取錢,在A取錢的過程結束前,B不能向這張卡轉賬。

   4、持久性(Durability):事務完成後,事務對數據庫的所有更新將被保存到數據庫,不能回滾。

二、事務的併發問題

  1、髒讀:事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據

  2、不可重複讀(修改):事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果 不一致。【需要行鎖就可以解決】

  3、幻讀(新增/刪除):系統管理員A將數據庫中所有學生的成績從具體分數改爲ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。【需要表鎖才能解決】

  小結:不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表

 

三、MySQL事務隔離級別,其實很好記,其實就是讀的範圍不同而已,看後面的標註就很好記了

1.可以讀到未提交的數據

2.可以讀到已提交的數據

3.可以重複的讀同一條數據沒問題

4.串行化

事務隔離級別 髒讀 不可重複讀 幻讀
讀未提交(read-uncommitted)可以讀到未提交的數據
讀已提交(read-committed)可以讀到已提交的數據
可重複讀(repeatable-read)
串行化(serializable)

併發性能也是從上到下,越來越差,因爲越來越嚴格
 

mysql默認的事務隔離級別爲repeatable-read

show VARIABLES like '%ISOLATION%'
transaction_isolation	REPEATABLE-READ

四、用例子說明各個隔離級別的情況

 1、讀未提交:

    (1)打開一個客戶端A,並設置當前事務模式爲read uncommitted(未提交讀),查詢表account的初始值:

 

    (2)在客戶端A的事務提交之前,打開另一個客戶端B,更新表account:

 

 

    (3)這時,雖然客戶端B的事務還沒提交,但是客戶端A就可以查詢到B已經更新的數據:

 

    (4)一旦客戶端B的事務因爲某種原因回滾,所有的操作都將會被撤銷,那客戶端A查詢到的數據其實就是髒數據:

 

     (5)在客戶端A執行更新語句update account set balance = balance - 50 where id =1,lilei的balance沒有變成350,居然是400,是不是很奇怪,數據不一致啊,如果你這麼想就太天真 了,在應用程序中,我們會用400-50=350,並不知道其他會話回滾了,要想解決這個問題可以採用讀已提交的隔離級別

 

  2、讀已提交

    (1)打開一個客戶端A,並設置當前事務模式爲read committed(未提交讀),查詢表account的所有記錄:

 

    (2)在客戶端A的事務提交之前,打開另一個客戶端B,更新表account:

 

    (3)這時,客戶端B的事務還沒提交,客戶端A不能查詢到B已經更新的數據,解決了髒讀問題:

 

    (4)客戶端B的事務提交

    (5)客戶端A執行與上一步相同的查詢,結果 與上一步不一致,即產生了不可重複讀的問題

 

   3、可重複讀

     (1)打開一個客戶端A,並設置當前事務模式爲repeatable read,查詢表account的所有記錄

    (2)在客戶端A的事務提交之前,打開另一個客戶端B,更新表account並提交

    (3)在客戶端A查詢表account的所有記錄,與步驟(1)查詢結果一致,沒有出現不可重複讀的問題

    (4)在客戶端A,接着執行update balance = balance - 50 where id = 1,balance沒有變成400-50=350,lilei的balance值用的是步驟(2)中的350來算的,所以是300,數據的一致性倒是沒有被破壞。

可重複讀的隔離級別下使用了MVCC機制

select操作不會更新版本號,是快照讀(歷史版本);

insert、update和delete會更新版本號,是當前讀(當前版本)。

(5)重新打開客戶端B,插入一條新數據後提交

(6)在客戶端A查詢表account的所有記錄,沒有 查出 新增數據,所以沒有出現幻讀

下面來一個更直觀的例子:

  1. 準備兩個終端,在此命名爲 mysql 終端 1 和 mysql 終端 2,準備一張測試表 test 並調整隔離級別爲 REPEATABLE READ,任意一個終端執行即可。
    SET @@session.transaction_isolation = 'REPEATABLE-READ';
    
    create database test;
    
    use test;
    
    create table test(id int primary key,name varchar(20));

     

  2. 登錄 mysql 終端 1,開啓一個事務。
    begin;
    
    select * from test; -- 無記錄

     

  3. 登錄 mysql 終端 2,開啓一個事務。
    begin;
    
    select * from test; -- 無記錄

     

  4. 切換到 mysql 終端 1,增加一條記錄並提交。
    insert into test(id,name) values(1,'a');
    
    commit;

     

  5. 切換到 msyql 終端 2。
    	
    select * from test; --此時查詢還是無記錄

    通過這一步可以證明,在該隔離級別下已經讀取不到別的已提交的事務,如果想看到 mysql 終端 1 提交的事務,在 mysql 終端 2 將當前事務提交後再次查詢就可以讀取到 mysql 終端 1 提交的事務。我們接着實驗,看看在該隔離級別下是否會存在別的問題。

  6. 此時接着在 mysql 終端 2 插入一條數據。
    insert into test(id,name) values(1,'b'); 
    -- 此時報主鍵衝突的錯誤

     

也許到這裏您心裏可能會有疑問,

明明在第 5 步沒有數據,爲什麼在這裏會報錯呢?其實這就是該隔離級別下可能產生的問題,MySQL 稱之爲幻讀。

注意我在這裏強調的是 MySQL 數據庫,Oracle 數據庫對於幻讀的定義可能有所不同

補充:

  1、事務隔離級別爲讀提交時,寫數據只會鎖住相應的行

  2、事務隔離級別爲可重複讀時,如果檢索條件有索引(包括主鍵索引)的時候,默認加鎖方式是next-key 鎖;如果檢索條件沒有索引,更新數據時會鎖住整張表。一個間隙被事務加了鎖,其他事務是不能在這個間隙插入記錄的,這樣可以防止幻讀。

  3、事務隔離級別爲串行化時,讀寫數據都會鎖住整張表

   4、隔離級別越高,越能保證數據的完整性和一致性,但是對併發性能的影響也越大。


鎖:[二者關係:鎖是實現事務隔離級別的手段]

鎖也是數據庫管理系統區別文件系統的重要特徵之一。

鎖機制使得在對數據庫進行併發訪問時,可以保障數據的完整性和一致性。

對於鎖的實現,各個數據庫廠商的實現方法都會有所不同。

本文討論 MySQL 中的 InnoDB 引擎的鎖

鎖的類型

InnoDB 實現了兩種類型的行級鎖:

  • 共享鎖(也稱爲 S 鎖)【讀鎖】:允許事務讀取一行數據。

    可以使用 SQL 語句 select * from tableName where … lock in share mode; 手動加 S 鎖。

  • 獨佔鎖(也稱爲 X 鎖)【寫鎖】:允許事務刪除或更新一行數據。

    可以使用 SQL 語句 select * from tableName where … for update; 手動加 X 鎖。也就是說這個語句對應的鎖就相當於update帶來的效果 

  • 一旦一個事務獲取了這個鎖,其他的事務是沒法在這些數據上執行 for update

  • 雖然我們平時幾乎不會使用select for update進行查詢,但是,要記住,update語句之前就是要進行一次for update的select查詢的!

  • 【結論】: 

    lock in share mode適用於兩張表存在業務關係時的一致性要求,

  • for  update適用於操作同一張表時的一致性要求。

  • 注:普通 select 語句默認不加鎖,而CUD操作默認加排他鎖。

行鎖的算法

InnoDB 存儲引擎使用三種行鎖的算法用來滿足相關事務隔離級別的要求。

  • Record Locks

    該鎖爲索引記錄上的鎖,如果表中沒有定義索引,InnoDB 會默認爲該表創建一個隱藏的聚簇索引,並使用該索引鎖定記錄。

  • Gap Locks

    該鎖會鎖定一個範圍,但是不括記錄本身。可以通過修改隔離級別爲 READ COMMITTED 或者配置 innodb_locks_unsafe_for_binlog 參數爲 ON

  • Next-key Locks

    該鎖就是 Record Locks 和 Gap Locks 的組合,即鎖定一個範圍並且鎖定該記錄本身。InnoDB 使用 Next-key Locks 解決幻讀問題。需要注意的是,如果索引有唯一屬性,則 InnnoDB 會自動將 Next-key Locks 降級爲 Record Locks。舉個例子,如果一個索引有 1, 3, 5 三個值,則該索引鎖定的區間爲 (-∞,1], (1,3], (3,5], (5,+ ∞)

死鎖

死鎖是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程

InnoDB 引擎採取的是 wait-for graph 等待圖的方法來自動檢測死鎖,如果發現死鎖會自動回滾一個事務。

  1. 準備兩個終端,在此命名爲 mysql 終端 1 和 mysql 終端 2,分別登入 mysql,再準備一張測試表 test 寫入兩條測試數據,並調整隔離級別爲 SERIALIZABLE,任意一個終端執行即可。
    SET @@session.transaction_isolation = 'REPEATABLE-READ';
    
    create database test;
    
    use test;
    
    create table test(id int primary key);
    
    insert into test(id) values(1),(2);

     

  2. 登錄 mysql 終端 1,開啓一個事務,手動給 ID 爲 1 的記錄加 X 鎖。
    begin;
    
    select * from test where id = 1 for update;

     

  3. 登錄 mysql 終端 2,開啓一個事務,手動給 ID 爲 2 的記錄加 X 鎖。
    begin;
    
    select * from test where id = 2 for update;

     

  4. 切換到 mysql 終端 1,手動給 ID 爲 2 的記錄加 X 鎖,此時會一直卡住,因爲此時在等待第 3 步中 X 鎖的釋放,直到超時,超時時間由 innodb_lock_wait_timeout 控制。
    select * from test where id = 2 for update;
     

此時,通過 

--查看數據庫死鎖
show engine innodb status

\G 命令可以看到 LATEST DETECTED DEADLOCK 相關信息,即表明有死鎖發生;或者通過配置 innodb_print_all_deadlocks(MySQL 5.6.2 版本開始提供)參數爲 ON 將死鎖相關信息打印到 MySQL 的錯誤日誌。

 

鎖的優化建議

鎖如果利用不好,會給業務造成大量的卡頓現象,在瞭解了鎖相關的一些知識點後,我們可以有意識的去避免鎖帶來的一些問題。

  1. 合理設計索引,讓 InnoDB 在索引鍵上面加鎖的時候儘可能準確,儘可能的縮小鎖定範圍,避免造成不必要的鎖定而影響其他 Query 的執行。
  2. 儘可能減少基於範圍的數據檢索過濾條件,避免因爲間隙鎖帶來的負面影響而鎖定了不該鎖定的記錄。
  3. 儘量控制事務的大小,減少鎖定的資源量和鎖定時間長度。
  4. 在業務環境允許的情況下,儘量使用較低級別的事務隔離,以減少 MySQL 因爲實現事務隔離級別所帶來的附加成本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章