從新理解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 最高級別,事務級 × × ×

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

隔離級別測試

建立數據庫數據庫

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事務未修改的數據,發生贓讀spa

Read Committed

Read Committed

Step 1: 設置A的隔離級別爲Read Committed,開啓事務並讀取數據
Step 2: B開啓事務,修改數據,但不提交
Step 3: A讀取數據,發現數據未變
Step 4: B提交事務
Step 5: A讀取數據,發現數據改變線程

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

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相似的操做。
相關文章
相關標籤/搜索