歡迎光臨
每天分享高質量文章

MySQL事務隔離級別和Spring事務關係介紹

點選上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

精品專欄

 

事務隔離級別介紹

隔離級別 臟讀 不可重覆讀 幻讀
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重覆讀(Repeatable read) 不可能 不可能 可能
可序列化(Serializable ) 不可能 不可能 不可能
  1. 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會話中未提交事務修改的資料

  2. 提交讀(Read Committed):只能讀取到已經提交的資料。Oracle等多數資料庫預設都是該級別 (不重覆讀)

  3. 可重覆讀(Repeated Read):可重覆讀。在同一個事務內的查詢都是事務開始時刻一致的,InnoDB預設級別。在SQL標準中,該隔離級別消除了不可重覆讀,但是還存在幻象讀,但是innoDB解決了幻讀

  4. 序列讀(Serializable):完全序列化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞

接下來一次來驗證每個隔離級別的特性,首先我們先建一張表,我們建立賬戶表account用來測試我們的事務隔離級別:

RU (read uncommitted)讀未提交隔離級別

首先我們開啟Console A,然後設定session事務隔離級別為read uncommitted; 然後同樣開啟Console B,設定成read uncommitted;

  1. mysql> set session transaction isolation level read uncommitted;

  2. Query OK, 0 rows affected (0.03 sec)

  3. mysql> select @@session.tx_isolation;

  4. +------------------------+

  5. | @@session.tx_isolation |

  6. +------------------------+

  7. | READ-UNCOMMITTED       |

  8. +------------------------+

  9. 1 rows in set (0.03 sec)

我們兩個console的事務隔離級別都是read uncommitted,下麵測試RU級別會發生的情況

可以發現RU樣式下,一個事務可以讀取到另一個未提交(commit)的資料,導致了臟讀。如果B事務回滾了,就會造成資料的不一致。RU是事務隔離級別最低的。

RC (read committed)讀提交隔離級別

現在我們將事務隔離級別設定成RC (read committed)

  1. set session transaction isolation level read uncommitted;

小結

我們在 RC 樣式下,可以發現。在 console B 沒有提交資料修改的 commit 的時候,console A 是讀不到修改後的資料的,這就避免了在 RU 樣式中的臟讀,但是有一個問題我們會發現,在 console A 同一個事務中。兩次 select 的資料不一樣,這就存在了不可重覆讀的問題. PS:RC事務隔離級別是 Oracle資料庫的預設隔離級別.

RR (Repeatable read)可重覆讀隔離級別

小結

在 RR 級別中,我們解決了不可重覆讀的問題,即在這種隔離級別下,在一個事務中我們能夠保證能夠獲取到一樣的資料(即使已經有其他事務修改了我們的資料)。但是無法避免幻讀,幻讀簡單的解釋就是在資料有新增的時候,也無法保證兩次得到的資料不一致,但是不同資料庫對不同的 RR 級別有不同的實現,有時候或加上間隙鎖來避免幻讀。

innoDB 解決了幻讀

前面的定義中 RR 級別是可能產生幻讀,這是在傳統的RR級別定義中會出現的。但是在innoDB引擎中利用MVCC多版本併發控制解決了這個問題

這算是幻讀嗎?在標準的RR隔離級別定義中是無法解決幻讀問題的,比如我要保證可重覆讀,那麼我們可以在我們的結果集的範圍加一個鎖(between 1 and 11),防止資料更改.但是我們畢竟不是鎖住真個表,所以insert資料我們並不能保證他不插入。所以是有幻讀的問題存在的。但是innodb引擎解決了幻讀的問題,基於MVCC(多版本併發控制):在InnoDB中,會在每行資料後新增兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行資料何時被建立,另外一個記錄這行資料何時過期(或者被刪除)。 在實際操作中,儲存的並不是時間,而是事務的版本號,每開啟一個新事務,事務的版本號就會遞增。所以當我們執行update的時候,當前事務的版本號已經更新了?所以也算是幻讀??(存疑)主要是gap間隙鎖+MVCC解決幻讀問題?

序列化隔離級別:

所有事物序列,最高隔離級別,效能最差

存在的問題?

在RR模型,我們雖然避免了幻讀,但是存在一個問題,我們得到的資料不是資料中實時的資料,如果是對實時資料比較敏感的業務,這是不現實的。
對於這種讀取歷史資料的方式,我們叫它快照讀 (snapshot read),而讀取資料庫當前版本資料的方式,叫當前讀 (current read)。很顯然,在MVCC中:

  • 快照讀:就是select

  • select * from table ….;

  • 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,處理的都是當前的資料,需要加鎖。

  • select * from table where ? lock in share mode;

  • select * from table where ? for update;

  • insert;

  • update ;

  • delete;

事務的隔離級別實際上都是定義了當前讀的級別,MySQL為了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些“當前讀”,就需要另外的模組來解決了。

比如,我們有以下的訂單業務場景,我們隊一個商品下單的操作,我們得首先檢查這個訂單的數量還剩多少,然後下單。

事務1:

  1. select num from t_goods where id=1;

  2. update t_goods set num=num-$mynum where id=1;

事務2:

  1. select num from t_goods where id=1;

  2. update t_goods set num=num-$mynum where id=1;

假設這個時候數量只有1,我們下單也是隻有1.如果在併發的情況下,事務1查詢到還有一單準備下單,但是這個時候事務2已經提交了。訂單變成0.這個事務1在執行update,就會造成事故。

  1. 解決問題方法1(悲觀鎖):就是利用for update對著個商品加鎖,事務完成之後釋放鎖。切記where條件的有索引,否則會鎖全表。

  2. 解決方法2(樂觀鎖):給資料庫表加上個version欄位。然後SQL改寫:

  1. select num,version from t_goods where id=1;

  2. update t_goods set num=num-1,version=verison+1 where id=1 and version=${version}

Spring管理事務的方式。

程式設計式事務

程式設計式事務就是利用手動程式碼編寫事務相關的業務邏輯,這種方式比較複雜、囉嗦,但是更加靈活可控制(個人比較喜歡)

  1. public void testTransactionTemplate() {

  2. TransactionTemplate transactionTemplate = new TransactionTemplate(txManager);

  3. transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); //設定事務隔離級別

  4. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設定為required傳播級別

  5. ....

  6. transactionTemplate.execute(new TransactionCallbackWithoutResult() {

  7. @Override

  8. protected void doInTransactionWithoutResult(TransactionStatus status) {  //事務塊

  9. jdbcTemplate.update(INSERT_SQL, "test");

  10. }});

  11. }

宣告式事務

  1. 為了避免我們每次都手動寫程式碼,利用Spring AOP的方式對每個方法代理環繞,利用xml配置避免了寫程式碼。

  1. id="txAdvice" transaction-manager="txManager">

  2.  

  3. name="save*" propagation="REQUIRED" />

  4. name="add*" propagation="REQUIRED" />

  5. name="create*" propagation="REQUIRED" />

  6. name="insert*" propagation="REQUIRED" />

  7. name="update*" propagation="REQUIRED" />

  8. name="merge*" propagation="REQUIRED" />

  9. name="del*" propagation="REQUIRED" />

  10. name="remove*" propagation="REQUIRED" />

  11. name="put*" propagation="REQUIRED" />

  12. name="get*" propagation="SUPPORTS" read-only="true" />

  13. name="count*" propagation="SUPPORTS" read-only="true" />

  14. name="find*" propagation="SUPPORTS" read-only="true" />

  15. name="list*" propagation="SUPPORTS" read-only="true" />

  16. name="*" propagation="SUPPORTS" read-only="true" />

  • id="txPointcut" expression="execution(* org.transaction..service.*.*(..))" />

  • advice-ref="txAdvice" pointcut-ref="txPointcut" />

    1. 同時也可以用註解的方式

    1. transaction-manager="transactioManager" />

    1. @Target({ElementType.METHOD, ElementType.TYPE})

    2. @Retention(RetentionPolicy.RUNTIME)

    3. @Inherited

    4. @Documented

    5. public @interface Transactional {

    6. @AliasFor("transactionManager")

    7. String value() default "";

    8. @AliasFor("value")

    9. String transactionManager() default "";

    10. Propagation propagation() default Propagation.REQUIRED;//傳播級別

    11. Isolation isolation() default Isolation.DEFAULT;//事務隔離級別

    12. int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;//事務超時時間

    13. boolean readOnly() default false;//只讀事務

    14. Class extends Throwable>[] rollbackFor() default {};//丟擲哪些異常 會執行回滾

    15. String[] rollbackForClassName() default {};

    16. Class extends Throwable>[] noRollbackFor() default {};

    17. String[] noRollbackForClassName() default {};//不回滾的異常名稱

    18. }

    19. //transaction註解可以放在方法上或者類上

    我們在這裡不對兩種事務程式設計做過多的講解

    Spring事務傳播:

    事務傳播行為:

    Spring管理的事務是邏輯事務,而且物理事務和邏輯事務最大差別就在於事務傳播行為,事務傳播行為用於指定在多個事務方法間呼叫時,事務是如何在這些方法間傳播的,Spring共支援7種傳播行為
    為了演示事務傳播行為,我們新建一張使用者表

    1. EATE TABLE user (

    2. `id` int(11) NOT NULL AUTO_INCREMENT,

    3. `username` varchar(255) NOT NULL,

    4. `pwd` varchar(255) NOT NULL,

    5. PRIMARY KEY (`id`)

    6. ) ENGINE=`InnoDB` AUTO_INCREMENT=10 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0;

    Required

    必須有邏輯事務,否則新建一個事務,使用PROPAGATION_REQUIRED指定,表示如果當前存在一個邏輯事務,則加入該邏輯事務,否則將新建一個邏輯事務,如下圖所示;

    測試的程式碼如下,在account插入的地方主動回滾

    1. public int insertAccount(final String customer, final int money) {

    2. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設定為required傳播級別

    3. int re= transactionTemplate.execute(new TransactionCallback<Integer>() {

    4. public Integer doInTransaction( TransactionStatus status) {

    5. int i = accountDao.insertAccount(customer, money);

    6. status.setRollbackOnly();//主動回滾

    7. return i;

    8. }

    9. });

    10. return re;

    11. }

    12. public int inertUser(final String username, final String password) {

    13. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設定為required傳播級別

    14. transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    15. @Override

    16. protected void doInTransactionWithoutResult(TransactionStatus status) {

    17. int i = userDao.inertUser(username, password);

    18. int hahha = accountService.insertAccount("hahha", 2222);

    19. //                status.setRollbackOnly();

    20. System.out.println("user==="+i);

    21. System.out.println("account===="+hahha);

    22. }

    23. });

    24. return 0;

    25. }

    按照required的邏輯,程式碼執行的邏輯如下:

    1. 在呼叫userService物件的insert方法時,此方法用的是Required傳播行為且此時Spring事務管理器發現還沒開啟邏輯事務,因此Spring管理器覺得開啟邏輯事務

    2. 在此邏輯事務中呼叫了accountService物件的insert方法,而在insert方法中發現同樣用的是Required傳播行為,因此直接使用該已經存在的邏輯事務;

    3. 傳回userService,執行完並關閉事務

    所以在這種情況下,兩個事務屬於同一個事務,一個回滾則兩個任務都回滾。

    RequiresNew

    建立新的邏輯事務,使用PROPAGATIONREQUIRESNEW指定,表示每次都建立新的邏輯事務(物理事務也是不同的)如下圖所示:

    Supports

    支援當前事務,使用PROPAGATION_SUPPORTS指定,指如果當前存在邏輯事務,就加入到該邏輯事務,如果當前沒有邏輯事務,就以非事務方式執行,如下圖所示:

    NotSupported

    不支援事務,如果當前存在事務則暫停該事務,使用PROPAGATIONNOTSUPPORTED指定,即以非事務方式執行,如果當前存在邏輯事務,就把當前事務暫停,以非事務方式執行。

    Mandatory

    必須有事務,否則丟擲異常,使用PROPAGATION_MANDATORY指定,使用當前事務執行,如果當前沒有事務,則丟擲異常(IllegalTransactionStateException)。當執行在存在邏輯事務中則以當前事務執行,如果沒有執行在事務中,則丟擲異常

    Never

    不支援事務,如果當前存在是事務則丟擲異常,使用PROPAGATION_NEVER指定,即以非事務方式執行,如果當前存在事務,則丟擲異常(IllegalTransactionStateException)

    Nested

    巢狀事務支援,使用PROPAGATION_NESTED指定,如果當前存在事務,則在巢狀事務內執行,如果當前不存在事務,則建立一個新的事務,巢狀事務使用資料庫中的儲存點來實現,即巢狀事務回滾不影響外部事務,但外部事務回滾將導致巢狀事務回滾。

    Nested和RequiresNew的區別

    1. RequiresNew每次都建立新的獨立的物理事務,而Nested只有一個物理事務;

    2. Nested巢狀事務回滾或提交不會導致外部事務回滾或提交,但外部事務回滾將導致巢狀事務回滾,而 RequiresNew由於都是全新的事務,所以之間是無關聯的;

    3. Nested使用JDBC 3的儲存點(save point)實現,即如果使用低版本驅動將導致不支援巢狀事務。

    4. 使用巢狀事務,必須確保具體事務管理器實現的nestedTransactionAllowed屬性為true,否則不支援巢狀事務,如DataSourceTransactionManager預設支援,而HibernateTransactionManager預設不支援,需要設定來開啟。

    【死磕Sharding-jdbc】— 死磕 Sharding-jdbc 精品合集

    9個提升逼格的redis命令

    你的JVM還好嗎?GC初步診斷

    一個簡單java程式的執行全過程

    AOP 的利器:ASM 3.0 介紹

    END

    贊(0)

    分享創造快樂

    © 2024 知識星球   網站地圖