帶你讀懂Spring 事務——事務的隔離級別(超詳細,快藏)

不瞭解事務的鐵汁可以先看前兩篇,講的超詳細,有問題還請您指點一二

特別提示:本文所進行的實驗都是在MySQL 5.7.30下使用InnoDB引擎進行的

一、什麼是事務隔離

錘子在《帶你讀懂Spring事務》的第一篇中介紹過事務的定義以及它的四個特性,其中有一個特性就是隔離性,當時說了後面細講,所以今天就來聊一聊事務的隔離特性。

根據事務的定義,我們已經知道了一個事務並不是一個單一的操作,它其實包含了多步操作,同時事務的執行的也可以是併發的,就是同時開啓多個事務,進行業務操作,併發執行的事務裏面的多步操作對着相同的數據進行查找修改,如果沒有一個規則,在高併發的環境下可以引發的結果就可想而知,這個時候我們就需要定義一種規則,根據不同的業務場景來保證我們數據的可控性,這種規則就是事務的隔離級別,我們通俗的講,事務的隔離級別就是一種控制併發執行的事務對數據操作的規則

在標準SQL(SQL 92)中定義了四種事務的隔離級別:READ UNCOMMITTED(讀未提交)、READ COMMITTED(讀已提交)、REPEATABLE READ(可重複讀)、SERIALIZABLE(串行化),其中隔離級別最寬鬆的是讀未提交,最嚴格的是可串行化,當事務隔離級別較低時會引起一些數據問題(後文會講解),當事務隔離級別設置爲可串行化的時候,也就意味着事務的執行就類似於串行了,這個時候性能就會受到影響,所以在實際的業務中,是根據自己的需求來設置合理的事務的隔離級別,在性能和數據安全的兩者之間找一個平衡點

在不同的數據庫中事務的隔離級別還有小小的不同,在常用的關係型數據庫中Oracle的事務隔離級別就只有三種:讀已提交、串行化、只讀(Read-Only)。在Oracle中增加了一個只讀級別,而去掉了讀未提交和可重複讀兩個級別。
Oracle中的讀已提交和串行化的設計與標準SQL沒有太多區別,我們主要說一下這個只讀級別,它就是類似於一種快照的方式,處於只讀級別的事務只能看到事務執行前就已經提交的數據,且事務中不能執行 INSERT , UPDATE ,及 DELETE 語句,就相當於在事務開始的時候,對數據創建了一個快照,整個事務的執行都在該快照的上進行讀取,且不能修改。
另外幾種常用的關係型數據庫MySQL,MariaDB,PostgreSQL都是提供了按照標準SQL的四種隔離級別,那麼接下來錘子就和大家一起看看這四種事務隔離級別都究竟是怎樣的

二、四種事務隔離級別

同樣在開始介紹四種事務隔離之前,錘子還是要設計一個場景幫助大家理解。

**1.**現有表A,兩個字段 id和money,id爲主鍵,money爲金錢,數據庫中一條id=1,money=20的數據,如下表所示

id money
1 20

**2.**有如下事務操作

時間 事務1 事務2
T1 begin 事務1
T2 select money from A where id = 1
T3 begin 事務2
T4 update A set money=100 where id=1
T5 select money from A where id = 1
T6 update A set money=200 where id=1
T7 select money from A where id = 1
T8 commit
T9 select money from A where id = 1
T10 commit

READ UNCOMMITTED——讀未提交

**事務執行過程中可以讀取到併發執行的其他事務中未提交的數據。**在這種事務隔離級別下可能會引起的問題包括:髒讀,不可重複讀和幻讀。

舉栗子

設置場景中事務的隔離級別是讀未提交,那麼現象是什麼樣的呢?

時序描述如下

  • T1 時刻,開啓事務1
  • T2 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T3 時刻,開啓事務2
  • T4 時刻,事務2,更新A表id爲1的記錄的money爲100
  • T5 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=100(讀取到T4時刻,事務2更新未提交數據)
  • T6 時刻,事務2,更新A表id爲1的記錄的money爲200
  • T7 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=200(讀取到T6時刻,事務2更新未提交數據)
  • T8 時刻,事務2提交
  • T9 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=200
  • T10 時刻,事務1提交

從上面描述可以看出,事務1在T5和T7時刻查詢的數據都是事務2更新了但是還沒有進行提交的數據,事務2是在T8時刻才提交,所以這就是讀未提交,讀取到了其他事務沒有提交的數據。

前面錘子也說了,這是事務隔離中最低的級別,這個隔離級別下的事務會讀取到其他事務未提交的數據,所以在其他事務回滾的時候,當前事務讀取的數據就成了髒數據(髒讀我們後文細講)

READ COMMITTED——讀已提交

**事務執行過程中只能讀到其他事務已經提交的數據。**讀已提交保證了併發執行的事務不會讀到其他事務未提交的修改數據,避免了髒讀問題。在這種事務隔離級別下可能引發的問題包括:不可重複讀和幻讀

舉栗子

設置場景中事務的隔離級別是讀已提交,那麼現象是什麼樣的呢?

時序描述如下

  • T1 時刻,開啓事務1
  • T2 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T3 時刻,開啓事務2
  • T4 時刻,事務2,更新A表id爲1的記錄的money爲100
  • T5 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T6 時刻,事務2,更新A表id爲1的記錄的money爲200
  • T7 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T8 時刻,事務2提交
  • T9 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=200讀取到事務2提交的數據
  • T10 時刻,事務1提交

從上面的描述可以看出,在讀已提交的事務隔離級別下,事務1不會讀到事務2中修改但未提交的數據,在T8時刻事務2提交後,T9時刻事務1讀取的數據才發生變化,這種現象就是讀已提交。

REPEATABLE READ——可重複讀

**當前事務執行開始後,所讀取到的數據都是該事務剛開始時所讀取的數據和自己事務內修改的數據。**這種事務隔離級別下,無論其他事務對數據怎麼修改,在當前事務下讀取到的數據都是該事務開始時的數據,所以這種隔離級別下可以避免不可重複讀的問題,但還是有可能出現幻讀,那是爲什麼呢?

答案我們在下文第三部分講解幻讀時詳細講,現在我們還是先看看在可重複讀隔離級別下,上面場景會變成什麼樣。

舉栗子

設置場景中事務的隔離級別是可重複讀,那麼現象是什麼樣的呢?

時序描述如下

  • T1 時刻,開啓事務1
  • T2 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T3 時刻,開啓事務2
  • T4 時刻,事務2,更新A表id爲1的記錄的money爲100
  • T5 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T6 時刻,事務2,更新A表id爲1的記錄的money爲200
  • T7 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T8 時刻,事務2提交
  • T9 時刻,事務1,查詢A表id=1的記錄的money,結果爲money=20
  • T10 時刻,事務1提交

從上面描述看出,不論事務2對數據進行了幾次更新,到後面即使事務2進行事務提交,事務1裏面始終讀取的還是自己開始事務前的數據,這種情況就是一種快照讀(類似於Oracle下的Read-Only級別),但是這種情況下事務中也是可以進行insert,update和delete操作的。

SERIALIZABLE——串行化

**事務的執行是串行執行。**這種隔離級別下,可以避免髒讀、不可重複讀和幻讀等問題,但是由於事務一個一個執行,所以性能就比較低。

所以在這種隔離級別下,上面的場景中是事務1執行完成後,纔會執行事務2,兩個事務在執行時不會相互有影響。

三、可能會引發的三種問題以及危害

講完了標準SQL的事務的四種隔離級別,那麼接下來,錘子就帶大家,看一下,這四種隔離級別下,會發生的問題。前面咱們也提到了主要有三種問題:髒讀、不可重複讀和幻讀。那麼這些問題要怎麼理解?錘子接下來,會一個一個舉例進行詳細講解

髒讀

髒讀其實就是讀到了,其他事務回滾前的未提交的髒數據。四種事務隔離級別中只有讀未提交纔會出現這種問題。咱們舉個簡單的栗子,理解一下髒讀。

現在某電商平臺有手機10庫存10部,用戶A在平臺下單購買了10臺手機,此時手機庫存爲0,因爲地址填錯了,A用戶執行取消訂單操作,取消訂單的事務操作步驟如下:

1.開啓事物
2.訂單狀態改爲取消
3.庫存還原
4.執行其他業務邏輯
5.提交事物

而在取消訂單的事務中的第4步執行其他邏輯操作的時刻,用戶B在查看該手機的庫存時就看到了庫存爲10(讀取到了其他事務未提交的數據),於是用戶B下單買下來10臺手機,後來由於A用戶的取消訂單的部分操作失敗了,訂單沒有取消,數據發生了回滾,那麼現在就出現了實際庫存爲10的手機被賣出了20臺,導致了超賣問題。

這樣就由髒讀造成了一個實際生產中超賣問題,在實際生產中,髒讀會導致很多問題,所以我們在使用事務的時候,不要輕易將事務隔離級別設置爲讀未提交,一定要仔細思考後再選擇。

不可重複讀

不可重複讀就是在一個事務多次相同的讀取可能會讀出不同的數據。讀未提交和讀已提交的隔離級別下都可能會出現不可重複讀的問題。

  • 在讀未提交隔離級別下,當前事務是可以讀到其他事務未提交的數據,所以其他事務一直對數據進行修改的話,當前事務多次讀取的數據就會不同。

  • 在讀已提交的隔離級別下,當前事務只能讀到其他事務已提交的數據,所以在其他事務修改還未提交時,當前事務的多次讀取是一樣的,但是一旦其他事務提交了修改,當前事務再讀取到的數據,就與之前不一致了,這也是不可重複讀的現場。

這樣看起來在讀已提交隔離級別下,當前事務都是讀取的最新的已提交到數據庫的數據,也不會有髒數據,好像並沒有什麼不妥,而且也合情合理,本來數據就是要讀取最新的操作嘛,那麼爲什麼不可重複讀,還要被列爲一個問題呢?相信你看了下面的例子就明白了

場景如下:

某電商平臺做活動,用戶在平臺消費5000-10000元的送一個手機,消費超過10000元的送電腦。現在有生成獲獎用戶報表的事務如下:

1.開始事務
2.查詢消費在5000到10000元的用戶
3.打印送手機用戶名單
4.查詢消費在10000元以上的用戶
5.打印送電腦用戶名單
6.結束事務

錘子是該電商平臺的用戶,錘子之前的消費是6000元,在上述第2步的時候,錘子符合送手機的條件,而在上述第3步操作的時候,錘子又在電商平臺消費了5000元,那麼上述事務走到第4步的時候,錘子也符合了送電腦的條件,那麼最終錘子就即獲得了手機又獲得了電腦,這個時候錘子是高興了,可是電商平臺就不高興了,要吊起來打寫這個功能的程序員哥哥嘍。

看完上面的場景是不是發現,總是讀取最新的數據並不是最好的,在某些場景下就是需要快照讀,特別是對截至時間要求非常精確的地方,在事務開始的那一刻所有數據就應該固定,在整個事務的過程中,所有數據讀取都要以事務開始的那一刻爲準。

要處理這種情形下的問題,就要提高一下事務隔離級別到可重複讀,在查出送手機用戶的名單後加行鎖,這樣錘子又消費5000元的操作完成就在生成整個名單之後了,這就保證了錘子不會收到兩個獎品。所以鐵汁們在開發中要注意了,千萬別踩到這樣的坑,不然要被吊起來打了。

幻讀

其他事務在一個尚未提交的當前事務的讀取的行的範圍中插入新行或刪除現有行,會對當前事務的對數據的讀取產生幻象。幻讀在讀未提交、讀已提交和可重複讀三種事務隔離級別下都會出現。

那麼下面咱們就來舉栗子說明一下幻讀

場景如下:

某電商平臺還在做活動,用戶在平臺消費5000-10000元的送一個手機,消費超過10000元的送電腦。現在有生成獲獎用戶報表的事務如下:

1.開始事務
2.查詢消費在5000到10000元的用戶
3.打印送手機用戶名單
4.查詢消費在10000元以上的用戶
5.打印送電腦用戶名單
6.結束事務

這個時候錘子和郝大都還不是該電商平臺的用戶,他們看到好消息,都想參加這個活動,於是兩個人都去註冊用戶並消費。重點來了生成中間獲獎名單的事務執行到第3步的時刻,錘子和郝大在此刻都註冊了用戶,並且錘子消費了6000元,郝大消費了12000元,兩個人都以爲可以得到自己想要的獎品了,結果最後中獎名單出來後,發現郝大獲得了電腦,而錘子什麼也沒有,可是明明錘子和郝大一起註冊的用戶,但是郝大卻獲得了獎品,錘子卻沒獲得。

上面描述的這種現象就是讀已提交隔離級別下的一種幻讀現象,兩個用戶同時註冊(同時向表中插入數據),且各自都符合不同的獎品條件要求,但是一個有獎品,一個沒有獎品,就會讓人感覺,這個福利有內幕的感覺。這就是讀已提交下幻讀造成的一種影響。

同樣上面的場景,如果事務隔離級別提高到可重複讀,那麼在不改變上述流程的情況下,在MySQL下就不會出現幻讀了,因爲他們的註冊事務是在生成中獎名單之後,所以郝大和錘子都不會有獎品。因爲在MySql的設計中可重複讀的事務隔離級別的數據讀取是快照讀,即使其他事務進行insert或是delete,在當前事務中僅僅讀取的話是讀不到其他事務提交的數據,但是這並不代表MySQL中的可重複讀隔離級別就可以完全避免幻讀了

上面的場景下,我們提升事務隔離級別到可重複讀,然後再修改一下生產獲獎名單的事務,在第3步的後面添加一步update的操作(將用戶表中所有用戶記錄的更新時間都更新一下),那麼在update之後,再執行查詢消費在10000元以上的用戶的時候,郝大的數據又會被查出來,這個時候,又出現了,同時註冊的兩個人郝大有獎品,錘子沒有獎品。

那麼上面爲什麼進行一次update後,郝大的數據又會被查出來呢?

想知道這個原因還要知道兩個概念:當前讀和快照讀(詳解如下)

  • 當前讀:讀取的是最新版本數據, 會對讀取的記錄加鎖, 阻塞併發事務修改相同記錄,避免出現安全問題。
  • 快照讀:可能讀取到的數據不是最新版本而是歷史版本,而且讀取時不會加鎖。

現在知道了這兩個概念,那麼下面就描述一下,MySQL在可重複讀的隔離級別下開啓事務時,默認是使用的快照讀,所以在整個事務中如果只有查詢,那麼查詢的都是快照數據,就不會受到其他事務影響,但是我們上面又在事務中添加了一個更新語句,當進行更新時快照讀就會變成當前讀,因爲在事務中更新數據是需要對數據進行加鎖,直到事務提交纔會釋放鎖,所有由快照讀變爲當前讀後,讀取的數據就是最新的,也就把後來添加的郝大賬戶計算了進去。


到此我們把四種隔離級別和會引發的三種問題都進行了分析,所以大家在實際使用中要根據自己的業務進行合理選擇,避免被老闆吊着打

四、Spring中事務的隔離級別使用

前面寫了這麼多好像跟Spring都沒有什麼關係,哈哈哈,那麼接下來,錘子帶大家看一下,Spring中事務隔離級別的使用。

事務隔離級別枚舉類Isolation

在Spring中事務的隔離級別也被封裝成了一個枚舉類org.springframework.transaction.annotation包下的Isolation類,同樣的這個類枚舉類也是與@Transactional結合使用,這個枚舉類中定義的事務隔離級別與TransactionDefinition接口下定義的事務隔離級別相對應。

貼一下源碼:


package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction isolation levels for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Isolation {
	...
}

Spring中定義的五個隔離級別選項

  • ISOLATION_DEFAULT

使用數據庫的默認級別,對於Oracle來說默認事務隔離級別是讀已提交,對於MySQL來說默認事務隔離級別是可重複讀,使用方式就是@Transactional(isolation = Isolation.DEFAULT),看下源碼註釋


	/**
	 * Use the default isolation level of the underlying datastore.
	 * All other levels correspond to the JDBC isolation levels.
	 * @see java.sql.Connection
	 */
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

  • ISOLATION_READ_UNCOMMITTED

讀未提交,含義上文有講,這裏不再贅述,看下源碼註釋


	/**
	 * A constant indicating that dirty reads, non-repeatable reads and phantom reads
	 * can occur. This level allows a row changed by one transaction to be read by
	 * another transaction before any changes in that row have been committed
	 * (a "dirty read"). If any of the changes are rolled back, the second
	 * transaction will have retrieved an invalid row.
	 * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED
	 */
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),


  • ISOLATION_READ_COMMITTED

讀已提交,源碼註釋如下


	/**
	 * A constant indicating that dirty reads are prevented; non-repeatable reads
	 * and phantom reads can occur. This level only prohibits a transaction
	 * from reading a row with uncommitted changes in it.
	 * @see java.sql.Connection#TRANSACTION_READ_COMMITTED
	 */
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

  • ISOLATION_REPEATABLE_READ

可重複讀,源碼註釋如下


	/**
	 * A constant indicating that dirty reads are prevented; non-repeatable reads
	 * and phantom reads can occur. This level only prohibits a transaction
	 * from reading a row with uncommitted changes in it.
	 * @see java.sql.Connection#TRANSACTION_READ_COMMITTED
	 */
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

  • ISOLATION_SERIALIZABLE

串行化,源碼註釋如下


	/**
	 * A constant indicating that dirty reads, non-repeatable reads and phantom
	 * reads are prevented. This level includes the prohibitions in
	 * {@code ISOLATION_REPEATABLE_READ} and further prohibits the situation
	 * where one transaction reads all rows that satisfy a {@code WHERE}
	 * condition, a second transaction inserts a row that satisfies that
	 * {@code WHERE} condition, and the first transaction rereads for the
	 * same condition, retrieving the additional "phantom" row in the second read.
	 * @see java.sql.Connection#TRANSACTION_SERIALIZABLE
	 */
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

**總結:**在本文中已經較爲詳細的講解了事務的集中隔離級別和會引發的問題,從事務的隔離級別的設計中我們也能體會到,在數據安全和性能中,設計者也是一直在找一個平衡點,絕對的數據安全,就會導致性能變慢,同樣追求絕對的性能,數據的安全和準確性就得不到保障。所以設計並不是一個非黑即白的,我們自己在設計東西的時候,是要考慮需求的全面性,然後根據現實再去設計,在不同的方面作取捨,這樣或許纔是一個好的功能和產品。

文章歡迎轉載,轉載請註明出處,個人公衆號【愛做夢的錘子】,全網同id,個站 http://te-amo.site,歡迎關注,裏面會分享更多有用知識,還有我的私密照片

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章