從新學習MySQL數據庫6:淺談MySQL的中事務與鎖

『淺入深出』MySQL 中事務的實現html

在關係型數據庫中,事務的重要性不言而喻,只要對數據庫稍有了解的人都知道事務具備 ACID 四個基本屬性,而咱們不知道的可能就是數據庫是如何實現這四個屬性的;在這篇文章中,咱們將對事務的實現進行分析,嘗試理解數據庫是如何實現事務的,固然咱們也會在文章中簡單對 MySQL 中對 ACID 的實現進行簡單的介紹。前端

事務其實就是併發控制的基本單位;相信咱們都知道,事務是一個序列操做,其中的操做要麼都執行,要麼都不執行,它是一個不可分割的工做單位;數據庫事務的 ACID 四大特性是事務的基礎,瞭解了 ACID 是如何實現的,咱們也就清除了事務的實現,接下來咱們將依次介紹數據庫是如何實現這四個特性的。mysql

原子性程序員

在學習事務時,常常有人會告訴你,事務就是一系列的操做,要麼所有都執行,要都不執行,這其實就是對事務原子性的刻畫;雖然事務具備原子性,可是原子性並非只與事務有關係,它的身影在不少地方都會出現。面試

因爲操做並不具備原子性,而且能夠再分爲多個操做,當這些操做出現錯誤或拋出異常時,整個操做就可能不會繼續執行下去,而已經進行的操做形成的反作用就可能形成數據更新的丟失或者錯誤。算法

事務其實和一個操做沒有什麼太大的區別,它是一系列的數據庫操做(能夠理解爲 SQL)的集合,若是事務不具有原子性,那麼就沒辦法保證同一個事務中的全部操做都被執行或者未被執行了,整個數據庫系統就既不可用也不可信。sql

回滾日誌數據庫

想要保證事務的原子性,就須要在異常發生時,對已經執行的操做進行回滾,而在 MySQL 中,恢復機制是經過回滾日誌(undo log)實現的,全部事務進行的修改都會先記錄到這個回滾日誌中,而後在對數據庫中的對應行進行寫入。編程

這個過程其實很是好理解,爲了可以在發生錯誤時撤銷以前的所有操做,確定是須要將以前的操做都記錄下來的,這樣在發生錯誤時才能夠回滾。緩存

回滾日誌除了可以在發生錯誤或者用戶執行 ROLLBACK 時提供回滾相關的信息,它還可以在整個系統發生崩潰、數據庫進程直接被殺死後,當用戶再次啓動數據庫進程時,還可以馬上經過查詢回滾日誌將以前未完成的事務進行回滾,這也就須要回滾日誌必須先於數據持久化到磁盤上,是咱們須要先寫日誌後寫數據庫的主要緣由。

回滾日誌並不能將數據庫物理地恢復到執行語句或者事務以前的樣子;它是邏輯日誌,當回滾日誌被使用時,它只會按照日誌邏輯地將數據庫中的修改撤銷掉看,能夠理解爲,咱們在事務中使用的每一條 INSERT 都對應了一條 DELETE,每一條 UPDATE 也都對應一條相反的 UPDATE 語句。

在這裏,咱們並不會介紹回滾日誌的格式以及它是如何被管理的,本文重點關注在它究竟是一個什麼樣的東西,究竟解決了、如何解決了什麼樣的問題,若是想要了解具體實現細節的讀者,相信網絡上關於回滾日誌的文章必定很多。

事務的狀態

由於事務具備原子性,因此從遠處看的話,事務就是密不可分的一個總體,事務的狀態也只有三種:Active、Commited 和 Failed,事務要不就在執行中,要否則就是成功或者失敗的狀態:

可是若是放大來看,咱們會發現事務再也不是原子的,其中包括了不少中間狀態,好比部分提交,事務的狀態圖也變得愈來愈複雜。

事務的狀態圖以及狀態的描述取自 Database System Concepts 一書中第 14 章的內容。

  • Active:事務的初始狀態,表示事務正在執行;

  • Partially Commited:在最後一條語句執行以後;

  • Failed:發現事務沒法正常執行以後;

  • Aborted:事務被回滾而且數據庫恢復到了事務進行以前的狀態以後;

  • Commited:成功執行整個事務;

雖然在發生錯誤時,整個數據庫的狀態能夠恢復,可是若是咱們在事務中執行了諸如:向標準輸出打印日誌、向外界發出郵件、沒有經過數據庫修改了磁盤上的內容甚至在事務執行期間發生了轉帳匯款,那麼這些操做做爲可見的外部輸出都是沒有辦法回滾的;這些問題都是由應用開發者解決和負責的,在絕大多數狀況下,咱們都須要在整個事務提交後,再觸發相似的沒法回滾的操做

以訂票爲例,哪怕咱們在整個事務結束以後,才向第三方發起請求,因爲向第三方請求並獲取結果是一個須要較長事件的操做,若是在事務剛剛提交時,數據庫或者服務器發生了崩潰,那麼咱們就很是有可能丟失發起請求這一過程,這就形成了很是嚴重的問題;而這一點就不是數據庫所能保證的,開發者須要在適當的時候查看請求是否被髮起、結果是成功仍是失敗。

並行事務的原子性

到目前爲止,全部的事務都只是串行執行的,一直都沒有考慮過並行執行的問題;然而在實際工做中,並行執行的事務纔是常態,然而並行任務下,卻可能出現很是複雜的問題:

當 Transaction1 在執行的過程當中對 id = 1 的用戶進行了讀寫,可是沒有將修改的內容進行提交或者回滾,在這時 Transaction2 對一樣的數據進行了讀操做並提交了事務;也就是說 Transaction2 是依賴於 Transaction1 的,當 Transaction1 因爲一些錯誤須要回滾時,由於要保證事務的原子性,須要對 Transaction2 進行回滾,可是因爲咱們已經提交了 Transaction2,因此咱們已經沒有辦法進行回滾操做,在這種問題下咱們就發生了問題,Database System Concepts 一書中將這種現象稱爲不可恢復安排(Nonrecoverable Schedule),那什麼狀況下是能夠恢復的呢?

A recoverable schedule is one where, for each pair of transactions Ti and Tj such that Tj reads a data item previously written by Ti , the commit operation of Ti appears before the commit operation of Tj .

簡單理解一下,若是 Transaction2 依賴於事務 Transaction1,那麼事務 Transaction1 必須在 Transaction2 提交以前完成提交的操做:

然而這樣還不算完,當事務的數量逐漸增多時,整個恢復流程也會變得愈來愈複雜,若是咱們想要從事務發生的錯誤中恢復,也不是一件那麼容易的事情。

在上圖所示的一次事件中,Transaction2 依賴於 Transaction1,而 Transaction3 又依賴於 Transaction1,當 Transaction1 因爲執行出現問題發生回滾時,爲了保證事務的原子性,就會將 Transaction2 和 Transaction3 中的工做所有回滾,這種狀況也叫作級聯回滾(Cascading Rollback),級聯回滾的發生會致使大量的工做須要撤回,是咱們難以接受的,不過若是想要達到絕對的原子性,這件事情又是不得不去處理的,咱們會在文章的後面具體介紹如何處理並行事務的原子性。

持久性

既然是數據庫,那麼必定對數據的持久存儲有着很是強烈的需求,若是數據被寫入到數據庫中,那麼數據必定可以被安全存儲在磁盤上;而事務的持久性就體如今,一旦事務被提交,那麼數據必定會被寫入到數據庫中並持久存儲起來。

當事務已經被提交以後,就沒法再次回滾了,惟一可以撤回已經提交的事務的方式就是建立一個相反的事務對原操做進行『補償』,這也是事務持久性的體現之一。

重作日誌

與原子性同樣,事務的持久性也是經過日誌來實現的,MySQL 使用重作日誌(redo log)實現事務的持久性,重作日誌由兩部分組成,一是內存中的重作日誌緩衝區,由於重作日誌緩衝區在內存中,因此它是易失的,另外一個就是在磁盤上的重作日誌文件,它是持久的

當咱們在一個事務中嘗試對數據進行修改時,它會先將數據從磁盤讀入內存,並更新內存中緩存的數據,而後生成一條重作日誌並寫入重作日誌緩存,當事務真正提交時,MySQL 會將重作日誌緩存中的內容刷新到重作日誌文件,再將內存中的數據更新到磁盤上,圖中的第 四、5 步就是在事務提交時執行的。

在 InnoDB 中,重作日誌都是以 512 字節的塊的形式進行存儲的,同時由於塊的大小與磁盤扇區大小相同,因此重作日誌的寫入能夠保證原子性,不會因爲機器斷電致使重作日誌僅寫入一半並留下髒數據。

除了全部對數據庫的修改會產生重作日誌,由於回滾日誌也是須要持久存儲的,它們也會建立對應的重作日誌,在發生錯誤後,數據庫重啓時會從重作日誌中找出未被更新到數據庫磁盤中的日誌從新執行以知足事務的持久性。

回滾日誌和重作日誌

到如今爲止咱們瞭解了 MySQL 中的兩種日誌,回滾日誌(undo log)和重作日誌(redo log);在數據庫系統中,事務的原子性和持久性是由事務日誌(transaction log)保證的,在實現時也就是上面提到的兩種日誌,前者用於對事務的影響進行撤銷,後者在錯誤處理時對已經提交的事務進行重作,它們能保證兩點:

  1. 發生錯誤或者須要回滾的事務可以成功回滾(原子性);

  2. 在事務提交後,數據沒來得及寫會磁盤就宕機時,在下次從新啓動後可以成功恢復數據(持久性);

在數據庫中,這兩種日誌常常都是一塊兒工做的,咱們能夠將它們總體看作一條事務日誌,其中包含了事務的 ID、修改的行元素以及修改先後的值。

一條事務日誌同時包含了修改先後的值,可以很是簡單的進行回滾和重作兩種操做,在這裏咱們也不會對重作和回滾日誌展開進行介紹,可能會在以後的文章談一談數據庫系統的恢復機制時提到兩種日誌的使用。

隔離性

其實做者在以前的文章 『淺入淺出』MySQL 和 InnoDB 就已經介紹過數據庫事務的隔離性,不過爲了保證文章的獨立性和完整性,咱們還會對事務的隔離性進行介紹,介紹的內容可能稍微有所不一樣。

事務的隔離性是數據庫處理數據的幾大基礎之一,若是沒有數據庫的事務之間沒有隔離性,就會發生在 並行事務的原子性 一節中提到的級聯回滾等問題,形成性能上的巨大損失。若是全部的事務的執行順序都是線性的,那麼對於事務的管理容易得多,可是容許事務的並行執行卻能可以提高吞吐量和資源利用率,而且能夠減小每一個事務的等待時間。

當多個事務同時併發執行時,事務的隔離性可能就會被違反,雖然單個事務的執行可能沒有任何錯誤,可是從整體來看就會形成數據庫的一致性出現問題,而串行雖然可以容許開發者忽略並行形成的影響,可以很好地維護數據庫的一致性,可是卻會影響事務執行的性能。

事務的隔離級別

因此說數據庫的隔離性和一致性實際上是一個須要開發者去權衡的問題,爲數據庫提供什麼樣的隔離性層級也就決定了數據庫的性能以及能夠達到什麼樣的一致性;在 SQL 標準中定義了四種數據庫的事務的隔離級別:READ UNCOMMITED、READ COMMITED、REPEATABLE READ 和 SERIALIZABLE;每一個事務的隔離級別其實都比上一級多解決了一個問題:

  • RAED UNCOMMITED:使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read);

  • READ COMMITED:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,因此容許新的記錄插入到被鎖定記錄的附近,因此再屢次使用查詢語句時,可能獲得不一樣的結果(Non-Repeatable Read);

  • REPEATABLE READ:屢次讀取同一範圍的數據會返回第一次查詢的快照,不會返回不一樣的數據行,可是可能發生幻讀(Phantom Read);

  • SERIALIZABLE:InnoDB 隱式地將所有的查詢語句加上共享鎖,解決了幻讀的問題;

以上的全部的事務隔離級別都不容許髒寫入(Dirty Write),也就是當前事務更新了另外一個事務已經更新可是還未提交的數據,大部分的數據庫中都使用了 READ COMMITED 做爲默認的事務隔離級別,可是 MySQL 使用了 REPEATABLE READ 做爲默認配置;從 RAED UNCOMMITED 到 SERIALIZABLE,隨着事務隔離級別變得愈來愈嚴格,數據庫對於併發執行事務的性能也逐漸降低。

對於數據庫的使用者,從理論上說,並不須要知道事務的隔離級別是如何實現的,咱們只須要知道這個隔離級別解決了什麼樣的問題,可是不一樣數據庫對於不一樣隔離級別的是實現細節在不少時候都會讓咱們遇到意料以外的坑。

若是讀者不瞭解髒讀、不可重複讀和幻讀到底是什麼,能夠閱讀以前的文章 『淺入淺出』MySQL 和 InnoDB,在這裏咱們僅放一張圖來展現各個隔離層級對這幾個問題的解決狀況。

隔離級別的實現

數據庫對於隔離級別的實現就是使用併發控制機制對在同一時間執行的事務進行控制,限制不一樣的事務對於同一資源的訪問和更新,而最重要也最多見的併發控制機制,在這裏咱們將簡單介紹三種最重要的併發控制器機制的工做原理。

鎖是一種最爲常見的併發控制機制,在一個事務中,咱們並不會將整個數據庫都加鎖,而是隻會鎖住那些須要訪問的數據項, MySQL 和常見數據庫中的鎖都分爲兩種,共享鎖(Shared)和互斥鎖(Exclusive),前者也叫讀鎖,後者叫寫鎖。

讀鎖保證了讀操做能夠併發執行,相互不會影響,而寫鎖保證了在更新數據庫數據時不會有其餘的事務訪問或者更改同一條記錄形成不可預知的問題。

時間戳

除了鎖,另外一種實現事務的隔離性的方式就是經過時間戳,使用這種方式實現事務的數據庫,例如 PostgreSQL 會爲每一條記錄保留兩個字段;讀時間戳中報錯了全部訪問該記錄的事務中的最大時間戳,而記錄行的寫時間戳中保存了將記錄改到當前值的事務的時間戳。

使用時間戳實現事務的隔離性時,每每都會使用樂觀鎖,先對數據進行修改,在寫回時再去判斷當前值,也就是時間戳是否改變過,若是沒有改變過,就寫入,不然,生成一個新的時間戳並再次更新數據,樂觀鎖其實並非真正的鎖機制,它只是一種思想,在這裏並不會對它進行展開介紹。

多版本和快照隔離

經過維護多個版本的數據,數據庫能夠容許事務在數據被其餘事務更新時對舊版本的數據進行讀取,不少數據庫都對這一機制進行了實現;由於全部的讀操做再也不須要等待寫鎖的釋放,因此可以顯著地提高讀的性能,MySQL 和 PostgreSQL 都對這一機制進行本身的實現,也就是 MVCC,雖然各自實現的方式有所不一樣,MySQL 就經過文章中提到的回滾日誌實現了 MVCC,保證事務並行執行時可以不等待互斥鎖的釋放直接獲取數據。

隔離性與原子性

在這裏就須要簡單提一下在在原子性一節中遇到的級聯回滾等問題了,若是一個事務對數據進行了寫入,這時就會獲取一個互斥鎖,其餘的事務就想要得到改行數據的讀鎖就必須等待寫鎖的釋放,天然就不會發生級聯回滾等問題了。

不過在大多數的數據庫,好比 MySQL 中都使用了 MVCC 等特性,也就是正常的讀方法是不須要獲取鎖的,在想要對讀取的數據進行更新時須要使用 SELECT ... FOR UPDATE 嘗試獲取對應行的互斥鎖,以保證不一樣事務能夠正常工做。

一致性

做者認爲數據庫的一致性是一個很是讓人迷惑的概念,緣由是數據庫領域其實包含兩個一致性,一個是 ACID 中的一致性、另外一個是 CAP 定義中的一致性。

這兩個數據庫的一致性說的徹底不是一個事情,不少不少人都對這二者的概念有很是深的誤解,當咱們在討論數據庫的一致性時,必定要清楚上下文的語義是什麼,儘可能明確的問出咱們要討論的究竟是 ACID 中的一致性仍是 CAP 中的一致性。

ACID

數據庫對於 ACID 中的一致性的定義是這樣的:若是一個事務原子地在一個一致地數據庫中獨立運行,那麼在它執行以後,數據庫的狀態必定是一致的。對於這個概念,它的第一層意思就是對於數據完整性的約束,包括主鍵約束、引用約束以及一些約束檢查等等,在事務的執行的先後以及過程當中不會違背對數據完整性的約束,全部對數據庫寫入的操做都應該是合法的,並不能產生不合法的數據狀態。

A transaction must preserve database consistency - if a transaction is run atomically in isolation starting from a consistent database, the database must again be consistent at the end of the transaction.

咱們能夠將事務理解成一個函數,它接受一個外界的 SQL 輸入和一個一致的數據庫,它必定會返回一個一致的數據庫。

而第二層意思實際上是指邏輯上的對於開發者的要求,咱們要在代碼中寫出正確的事務邏輯,好比銀行轉帳,事務中的邏輯不可能只扣錢或者只加錢,這是應用層面上對於數據庫一致性的要求。

Ensuring consistency for an individual transaction is the responsibility of the application programmer who codes the transaction. - Database System Concepts

數據庫 ACID 中的一致性對事務的要求不止包含對數據完整性以及合法性的檢查,還包含應用層面邏輯的正確。

CAP 定理中的數據一致性,實際上是說分佈式系統中的各個節點中對於同一數據的拷貝有着相同的值;而 ACID 中的一致性是指數據庫的規則,若是 schema 中規定了一個值必須是惟一的,那麼一致的系統必須確保在全部的操做中,該值都是惟一的,由此來看 CAP 和 ACID 對於一致性的定義有着根本性的區別。

總結

事務的 ACID 四大基本特性是保證數據庫可以運行的基石,可是徹底保證數據庫的 ACID,尤爲是隔離性會對性能有比較大影響,在實際的使用中咱們也會根據業務的需求對隔離性進行調整,除了隔離性,數據庫的原子性和持久性相信都是比較好理解的特性,前者保證數據庫的事務要麼所有執行、要麼所有不執行,後者保證了對數據庫的寫入都是持久存儲的、非易失的,而一致性不只是數據庫對自己數據的完整性的要求,同時也對開發者提出了要求 - 寫出邏輯正確而且合理的事務。

最後,也是最重要的,當別人在將一致性的時候,必定要搞清楚他的上下文,若是對文章的內容有疑問,能夠在評論中留言。

淺談數據庫併發控制 - 鎖和 MVCC

轉自https://draveness.me/database-concurrency-control

在學習幾年編程以後,你會發現全部的問題都沒有簡單、快捷的解決方案,不少問題都須要權衡和妥協,而本文介紹的就是數據庫在併發性能和可串行化之間作的權衡和妥協 - 併發控制機制。

若是數據庫中的全部事務都是串行執行的,那麼它很是容易成爲整個應用的性能瓶頸,雖說無法水平擴展的節點在最後都會成爲瓶頸,可是串行執行事務的數據庫會加速這一過程;而併發(Concurrency)使一切事情的發生都有了可能,它可以解決必定的性能問題,可是它會帶來更多詭異的錯誤。

引入了併發事務以後,若是不對事務的執行進行控制就會出現各類各樣的問題,你可能沒有享受到併發帶來的性能提高就已經被各類奇怪的問題折磨的欲仙欲死了。

概述

如何控制併發是數據庫領域中很是重要的問題之一,不過到今天爲止事務併發的控制已經有了不少成熟的解決方案,而這些方案的原理就是這篇文章想要介紹的內容,文章中會介紹最爲常見的三種併發控制機制:

分別是悲觀併發控制、樂觀併發控制和多版本併發控制,其中悲觀併發控制實際上是最多見的併發控制機制,也就是鎖;而樂觀併發控制其實也有另外一個名字:樂觀鎖,樂觀鎖其實並非一種真實存在的鎖,咱們會在文章後面的部分中具體介紹;最後就是多版本併發控制(MVCC)了,與前二者對立的命名不一樣,MVCC 能夠與前二者中的任意一種機制結合使用,以提升數據庫的讀性能。

既然這篇文章介紹了不一樣的併發控制機制,那麼必定會涉及到不一樣事務的併發,咱們會經過示意圖的方式分析各類機制是如何工做的。

悲觀併發控制

控制不一樣的事務對同一份數據的獲取是保證數據庫的一致性的最根本方法,若是咱們可以讓事務在同一時間對同一資源有着獨佔的能力,那麼就能夠保證操做同一資源的不一樣事務不會相互影響。

最簡單的、應用最廣的方法就是使用鎖來解決,當事務須要對資源進行操做時須要先得到資源對應的鎖,保證其餘事務不會訪問該資源後,在對資源進行各類操做;在悲觀併發控制中,數據庫程序對於數據被修改持悲觀的態度,在數據處理的過程當中都會被鎖定,以此來解決競爭的問題。

讀寫鎖

爲了最大化數據庫事務的併發能力,數據庫中的鎖被設計爲兩種模式,分別是共享鎖和互斥鎖。當一個事務得到共享鎖以後,它只能夠進行讀操做,因此共享鎖也叫讀鎖;而當一個事務得到一行數據的互斥鎖時,就能夠對該行數據進行讀和寫操做,因此互斥鎖也叫寫鎖。

共享鎖和互斥鎖除了限制事務可以執行的讀寫操做以外,它們之間還有『共享』和『互斥』的關係,也就是多個事務能夠同時得到某一行數據的共享鎖,可是互斥鎖與共享鎖和其餘的互斥鎖並不兼容,咱們能夠很天然地理解這麼設計的緣由:多個事務同時寫入同一數據不免會發生各類詭異的問題。

若是當前事務沒有辦法獲取該行數據對應的鎖時就會陷入等待的狀態,直到其餘事務將當前數據對應的鎖釋放才能夠得到鎖並執行相應的操做。

兩階段鎖協議

兩階段鎖協議(2PL)是一種可以保證事務可串行化的協議,它將事務的獲取鎖和釋放鎖劃分紅了增加(Growing)和縮減(Shrinking)兩個不一樣的階段。

在增加階段,一個事務能夠得到鎖可是不能釋放鎖;而在縮減階段事務只能夠釋放鎖,並不能得到新的鎖,若是隻看 2PL 的定義,那麼到這裏就已經介紹完了,可是它還有兩個變種:

  1. Strict 2PL:事務持有的互斥鎖必須在提交後再釋放;

  2. Rigorous 2PL:事務持有的全部鎖必須在提交後釋放;

雖然鎖的使用可以爲咱們解決不一樣事務之間因爲併發執行形成的問題,可是兩階段鎖的使用卻引入了另外一個嚴重的問題,死鎖;不一樣的事務等待對方已經鎖定的資源就會形成死鎖,咱們在這裏舉一個簡單的例子:

兩個事務在剛開始時分別獲取了 draven 和 beacon 資源面的鎖,而後再請求對方已經得到的鎖時就會發生死鎖,雙方都沒有辦法等到鎖的釋放,若是沒有死鎖的處理機制就會無限等待下去,兩個事務都沒有辦法完成。

死鎖的處理

死鎖在多線程編程中是常常遇到的事情,一旦涉及多個線程對資源進行爭奪就須要考慮當前的幾個線程或者事務是否會形成死鎖;解決死鎖大致來看有兩種辦法,一種是從源頭杜絕死鎖的產生和出現,另外一種是容許系統進入死鎖的狀態,可是在系統出現死鎖時可以及時發現而且進行恢復。

預防死鎖

有兩種方式能夠幫助咱們預防死鎖的出現,一種是保證事務之間的等待不會出現環,也就是事務之間的等待圖應該是一張有向無環圖,沒有循環等待的狀況或者保證一個事務中想要得到的全部資源都在事務開始時以原子的方式被鎖定,全部的資源要麼被鎖定要麼都不被鎖定。

可是這種方式有兩個問題,在事務一開始時很難判斷哪些資源是須要鎖定的,同時由於一些很晚纔會用到的數據被提早鎖定,數據的利用率與事務的併發率也很是的低。一種解決的辦法就是按照必定的順序爲全部的數據行加鎖,同時與 2PL 協議結合,在加鎖階段保證全部的數據行都是從小到大依次進行加鎖的,不過這種方式依然須要事務提早知道將要加鎖的數據集。

另外一種預防死鎖的方法就是使用搶佔加事務回滾的方式預防死鎖,當事務開始執行時會先得到一個時間戳,數據庫程序會根據事務的時間戳決定事務應該等待仍是回滾,在這時也有兩種機制供咱們選擇,一種是 wait-die 機制:

當執行事務的時間戳小於另外一事務時,即事務 A 先於 B 開始,那麼它就會等待另外一個事務釋放對應資源的鎖,不然就會保持當前的時間戳並回滾。

另外一種機制叫作 wound-wait,這是一種搶佔的解決方案,它和 wait-die 機制的結果徹底相反,當前事務若是先於另外一事務執行並請求了另外一事務的資源,那麼另外一事務會馬上回滾,將資源讓給先執行的事務,不然就會等待其餘事務釋放資源:

兩種方法都會形成沒必要要的事務回滾,由此會帶來必定的性能損失,更簡單的解決死鎖的方式就是使用超時時間,可是超時時間的設定是須要仔細考慮的,不然會形成耗時較長的事務沒法正常執行,或者沒法及時發現須要解決的死鎖,因此它的使用仍是有必定的侷限性。

死鎖檢測和恢復

若是數據庫程序沒法經過協議從原理上保證死鎖不會發生,那麼就須要在死鎖發生時及時檢測到並從死鎖狀態恢復到正常狀態保證數據庫程序能夠正常工做。在使用檢測和恢復的方式解決死鎖時,數據庫程序須要維護數據和事務之間的引用信息,同時也須要提供一個用於判斷當前數據庫是否進入死鎖狀態的算法,最後須要在死鎖發生時提供合適的策略及時恢復。

在上一節中咱們其實提到死鎖的檢測能夠經過一個有向的等待圖來進行判斷,若是一個事務依賴於另外一個事務正在處理的數據,那麼當前事務就會等待另外一個事務的結束,這也就是整個等待圖中的一條邊:

如上圖所示,若是在這個有向圖中出現了環,就說明當前數據庫進入了死鎖的狀態 TransB -> TransE -> TransF -> TransD -> TransB,在這時就須要死鎖恢復機制接入了。

如何從死鎖中恢復其實很是簡單,最多見的解決辦法就是選擇整個環中一個事務進行回滾,以打破整個等待圖中的環,在整個恢復的過程當中有三個事情須要考慮:

每次出現死鎖時其實都會有多個事務被波及,而選擇其中哪個任務進行回滾是必需要作的事情,在選擇犧牲品(Victim)時的黃金原則就是最小化代價,因此咱們須要綜合考慮事務已經計算的時間、使用的數據行以及涉及的事務等因素;當咱們選擇了犧牲品以後就能夠開始回滾了,回滾其實有兩種選擇一種是所有回滾,另外一種是部分回滾,部分回滾會回滾到事務以前的一個檢查點上,若是沒有檢查點那天然沒有辦法進行部分回滾。

在死鎖恢復的過程當中,其實還可能出現某些任務在屢次死鎖時都被選擇成爲犧牲品,一直都不會成功執行,形成飢餓(Starvation),咱們須要保證事務會在有窮的時間內執行,因此要在選擇犧牲品時將時間戳加入考慮的範圍。

鎖的粒度

到目前爲止咱們都沒有對不一樣粒度的鎖進行討論,一直以來咱們都討論的都是數據行鎖,可是在有些時候咱們但願將多個節點看作一個數據單元,使用鎖直接將這個數據單元、表甚至數據庫鎖定起來。這個目標的實現須要咱們在數據庫中定義不一樣粒度的鎖:

當咱們擁有了不一樣粒度的鎖以後,若是某個事務想要鎖定整個數據庫或者整張表時只須要簡單的鎖住對應的節點就會在當前節點加上顯示(explicit)鎖,在全部的子節點上加隱式(implicit)鎖;雖然這種不一樣粒度的鎖可以解決父節點被加鎖時,子節點不能被加鎖的問題,可是咱們沒有辦法在子節點被加鎖時,馬上肯定父節點不能被加鎖。

在這時咱們就須要引入意向鎖來解決這個問題了,當須要給子節點加鎖時,先給全部的父節點加對應的意向鎖,意向鎖之間是徹底不會互斥的,只是用來幫助父節點快速判斷是否能夠對該節點進行加鎖:

這裏是一張引入了兩種意向鎖,意向共享鎖和意向互斥鎖以後全部的鎖之間的兼容關係;到這裏,咱們經過不一樣粒度的鎖和意向鎖加快了數據庫的吞吐量。

樂觀併發控制

除了悲觀併發控制機制 - 鎖以外,咱們其實還有其餘的併發控制機制,樂觀併發控制(Optimistic Concurrency Control)。樂觀併發控制也叫樂觀鎖,可是它並非真正的鎖,不少人都會誤覺得樂觀鎖是一種真正的鎖,然而它只是一種併發控制的思想。

在這一節中,咱們將會先介紹基於時間戳的併發控制機制,而後在這個協議的基礎上進行擴展,實現樂觀的併發控制機制。

基於時間戳的協議

鎖協議按照不一樣事務對同一數據項請求的時間依次執行,由於後面執行的事務想要獲取的數據已將被前面的事務加鎖,只能等待鎖的釋放,因此基於鎖的協議執行事務的順序與得到鎖的順序有關。在這裏想要介紹的基於時間戳的協議可以在事務執行以前先決定事務的執行順序。

每個事務都會具備一個全局惟一的時間戳,它便可以使用系統的時鐘時間,也可使用計數器,只要可以保證全部的時間戳都是惟一而且是隨時間遞增的就能夠。

基於時間戳的協議可以保證事務並行執行的順序與事務按照時間戳串行執行的效果徹底相同;每個數據項都有兩個時間戳,讀時間戳和寫時間戳,分別表明了當前成功執行對應操做的事務的時間戳。

該協議可以保證全部衝突的讀寫操做都能按照時間戳的大小串行執行,在執行對應的操做時不須要關注其餘的事務只須要關心數據項對應時間戳的值就能夠了:

不管是讀操做仍是寫操做都會從左到右依次比較讀寫時間戳的值,若是小於當前值就會直接被拒絕而後回滾,數據庫系統會給回滾的事務添加一個新的時間戳並從新執行這個事務。

基於驗證的協議

樂觀併發控制其實本質上就是基於驗證的協議,由於在多數的應用中只讀的事務佔了絕大多數,事務之間由於寫操做形成衝突的可能很是小,也就是說大多數的事務在不須要併發控制機制也能運行的很是好,也能夠保證數據庫的一致性;而併發控制機制其實向整個數據庫系統添加了不少的開銷,咱們其實能夠經過別的策略下降這部分開銷。

而驗證協議就是咱們找到的解決辦法,它根據事務的只讀或者更新將全部事務的執行分爲兩到三個階段:

在讀階段,數據庫會執行事務中的所有讀操做和寫操做,並將全部寫後的值存入臨時變量中,並不會真正更新數據庫中的內容;在這時候會進入下一個階段,數據庫程序會檢查當前的改動是否合法,也就是是否有其餘事務在 RAED PHASE 期間更新了數據,若是經過測試那麼直接就進入 WRITE PHASE 將全部存在臨時變量中的改動所有寫入數據庫,沒有經過測試的事務會直接被終止。

爲了保證樂觀併發控制可以正常運行,咱們須要知道一個事務不一樣階段的發生時間,包括事務開始時間、驗證階段的開始時間以及寫階段的結束時間;經過這三個時間戳,咱們能夠保證任意衝突的事務不會同時寫入數據庫,一旦由一個事務完成了驗證階段就會當即寫入,其餘讀取了相同數據的事務就會回滾從新執行。

做爲樂觀的併發控制機制,它會假定全部的事務在最終都會經過驗證階段而且執行成功,而鎖機制和基於時間戳排序的協議是悲觀的,由於它們會在發生衝突時強制事務進行等待或者回滾,哪怕有不須要鎖也可以保證事務之間不會衝突的可能。

多版本併發控制

到目前爲止咱們介紹的併發控制機制其實都是經過延遲或者終止相應的事務來解決事務之間的競爭條件(Race condition)來保證事務的可串行化;雖然前面的兩種併發控制機制確實可以從根本上解決併發事務的可串行化的問題,可是在實際環境中數據庫的事務大都是隻讀的,讀請求是寫請求的不少倍,若是寫請求和讀請求以前沒有併發控制機制,那麼最壞的狀況也是讀請求讀到了已經寫入的數據,這對不少應用徹底是能夠接受的。

在這種大前提下,數據庫系統引入了另外一種併發控制機制 - 多版本併發控制(Multiversion Concurrency Control),每個寫操做都會建立一個新版本的數據,讀操做會從有限多個版本的數據中挑選一個最合適的結果直接返回;在這時,讀寫操做之間的衝突就再也不須要被關注,而管理和快速挑選數據的版本就成了 MVCC 須要解決的主要問題。

MVCC 並非一個與樂觀和悲觀併發控制對立的東西,它可以與二者很好的結合以增長事務的併發量,在目前最流行的 SQL 數據庫 MySQL 和 PostgreSQL 中都對 MVCC 進行了實現;可是因爲它們分別實現了悲觀鎖和樂觀鎖,因此 MVCC 實現的方式也不一樣。

MySQL 與 MVCC

MySQL 中實現的多版本兩階段鎖協議(Multiversion 2PL)將 MVCC 和 2PL 的優勢結合了起來,每個版本的數據行都具備一個惟一的時間戳,當有讀事務請求時,數據庫程序會直接從多個版本的數據項中具備最大時間戳的返回。

更新操做就稍微有些複雜了,事務會先讀取最新版本的數據計算出數據更新後的結果,而後建立一個新版本的數據,新數據的時間戳是目前數據行的最大版本 +1:

數據版本的刪除也是根據時間戳來選擇的,MySQL 會將版本最低的數據定時從數據庫中清除以保證不會出現大量的遺留內容。

PostgreSQL 與 MVCC

與 MySQL 中使用悲觀併發控制不一樣,PostgreSQL 中都是使用樂觀併發控制的,這也就致使了 MVCC 在於樂觀鎖結合時的實現上有一些不一樣,最終實現的叫作多版本時間戳排序協議(Multiversion Timestamp Ordering),在這個協議中,全部的的事務在執行以前都會被分配一個惟一的時間戳,每個數據項都有讀寫兩個時間戳:

當 PostgreSQL 的事務發出了一個讀請求,數據庫直接將最新版本的數據返回,不會被任何操做阻塞,而寫操做在執行時,事務的時間戳必定要大或者等於數據行的讀時間戳,不然就會被回滾。

這種 MVCC 的實現保證了讀事務永遠都不會失敗而且不須要等待鎖的釋放,對於讀請求遠遠多於寫請求的應用程序,樂觀鎖加 MVCC 對數據庫的性能有着很是大的提高;雖然這種協議可以針對一些實際狀況作出一些明顯的性能提高,可是也會致使兩個問題,一個是每一次讀操做都會更新讀時間戳形成兩次的磁盤寫入,第二是事務之間的衝突是經過回滾解決的,因此若是衝突的可能性很是高或者回滾代價巨大,數據庫的讀寫性能還不如使用傳統的鎖等待方式。

1. MVCC簡介與實踐

MySQL 在InnoDB引擎下有當前讀和快照讀兩種模式。

1 當前讀即加鎖讀,讀取記錄的最新版本號,會加鎖保證其餘併發事物不能修改當前記錄,直至釋放鎖。插入/更新/刪除操做默認使用當前讀,顯示的爲select語句加lock in share mode或for update的查詢也採用當前讀模式。

2 快照讀:不加鎖,讀取記錄的快照版本,而非最新版本,使用MVCC機制,最大的好處是讀取不須要加鎖,讀寫不衝突,用於讀操做多於寫操做的應用,所以在不顯示加[lock in share mode]/[for update]的select語句,即普通的一條select語句默認都是使用快照讀MVCC實現模式。因此樓主的爲了讓你們明白所作的演示操做,既有當前讀也有快照讀……

1.1 什麼是MVCC

MVCC是一種多版本併發控制機制。

1.2 MVCC是爲了解決什麼問題?

  • 大多數的MYSQL事務型存儲引擎,如,InnoDB,Falcon以及PBXT都不使用一種簡單的行鎖機制.事實上,他們都和MVCC–多版本併發控制來一塊兒使用.

  • 你們都應該知道,鎖機制能夠控制併發操做,可是其系統開銷較大,而MVCC能夠在大多數狀況下代替行級鎖,使用MVCC,能下降其系統開銷.

1.3 MVCC實現

MVCC是經過保存數據在某個時間點的快照來實現的. 不一樣存儲引擎的MVCC. 不一樣存儲引擎的MVCC實現是不一樣的,典型的有樂觀併發控制和悲觀併發控制.

2.MVCC 具體實現分析

下面,咱們經過InnoDB的MVCC實現來分析MVCC使怎樣進行併發控制的.  InnoDB的MVCC,是經過在每行記錄後面保存兩個隱藏的列來實現的,這兩個列,分別保存了這個行的建立時間,一個保存的是行的刪除時間。這裏存儲的並非實際的時間值,而是系統版本號(能夠理解爲事務的ID),沒開始一個新的事務,系統版本號就會自動遞增,事務開始時刻的系統版本號會做爲事務的ID.下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操做的.

2.1簡單的小例子

create table yang(  id int primary key auto_increment,  name varchar(20));

假設系統的版本號從1開始.

INSERT

InnoDB爲新插入的每一行保存當前系統版本號做爲版本號.  第一個事務ID爲1;

start transaction; insert into yang values(NULL,'yang')  ; insert into yang values(NULL,'long'); insert into yang values(NULL,'fei'); commit;

  • 1

  • 2

  • 3

  • 4

  • 5

對應在數據中的表以下(後面兩列是隱藏列,咱們經過查詢語句並看不到)

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

SELECT

InnoDB會根據如下兩個條件檢查每行記錄:  a.InnoDB只會查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣能夠確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的.  b.行的刪除版本要麼未定義,要麼大於當前事務版本號,這能夠確保事務讀取到的行,在事務開始以前未被刪除.  只有a,b同時知足的記錄,才能返回做爲查詢結果.

DELETE

InnoDB會爲刪除的每一行保存當前系統的版本號(事務的ID)做爲刪除標識.  看下面的具體例子分析:  第二個事務,ID爲2;

start transaction; select *  from yang;  //(1) select *  from yang;  //(2) commit;  

  • 1

  • 2

  • 3

  • 4

假設1

假設在執行這個事務ID爲2的過程當中,剛執行到(1),這時,有另外一個事務ID爲3往這個表裏插入了一條數據;  第三個事務ID爲3;

start transaction; insert into yang values(NULL,'tian'); commit;

  • 1

  • 2

  • 3

這時表中的數據以下:

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

而後接着執行事務2中的(2),因爲id=4的數據的建立時間(事務ID爲3),執行當前事務的ID爲2,而InnoDB只會查找事務ID小於等於當前事務ID的數據行,因此id=4的數據行並不會在執行事務2中的(2)被檢索出來,在事務2中的兩條select 語句檢索出來的數據都只會下表:

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

假設2

假設在執行這個事務ID爲2的過程當中,剛執行到(1),假設事務執行完事務3後,接着又執行了事務4;  第四個事務:

start   transaction;  delete  from yang where id=1; commit;  

  • 1

  • 2

  • 3

此時數據庫中的表以下:

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

接着執行事務ID爲2的事務(2),根據SELECT 檢索條件能夠知道,它會檢索建立時間(建立事務的ID)小於當前事務ID的行和刪除時間(刪除事務的ID)大於當前事務的行,而id=4的行上面已經說過,而id=1的行因爲刪除時間(刪除事務的ID)大於當前事務的ID,因此事務2的(2)select * from yang也會把id=1的數據檢索出來.因此,事務2中的兩條select 語句檢索出來的數據都以下:

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined

UPDATE

InnoDB執行UPDATE,其實是新插入了一行記錄,並保存其建立時間爲當前事務的ID,同時保存當前事務ID到要UPDATE的行的刪除時間.

假設3

假設在執行完事務2的(1)後又執行,其它用戶執行了事務3,4,這時,又有一個用戶對這張表執行了UPDATE操做:  第5個事務:

start  transaction; update yang set name='Long' where id=2; commit;

  • 1

  • 2

  • 3

根據update的更新原則:會生成新的一行,並在原來要修改的列的刪除時間列上添加本事務ID,獲得表以下:

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined
4 tian 3 undefined
2 Long 5 undefined

繼續執行事務2的(2),根據select 語句的檢索條件,獲得下表:

id name 建立時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined

仍是和事務2中(1)select 獲得相同的結果.

總結

數據庫的併發控制機制到今天已經有了很是成熟、完善的解決方案,咱們並不須要本身去設計一套新的協議來處理不一樣事務之間的衝突問題,從數據庫的併發控制機制中學習到的相關知識,不管是鎖仍是樂觀併發控制在其餘的領域或者應用中都被普遍使用,因此瞭解、熟悉不一樣的併發控制機制的原理是頗有必要的。

微信公衆號【黃小斜】大廠程序員,互聯網行業新知,終身學習踐行者。關注後回覆「Java」、「Python」、「C++」、「大數據」、「機器學習」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「筆試」、「面試」、「面經」、「計算機基礎」、「LeetCode」 等關鍵字能夠獲取對應的免費學習資料。 

![](https://img2018.cnblogs.com/blog/1092007/201908/1092007-20190824164621954-1468245456.jpg)
添加描述
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)
添加描述

相關文章
相關標籤/搜索