數據庫中事務和隔離級別解析

什麼是數據庫中的事務,能夠說事務就是一組原子性的SQL查詢,獨立的工做單元。咱們的事務內的語句,要麼所有執行成功,要麼所有執行失敗!html

事務要知足ACID特性,能夠經過Commit提交一個事務,也可使用Rollback進行回滾!下面咱們就介紹一下事務的ACID特性。java

1 ACID特性

1.1 原子性(actomicity)

一個事務必須被視爲一個不可分割的最小單元,整個事務中的全部操做要麼所有提交成功,要麼所有失敗回滾,對於一個事務來講,不可能只執行其中的一部分操做,這就是事務的原子性。git

舉個例子,咱們的銀行帳號有100元,去商家消費,下面開啓一個事務:github

  • 查詢咱們的帳戶有多少錢——返回100元
  • 對咱們的帳戶消費減去50元
  • 商家的帳戶的增長50元

咱們能夠把這一組操做當作是不可分割的,若是有出現第二步失敗,第三步成功,那咱們銀行不就虧了50?因此咱們的原子性就是爲了保證每一步都成功,否則就失敗都不執行!sql

1.2 一致性(consistency)

數據庫老是從一個一致性的狀態轉換到另一個一致性的狀態。數據庫

咱們一樣以上面爲例子,也就是說,若是其中的一步失敗了,咱們的事務就不會提交,作的修改也就不會保存到數據庫中!能夠說,咱們的一致性就是爲了事務先後的數據可以對的上。安全

1.3 隔離性(isolation)

一個事務所作的修改在最終提交之前,對其它事務是不可見的。服務器

以上面爲例,也就是當咱們上面帳號消費減去50,可是由於事務尚未提交,因此在另外一個事務來看,咱們的帳號仍是100。可是隔離性遠不止這麼簡單,咱們後面會有針對隔離性討論的隔離級別!併發

1.4 持久性(durability)

持久性也就是咱們的事務一旦提交就會永久保存到數據庫中。哪怕系統崩潰,修改的數據也不會丟失!系統發生奔潰能夠用重作日誌(Redo Log)進行恢復,從而實現持久性。與回滾日誌記錄數據的邏輯修改不一樣,重作日誌記錄的是數據頁的物理修改。mvc

2 隔離級別

咱們上面在介紹了事務的隔離性,可是那是考慮在單線程狀況下。若是在併發環境下,事務的隔離性很難保證,會出現不少併發一致性的問題,如髒讀、不可重複讀、幻讀。咱們先介紹一下這些問題

2.1 併發下一致性問題

髒讀

一個事務容許讀取另外一個未提交事務的修改的數據。好比咱們的事務A對age字段修改成20(假設原來是10),而後這個時候事務B進來了讀取到了age = 20,可是咱們的事務A回滾了,那麼事務B讀取到就是髒數據!

不可重複讀

不可重複讀指在一個事務內屢次讀取同一數據集合。在這一事務還未結束前,另外一事務也訪問了該同一數據集合並作了修改,因爲第二個事務的修改,第一次事務的兩次讀取的數據可能不一致。好比A事務讀取了age = 10,可是這個時候事務B對age 進行了修改age = 20。若是事務A再一次查詢的話,那麼讀取的結果就會與第一次結果不一樣!

這樣雖然可以理解,可是咱們可能會以爲不可重複讀不是挺好的嗎,實時更新了數據。但其實否則,咱們能夠想象一下,若是咱們事務A讀取了age的值爲10,準備更新當前age爲它的兩倍,那麼就是20!可是在咱們讀取了age爲10以後,事務B修改了age爲50,那咱們經過10計算出來的結果豈不是錯的嗎,應該是100而不是20了。因此不可重複讀會推翻咱們以前的計算,那爲了一致性,事務A只能從新執行了!

幻讀

幻讀本質上也屬於不可重複讀的狀況,事務A 讀取某個範圍的數據,事務B在這個範圍內插入新的數據,事務A 再次讀取這個範圍的數據,此時讀取的結果和和第一次讀取的結果不一樣。

可能這樣子對不可重複讀和幻讀容易混亂,由於二者非常相似。

但其實不可重複讀重點在於update和delete,而幻讀的重點在於insert。

好比咱們的不可重複讀,咱們把要讀那些數據用鎖鎖出,其餘事務沒法對其進行修改,這樣就能夠實現可重複讀。可是咱們要新增的數據倒是沒有加鎖的,這樣就會致使咱們在原來的事務裏面兩次查詢儘管加了鎖卻仍是發現結果不同,就好像幻覺同樣,因此叫幻讀!

讀鎖與寫鎖

咱們的事務隔離級別是經過鎖來是實現,因此在瞭解隔離級別以前,咱們要來了解一下下面會用的兩個基本鎖,讀與與。

讀鎖又叫共享鎖,是共享的,相互不阻塞,多個客戶在同一時刻能夠同時讀取同一個資源而不相互干擾。

寫鎖又叫排它鎖,是排他的,也就是說一個寫鎖會阻塞其餘的寫鎖和讀鎖,確保在給定時間內只有一個用戶能執行寫入並防止其餘用戶讀取正在寫入的同一資源。

2.2 事務的隔離級別

由於這些併發一致性的問題,因此咱們的數據庫中提出了事務的隔離級別來解決這些問題,咱們的數據庫的鎖也是爲了隔離級別而存在的!

隔離級別 髒讀(Dirty Read) 不可重複讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 未提交讀(Read Uncommitted):容許髒讀,也就是可能讀取到其餘會話中未提交事務修改的數據
  • 提交讀(Read Committed):只能讀取到已經提交的數據。Oracle等多數數據庫默認都是該級別 (不重複讀)
  • 可重複讀(Repeated Read):可重複讀。在同一個事務內的查詢都是事務開始時刻一致的,InnoDB默認級別。在SQL標準中,該隔離級別消除了不可重複讀,可是還存在幻象讀
  • 串行讀(Serializable):徹底串行化的讀,每次讀都須要得到表級共享鎖,讀寫相互都會阻塞

2.2.1 已提交讀(Read committed)

在RC級別裏,數據的讀取是不加鎖的,可是數據的寫入、修改和刪除是須要加鎖的。

這裏咱們是經過行鎖裏面的記錄鎖(Record Locks)來實現的,這裏分別介紹一下行鎖和記錄鎖。

關於行鎖和表鎖

表鎖是MySQL中最基本的鎖策略,而且是開銷最小的策略。表鎖會鎖定整張表,一個用戶在對錶進行寫操做前須要先得到寫鎖,這會阻塞其餘用戶對該表的全部讀寫操做。只有沒有寫鎖時,其餘讀取的用戶才能獲取讀鎖,讀鎖之間不相互阻塞。

行鎖能夠最大程度地支持併發,同時也帶來了最大開銷。InnoDB 和 XtraDB 以及一些其餘存儲引擎實現了行鎖。行鎖只在存儲引擎層實現,而服務器層沒有實現。

關於記錄鎖

咱們的記錄鎖就是行鎖的一種,所謂記錄鎖指的就是鎖定一個單個行記錄上的索引,而不是記錄自己。

咱們經過記錄鎖就能夠鎖定咱們要修改或者要刪除的數據,這樣在解鎖以前別的事務就沒法訪問到該數據,從而解決了咱們的併發一致性的第一個問題——髒讀。

2.2.2 可重複讀(Repeatable read)

咱們在上面介紹了併發的一致性問題,不可重複讀,反過來,就能夠理解咱們的可重複讀。咱們的RR級別的隔離也就是爲了解決這一個問題,解決後的應該以下:

咱們RR級別的實現,上面也提到了一下,就是在咱們sql第一次讀取數據後,就將這些數據加鎖,這樣其餘事務就沒法修改這些數據,就能夠實現可重複讀了,也就解決了咱們的不可重複讀的問題。可是咱們只能鎖住已有的數據,對與後來insert的數據咱們是沒法鎖住的,因此就避免不了幻讀(在上面介紹過了這個問題)。

2.2.3 MVCC多版本併發控制

雖然咱們的提交讀解決了髒讀問題,可重複讀解決了不可重複讀的問題。可是這兩種隔離級別都是採用了悲觀鎖的機制來解決這兩個問題的,對於咱們成熟的數據庫來講,出於性能的考慮,都會使用以樂觀鎖爲理論基礎的MVCC(多版本併發控制)來避免這兩種問題。

在InnoDB中,會在每行數據後添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據什麼時候被建立,另一個記錄這行數據什麼時候過時(或者被刪除)。 在實際操做中,存儲的並非時間,而是事務的版本號,每開啓一個新事務,事務的版本號就會遞增。 在可重讀Repeatable reads事務隔離級別下:

  • SELECT時,讀取建立版本號<=當前事務版本號,刪除版本號爲空或>當前事務版本號。
  • INSERT時,保存當前事務版本號爲行的建立版本號
  • DELETE時,保存當前事務版本號爲行的刪除版本號
  • UPDATE時,插入一條新紀錄,保存當前事務版本號爲行建立版本號,同時保存當前事務版本號到原來刪除的行

經過MVCC,雖然每行記錄都須要額外的存儲空間,更多的行檢查工做以及一些額外的維護工做,但能夠減小鎖的使用,大多數讀操做都不用加鎖,讀數據操做很簡單,性能很好,而且也能保證只會讀取到符合標準的行,也只鎖住必要行。

咱們的MVCC是能夠解決幻讀的讀問題的。好比咱們在下面的事務C中添加了一條teacher_id=1的數據commit,RR級別中應該會有幻讀現象,事務A在查詢teacher_id=1的數據時會讀到事務C新加的數據。但其實不會,經過MVCC咱們解決了這個讀的幻讀現象,並且咱們沒加鎖同時也解決了咱們的事務B的不重複讀問題變成可重複讀!

快照讀

咱們經過MVCC是能夠解決幻讀的讀問題,由於咱們的查詢都是查詢以前的快照,咱們的插入操做是不影響的。所謂快照,也就是指的是咱們讀取的並非最新一手的數據,而是以前的遺留數據。基本上咱們的select語句就是快照讀!

當前讀

咱們說了快照讀所讀取的並非最新一手的數據,那麼這在一些對於數據的時效特別敏感的業務中,就極可能出問題。因此咱們就須要當前讀來獲取實時的數據。可是咱們的MVCC並不能當前讀的幻讀問題。咱們當前讀的幻讀而是留給咱們下面要介紹的臨鍵鎖來解決!

2.2.4 臨鍵鎖解決幻讀

咱們的臨鍵鎖其實就是咱們的上面的記錄鎖+間隙鎖(gap)來實現的!

所謂間隙鎖就是鎖定索引之間的間隙,可是不包括索引自己。而咱們的臨鍵鎖就是兩者的整合,不只有索引的間隙,也包括當前的索引。例如一個索引包含如下值:10, 11, 13, and 20,那麼就須要鎖定如下區間:

(10, 11]
(11, 13]
(13, 20]

經過臨鍵鎖咱們解決問題能夠用以下流程圖表示:

2.2.5 可串行化(Serializable )

讀加共享鎖,寫加排他鎖,讀寫互斥。使用的悲觀鎖的理論,實現簡單,數據更加安全,可是併發能力很是差。若是你的業務併發的特別少或者沒有併發,同時又要求數據及時可靠的話,可使用這種模式。

3 參考資料

https://tech.meituan.com/2014/08/20/innodb-lock.html

https://github.com/CyC2018/CS-Notes/blob/master/notes/數據庫系統原理.md#record-locks

https://www.nowcoder.com/discuss/447742

相關文章
相關標籤/搜索