1. 程式人生 > >理解mysql的鎖、事務隔離級別及事務傳播行為

理解mysql的鎖、事務隔離級別及事務傳播行為

資料庫事務(Database Transaction) ,是指作為單個邏輯工作單元執行的一系列操作,要麼完全地執行,要麼完全地不執行。
ACID,是指在可靠資料庫管理系統(DBMS)中,事務(Transaction)所應該具有的四個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability)。
  • 原子性
    原子性是指事務是一個不可再分割的工作單位,事務中的操作要麼都發生,要麼都不發生。
    如,A向B轉錢,在事務中的扣款和加款兩條語句,要麼都執行,要麼都不執行。
  • 一致性
    一致性是指事務使得系統從一個一致的狀態轉換到另一個一致狀態。
    如,A和B存款總額為1000,A向B轉錢,無論失敗,最終A和B的存款總額依然為1000.
  • 隔離性
    多個事務併發訪問時,事務之間是隔離的,一個事務不應該影響其它事務執行效果。
    資料庫多個事務之間操作可能出現的問題以及事務隔離級別是這篇文章介紹的重點。
  • 永續性
    永續性,意味著在事務完成以後,該事務所對資料庫所作的更改便持久的儲存在資料庫之中,並不會被回滾。
    即使出現了任何事故比如斷電等,事務一旦提交,則持久化儲存在資料庫中。

事務的併發問題

  • 贓讀(Dirty Read)
    一個事務讀取到了另外一個事務沒有提交的資料
    事務A讀取了事務B更新的資料,然後B回滾操作,那麼A讀取到的資料是髒資料
  • 不可重複讀(Nonrepeatable Read)
    在同一事務中,兩次讀取同一資料,得到內容不同
    事務A多次讀取同一資料,事務B在事務A多次讀取的過程中,對資料作了更新並提交,導致事務A多次讀取同一資料時,結果不一致
  • 幻讀(Phantom Read)
    同一事務中,用同樣的操作讀取兩次,得到的記錄數不相同
    系統管理員A將資料庫中所有學生的成績從具體分數改為ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣

MySql的四中隔離級別

  • Read Uncommitted(讀取未提交內容)
    在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。
    讀取未提交的資料,則會發生贓讀
  • Read Committed(讀取提交內容)
    一個事務只能看見已經提交事務所做的改變。這是大多數資料庫系統的預設隔離級別,但非MySql
    一個事務多次讀取的過程中,另一個事務可能對同一條資料做修改並提交,導致前一個事務多次讀取到的資料不一致,則會發生不可重複讀
  • Repeatable Read(可重讀)
    它確保同一事務的多個例項在併發讀取資料時,會看到同樣的資料行。這是MySql的預設隔離級別
    但,此級別依然會發生幻讀
  • Serializable(可序列化)
    它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題
隔離級別 讀資料一致性 贓讀 不可重複讀 幻讀
Read Uncommitted 最低級別,只能保證不讀取物理上損壞的資料
Read Committed 語句級 ×
Repeatable Read 事務級 × ×
Serializable 最高級別,事務級 × × ×

低級別的隔離一般支援更高的併發處理,並擁有更低的系統開銷。高級別的隔離可靠性較高,但系統開銷較大。

隔離級別測試

建立資料庫

CREATE DATABASE IF NOT EXISTS txdemo DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

建立測試表

CREATE TABLE `user` (
    `id`    BIGINT NOT NULL AUTO_INCREMENT,
    `name`  VARCHAR(32) NOT NULL DEFAULT '',
    `age`   INT(16) NOT NULL DEFAULT '30',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入測試資料

INSERT INTO user (name, age) VALUES ('manerfan', 30), ('Abel', 28), ('Cherry', 42);

Read Uncommitted

Read Uncommitted

Step 1: 設定A的隔離級別為Read Uncommitted,開啟事務並讀取資料
Step 2: B開啟事務,修改資料,但不提交
Step 3: A讀取資料,發現資料已變
Step 4: B回滾,但不提交
Step 5: A讀取資料,發現資料恢復

A事務中可以讀取到B事務未修改的資料,發生贓讀

Read Committed

Read Committed

Step 1: 設定A的隔離級別為Read Committed,開啟事務並讀取資料
Step 2: B開啟事務,修改資料,但不提交
Step 3: A讀取資料,發現資料未變
Step 4: B提交事務
Step 5: A讀取資料,發現資料改變

已提交讀隔離級別解決了髒讀的問題,但是出現了不可重複讀的問題,即事務A在兩次查詢的資料不一致,因為在兩次查詢之間事務B更新了一條資料。

Repeatable Read

Repeatable Read

Step 1: 設定A的隔離級別為Repeatable Read,開啟事務並讀取資料
Step 2: B開啟事務,修改資料,但不提交
Step 3: A讀取資料,發現資料未變
Step 4: B提交事務
Step 5: A讀取資料,發現資料依然未變,解決了不可重複讀
Step 6: B插入新資料,並提交
Step 7: A讀取資料,發現資料還是未變,出現幻讀
Step 8: A提交事務,再次讀取資料,發現資料改變

Repeatable Read隔離級別只允許讀取已提交記錄,而且在一個事務兩次讀取一個記錄期間,其他事務的更新不會影響該事務。但該事務不要求與其他事務可序列化,可能會發生幻讀。

Serializable

clipboard.png

Step 1: 設定A的隔離級別為Serializable,開啟事務並讀取資料
Step 2: B開啟事務,修改資料,B事務阻塞,A的事務尚未提交,只能等待

clipboard.png

Step 3: A事務提交,B事務插入成功,但B事務不提交

clipboard.png

Step 4: A事務查詢,發現A事務阻塞,B的事務尚未提交,只能等待

clipboard.png

Step 5: B事務提交,A事務查詢成功

Serializable隔離級別完全鎖定欄位,若一個事務來查詢同一份資料就必須等待,直到前一個事務完成並解除鎖定為止。

樂觀鎖與悲觀鎖

樂觀鎖(Optimistic Lock),是指操作資料庫時(更新操作),總是認為這次的操作不會導致衝突,不到萬不得已不去拿鎖,在更新時採取判斷是否衝突,適用於讀操作遠多於更新操作的情況。
樂觀鎖並沒有被資料庫實現,需要自行實現,通常的實現方式為在表中增加版本version欄位,更新時判斷庫中version與取出時的version值是否相等,若相等則執行更新並將version加1,若不相等則說明資料被其他執行緒(程序)修改,放棄修改。

select (age, version) from user where id = #{id};
# 其他操作
update user set age = 18, version = version + 1 where id = #{id} and version = #{version} 

悲觀鎖(Pessimistic Lock),是指操作資料庫時(更新操作),總是認為這次的操作會導致衝突,每次都要通過獲取鎖才能進行資料操作,因此要先確保獲取鎖成功再進行業務操作。
悲觀鎖需要資料庫自身提供支援,MySql提供了共享鎖和排他鎖來實現對資料行的鎖定,兩種鎖的介紹如下介紹。

MySql InnoDB引擎 鎖

InnoDB實現了以下兩種型別的行鎖:

  • 共享鎖 (S): 允許一個事務去讀一行,阻止其他事務獲得相同資料集的排他鎖
  • 排他鎖 (X): 允許獲得排他鎖的事務更新資料,阻止其他事務取得相同資料集的共享讀鎖和排他寫鎖
  X S
X 衝突 衝突
S 衝突 相容

對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及資料集加排他鎖(X)
對於普通SELECT語句,InnoDB不會加任何鎖

事務可以通過以下語句顯式地給記錄集加共享鎖或排他鎖:

  • 共享鎖: SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE,等同讀鎖
  • 排他鎖: SELECT * FROM table_name WHERE ... FOR UPDATE,等同寫鎖

SELECT ... IN SHARE MODE獲得共享鎖,主要用在需要資料依存關係時來確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操作。但是如果當前事務也需要對該記錄進行更新操作,則很有可能造成死鎖,對於鎖定行記錄後需要進行更新操作的應用,應該使用SELECT... FOR UPDATE方式獲得排他鎖。

SELECT ... IN SHARE MODE模式

clipboard.png

Step 1: A查詢id為1的資料並加共享鎖
Step 2: B查詢id為2的資料並加共享鎖
Step 3: A更新id為2的資料,由於共享鎖與排他鎖衝突而阻塞
Step 4: B更新id為1的資料,由於A與B互相等待對方釋放鎖而丟擲死鎖異常

SELECT... FOR UPDATE模式

clipboard.png

Step 1: A查詢id為1的資料並加排他鎖
Step 2: B查詢id為1的資料不加任何鎖,成功
Step 3: B查詢id為1的資料並加排他鎖阻塞
Step 4: A更新id為1的資料(資料庫自動加排他鎖),成功

clipboard.png

Step 5: A提交事務,B獲取鎖查詢成功

InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,後者是通過在資料塊中對相應資料行加鎖來實現的。InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖衝突,從而影響併發效能。

clipboard.png

Step 1: A查詢id為1的資料並加排他鎖
Step 2: B查詢id為2的資料並加排他鎖成功
Step 3: A開啟新的事物,查詢age為32的資料並加排他鎖
Step 4: B開啟新的事物,查詢age為92的資料並加排他鎖阻塞,B與A查詢的資料並不是同一行,但B阻塞,說明A的排他鎖為表鎖非行鎖

Spring Transaction的事務傳播行為

Spring的 @Transactional 提供了設定事務隔離級別及事務傳播行為的方式

Isolation中定義了DEFAULT及以上介紹的四中隔離級別,這裡不再贅述

package org.springframework.transaction.annotation;
public enum Isolation {
    DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE;
}

Propagation中定義了其中傳播行為

package org.springframework.transaction.annotation;
public enum Propagation {
    REQUIRED, SUPPORTS, MANDATORY, REQUIRES_NEW, NOT_SUPPORTED, NEVER, NESTED;
}
  • PROPAGATION_REQUIRED:如果當前沒有事務,就建立一個新事務,如果當前存在事務,就加入該事務。
  • PROPAGATION_SUPPORTS:支援當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就以非事務執行。
  • PROPAGATION_MANDATORY:支援當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就丟擲異常。
  • PROPAGATION_REQUIRES_NEW:建立新事務,無論當前存不存在事務,都建立新事務。
  • PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
  • PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。
  • PROPAGATION_NESTED:如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。