1. 程式人生 > 其它 >資料庫原理二---MySQL事務與鎖

資料庫原理二---MySQL事務與鎖

資料庫事務的四大特性

  • 原子性A
    事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用

  • 一致性C
    執行事務前後,資料保持一致,多個事務對同一個資料讀取的結果是相同的

  • 隔離性I
    併發訪問資料庫時,一個使用者的事務不被其他事務所幹擾,各併發事務之間資料庫是獨立的

  • 永續性D一個事務被提交之後。它對資料庫中資料的改變是持久的,即使資料庫發生故障也不應該對其有任何影響

原子性和永續性定義了事務的邊界,行為的開始和結束,一致性和隔離性即是對事務中間狀態的管理。

ACID的核心是C,大家都是為得到C而提出的不同緯度的限制和規範
A確定一個功能的完整性,D對狀態負責,I作為C的等級係數,不同的I策略會出現不同的C。
隔離性I的設定就是對一致性不同程度的破壞,事實上,如果我們順序對資料進行讀寫,ACD是完全可以保證的,但這樣效率會非常低下。
選擇合適的隔離策略是為了在一致性和效能之間平衡,取得最好的綜合表現。

髒讀、幻讀、不可重複讀

  • 髒讀(Drity Read)
    某個事務已更新一份資料,另一個事務在此時讀取了同一份資料,由於某些原因,前一個RollBack了操作,則後一個事務所讀取的資料就會是不正確的。

  • 不可重複讀(Non-repeatable read)
    在一個事務的兩次查詢之中資料不一致,這可能是兩次查詢過程中間插入了一個事務更新的原有的資料。

  • 幻讀(Phantom Read)
    在一個事務的兩次查詢中資料筆數不一致,例如有一個事務查詢了幾列(Row)資料,而另一個事務卻在此時插入了新的幾列資料,先前的事務在接下來的查詢中,就會發現有幾列資料是它先前所沒有的。

幻讀,並不是說兩次讀取獲取的結果集不同,幻讀側重的方面是某一次的 select 操作得到的結果所表徵的資料狀態無法支撐後續的業務操作。
更為具體一些:select 某記錄是否存在,不存在,準備插入此記錄,但執行 insert 時發現此記錄已存在,無法插入,此時就發生了幻讀。

事務的隔離級別

為了達到事務的四大特性,資料庫定義了4種不同的事務隔離級別

  • READ-UNCOMMITTED(讀取未提交)
    最低的隔離級別,允許讀取尚未提交的資料變更,可能會導致髒讀、幻讀或不可重複讀。

  • READ-COMMITTED(讀取已提交)
    允許讀取併發事務已經提交的資料,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。

  • REPEATABLE-READ(可重複讀)
    對同一欄位的多次讀取結果都是一致的,除非資料是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。

  • SERIALIZABLE(可序列化)
    最高的隔離級別,完全服從ACID的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。

Mysql 預設採用的 REPEATABLE_READ隔離級別 Oracle 預設採用的 READ_COMMITTED隔離級別

MySQL的鎖機制

MySQL資料庫鎖機制(樂觀鎖,悲觀鎖)

資料庫管理系統(DBMS)中的併發控制的任務是確保在多個事務同時存取資料庫中同一資料時不破壞事務的隔離性和統一性以及資料庫的統一性。
樂觀併發控制(樂觀鎖)和悲觀併發控制(悲觀鎖)是併發控制主要採用的技術手段
要把樂觀併發控制和悲觀併發控制狹義的理解為DBMS中的概念,更不要把他們和資料中提供的鎖機制(行鎖、表鎖、頁鎖、排他鎖、共享鎖)混為一談。

悲觀鎖

悲觀併發控制(悲觀鎖)是一種併發控制的方法。它可以阻止一個事務以影響其他使用者的方式來修改資料。
如果一個事務執行的操作對其行資料應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖衝突的操作。
悲觀鎖主要用於資料爭用激烈的環境。

悲觀鎖的具體流程
在對任意記錄進行修改前,先嚐試為該記錄加上排他鎖。
如果加鎖失敗,說明該記錄正在被修改,那麼當前查詢可能要等待或者丟擲異常。具體響應方式由開發者根據實際需要決定。
如果成功加鎖,那麼就可以對記錄做修改,事務完成後就會解鎖了。
期間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或者直接丟擲異常。

悲觀鎖的優點和不足
悲觀鎖實際上是採取了“先取鎖在訪問”的策略,為資料的處理安全提供了保證。
但是在效率方面,由於額外的加鎖機制產生了額外的開銷,並且增加了死鎖的機會。
並且降低了併發性;當一個事物所以一行資料的時候,其他事物必須等待該事務提交之後,才能操作這行資料。

樂觀鎖

樂觀併發控制(樂觀鎖)是一種併發控制的方法。它假設多使用者併發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分資料。
在提交資料更新之前,每個事務會先檢查在該事務讀取資料後,有沒有其他事務又修改了該資料。如果其他事務有更新的話,正在提交的事務會進行回滾。

樂觀鎖相對悲觀鎖而言,樂觀鎖假設認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。
在對資料庫進行處理的時候,樂觀鎖並不會使用資料庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄資料版本。

資料版本,為資料增加的一個版本標識。當讀取資料時,將版本標識的值一同讀出,資料每更新一次同時對版本標識進行更新。
當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的版本標識進行比對,如果資料庫表當前版本號與第一次取出來的版本標識值相等,則予以更新,否則認為是過期資料。

樂觀鎖的優點和不足
樂觀併發控制相信事務之間的資料競爭(data race)的概率是比較小的,因此儘可能直接做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。
但如果直接簡單這麼做,還是有可能會遇到不可預期的結果,例如兩個事務都讀取了資料庫的某一行,經過修改以後寫回資料庫,這時就遇到了問題。

MySQL資料鎖機制(行鎖,頁鎖,表鎖)

MySQL資料鎖按照鎖的粒度劃分可分為:行鎖、表鎖、頁鎖

  • 行鎖
    行級鎖是MySQL中鎖定粒度最細的一種鎖,表示只針對當前操作的行進行加鎖。
    行級鎖能大大減少資料庫操作的衝突。其加鎖粒度最小,但加鎖的開銷也最大。有可能會出現死鎖的情況。
    行級鎖按照使用方式分為共享鎖和排他鎖。

    • 共享鎖
      共享鎖允許一個事務讀資料,不允許修改資料,如果其他事務要對該行加鎖,只能加共享鎖。即共享鎖允許多個執行緒同時獲取一個鎖,一個鎖可以同時被多個執行緒擁有。
    • 排他鎖
      排他鎖是修改資料時加的鎖,可以讀取和修改資料,一旦一個事務對該行資料加鎖,其他事務將不能再對該資料加任務鎖。即一個鎖在某一時刻只能被一個執行緒佔有,其他執行緒必須等待鎖被釋放之後才可能獲取到鎖。
  • 表鎖
    表級鎖是MySQL鎖中粒度最大的一種鎖,表示當前的操作對整張表加鎖,資源開銷比行鎖少,不會出現死鎖的情況,但是發生鎖衝突的概率很大。
    被大部分MySQL引擎支援,MyISAM和InnoDB都支援表級鎖,但是InnoDB預設是行級鎖。

  • 頁鎖
    頁級鎖是MySQL中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但衝突多,行級鎖衝突少,但速度慢。
    所以取了折中的頁級,一次鎖定相鄰的一組記錄。BDB支援頁級鎖。

隔離級別與鎖的關係

在Read Uncommitted級別下,讀取資料不需要加共享鎖,這樣就不會跟被修改的資料上的排他鎖衝突

在Read Committed級別下,讀操作需要加共享鎖,但是在語句執行完以後釋放共享鎖;
在悲觀鎖的情況下,由於加了共享鎖,所以事務1對資料A修改時,事務2不會讀入資料A,不會出現髒讀。

在Repeatable Read級別下,讀操作需要加共享鎖,但是在事務提交之前並不釋放共享鎖,也就是必須等待事務執行完畢以後才釋放共享鎖。
在悲觀鎖的條件下,事務依次執行,避免了在單條事務執行期間資料發生改變,不會出現不可重複讀

SERIALIZABLE 是限制性最強的隔離級別,因為該級別鎖定整個範圍的鍵,並一直持有鎖,直到事務完成。

樂觀鎖的情況下,在下文MVCC中補充。

當前讀&快照讀

  • 當前讀
    讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。

  • 快照讀
    像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是序列級別,序列級別的快照讀會退化成當前讀,之所以出現快照讀的情況是基於提高併發效能的考慮。
    快照讀的實現是基於MVCC

MVCC

MVCC(Multiversion concurrency control),多版本併發控制,提供併發訪問資料庫時,對事務內讀取的到的記憶體做處理,用來避免寫操作堵塞讀操作的併發問題。
一個支援MVCC的資料庫,在更新某些資料時,並非使用新資料覆蓋舊資料,而是標記舊資料是過時的,同時在其他地方新增一個數據版本。因此,同一份資料有多個版本儲存,但只有一個是最新的。

MVCC提供了 時間一致性的 處理思路,在MVCC下讀事務時,通常使用一個時間戳或者事務ID來確定訪問哪個狀態的資料庫及哪些版本的資料。讀事務跟寫事務彼此是隔離開來的,彼此之間不會影響。假設同一份資料,既有讀事務訪問,又有寫事務操作,實際上,寫事務會新建一個新的資料版本,而讀事務訪問的是舊的資料版本,直到寫事務提交,讀事務才會訪問到這個新的資料版本。

MVCC有兩種實現方式
第一種實現方式是將資料記錄的多個版本儲存在資料庫中,當這些不同版本資料不再需要時,垃圾收集器回收這些記錄。這個方式被PostgreSQL和Firebird/Interbase採用,SQL Server使用的類似機制,所不同的是舊版本資料不是儲存在資料庫中,而儲存在不同於主資料庫的另外一個數據庫tempdb中。
第二種實現方式只在資料庫儲存最新版本的資料,但是會在使用undo時動態重構舊版本資料,這種方式被Oracle和MySQL/InnoDB使用。

MVCC在MySQL中只在 Read Committed 和 Repeatable Read兩個隔離級別下工作。其他兩個隔離級別和MVCC不相容,Read Uncommitted總是讀取最新的記錄行,不需要MVCC的支援;Serializable 則會對所有讀取的記錄行都加鎖,單靠MVCC無法完成。

InnoDB的MVCC實現機制

InnoDB的MVCC實現,是通過儲存資料在某個時間點的快照來實現的。一個事務,不管其執行多長時間,其內部看到的資料是一致的。也就是事務在執行的過程中不會相互影響。
InnoDB的MVCC,通過在每行記錄後面儲存兩個隱藏的列來實現:一個儲存了行的建立時間,一個儲存行的過期時間(刪除時間),當然,這裡的時間並不是時間戳,而是系統版本號,每開始一個新的事務,系統版本號就會遞增。

RC , RR 級別下的 InnoDB MVVC實現的快照讀有什麼不同

在 RR 級別下的某個事務的對某條記錄的第一次快照讀會建立一個快照及 Read View, 將當前系統活躍的其他事務記錄起來,此後在呼叫快照讀的時候,還是使用的是同一個 Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那麼之後的快照讀使用的都是同一個 Read View,所以對之後的修改不可見;
即 RR 級別下,快照讀生成 Read View 時,Read View 會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於Read View建立的事務所做的修改均是可見。(所以在RR級別下,不會出現不可重複讀)
而在 RC 級別下的,事務中,每次快照讀都會新生成一個快照和 Read View , 這就是我們在 RC 級別下的事務中可以看到別的事務提交的更新的原因(所以在RC級別下,也可能出現不可重複讀)