MySQL事務(wù)隔離級(jí)別和Spring事務(wù)關(guān)系介紹

Good Goodbye - Linkin Park



事務(wù)隔離級(jí)別介紹


  1. 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會(huì)話中未提交事務(wù)修改的數(shù)據(jù)
  2. 提交讀(Read Committed):只能讀取到已經(jīng)提交的數(shù)據(jù)。Oracle等多數(shù)數(shù)據(jù)庫默認(rèn)都是該級(jí)別 (不重復(fù)讀)
  3. 可重復(fù)讀(Repeated Read):可重復(fù)讀。在同一個(gè)事務(wù)內(nèi)的查詢都是事務(wù)開始時(shí)刻一致的,InnoDB默認(rèn)級(jí)別。在SQL標(biāo)準(zhǔn)中,該隔離級(jí)別消除了不可重復(fù)讀,但是還存在幻象讀,但是innoDB解決了幻讀
  4. 串行讀(Serializable):完全串行化的讀,每次讀都需要獲得表級(jí)共享鎖,讀寫相互都會(huì)阻塞

接下來一次來驗(yàn)證每個(gè)隔離級(jí)別的特性,首先我們先建一張表,我們建立賬戶表account用來測(cè)試我們的事務(wù)隔離級(jí)別:

CREATE TABLE account (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `customer_name` varchar(255) NOT NULL,
    `money` int(11) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE `uniq_name` USING BTREE (customer_name)
) ENGINE=`InnoDB` AUTO_INCREMENT=10 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRIT



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

首先我們開啟Console A,然后設(shè)置session事務(wù)隔離級(jí)別為read uncommitted; 然后同樣開啟Console B,設(shè)置成read uncommitted;

mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.03 sec)
 
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED       |
+------------------------+
1 rows in set (0.03 sec)

我們兩個(gè)console的事務(wù)隔離級(jí)別都是read uncommitted,下面測(cè)試RU級(jí)別會(huì)發(fā)生的情況



小結(jié):

可以發(fā)現(xiàn)RU模式下,一個(gè)事務(wù)可以讀取到另一個(gè)未提交(commit)的數(shù)據(jù),導(dǎo)致了臟讀。如果B事務(wù)回滾了,就會(huì)造成數(shù)據(jù)的不一致。RU是事務(wù)隔離級(jí)別最低的。

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

現(xiàn)在我們將事務(wù)隔離級(jí)別設(shè)置成RC (read committed)

set session transaction isolation level read uncommitted;



小結(jié)

我們?cè)赗C模式下,可以發(fā)現(xiàn)。在console B沒有提交數(shù)據(jù)修改的commit的時(shí)候,console A是讀不到修改后的數(shù)據(jù)的,這就避免了在RU模式中的臟讀,但是有一個(gè)問題我們會(huì)發(fā)現(xiàn),在console A同一個(gè)事務(wù)中。兩次select的數(shù)據(jù)不一樣,這就存在了不可重復(fù)讀的問題.PS:RC事務(wù)隔離級(jí)別是Oracle數(shù)據(jù)庫的默認(rèn)隔離級(jí)別.

RR (Repeatable read)可重復(fù)讀隔離級(jí)別

小結(jié):

在RR級(jí)別中,我們解決了不可重復(fù)讀的問題,即在這種隔離級(jí)別下,在一個(gè)事務(wù)中我們能夠保證能夠獲取到一樣的數(shù)據(jù)(即使已經(jīng)有其他事務(wù)修改了我們的數(shù)據(jù))。但是無法避免幻讀,幻讀簡(jiǎn)單的解釋就是在數(shù)據(jù)有新增的時(shí)候,也無法保證兩次得到的數(shù)據(jù)不一致,但是不同數(shù)據(jù)庫對(duì)不同的RR級(jí)別有不同的實(shí)現(xiàn),有時(shí)候或加上間隙鎖來避免幻讀。

innoDB 解決了幻讀

前面的定義中RR級(jí)別是可能產(chǎn)生幻讀,這是在傳統(tǒng)的RR級(jí)別定義中會(huì)出現(xiàn)的。但是在innoDB引擎中利用MVCC多版本并發(fā)控制解決了這個(gè)問題

這算是幻讀嗎?在標(biāo)準(zhǔn)的RR隔離級(jí)別定義中是無法解決幻讀問題的,比如我要保證可重復(fù)讀,那么我們可以在我們的結(jié)果集的范圍加一個(gè)鎖(between 1 and 11),防止數(shù)據(jù)更改.但是我們畢竟不是鎖住真?zhèn)€表,所以insert數(shù)據(jù)我們并不能保證他不插入。所以是有幻讀的問題存在的。但是innodb引擎解決了幻讀的問題,基于MVCC(多版本并發(fā)控制):在InnoDB中,會(huì)在每行數(shù)據(jù)后添加兩個(gè)額外的隱藏的值來實(shí)現(xiàn)MVCC,這兩個(gè)值一個(gè)記錄這行數(shù)據(jù)何時(shí)被創(chuàng)建,另外一個(gè)記錄這行數(shù)據(jù)何時(shí)過期(或者被刪除)。 在實(shí)際操作中,存儲(chǔ)的并不是時(shí)間,而是事務(wù)的版本號(hào),每開啟一個(gè)新事務(wù),事務(wù)的版本號(hào)就會(huì)遞增。所以當(dāng)我們執(zhí)行update的時(shí)候,當(dāng)前事務(wù)的版本號(hào)已經(jīng)更新了?所以也算是幻讀??(存疑)主要是gap間隙鎖+MVCC解決幻讀問題?

串行化隔離級(jí)別:

所有事物串行,最高隔離級(jí)別,性能最差

存在的問題?

在RR模型,我們雖然避免了幻讀,但是存在一個(gè)問題,我們得到的數(shù)據(jù)不是數(shù)據(jù)中實(shí)時(shí)的數(shù)據(jù),如果是對(duì)實(shí)時(shí)數(shù)據(jù)比較敏感的業(yè)務(wù),這是不現(xiàn)實(shí)的。 對(duì)于這種讀取歷史數(shù)據(jù)的方式,我們叫它快照讀 (snapshot read),而讀取數(shù)據(jù)庫當(dāng)前版本數(shù)據(jù)的方式,叫當(dāng)前讀 (current read)。很顯然,在MVCC中:

  • 快照讀:就是select
  • select * from table ….;
  • 當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,處理的都是當(dāng)前的數(shù)據(jù),需要加鎖。
  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert;
  • update ;
  • delete;

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

比如,我們有以下的訂單業(yè)務(wù)場(chǎng)景,我們隊(duì)一個(gè)商品下單的操作,我們得首先檢查這個(gè)訂單的數(shù)量還剩多少,然后下單。

事務(wù)1:

select num from t_goods where id=1;
update t_goods set num=num-$mynum where id=1;

事務(wù)2:

select num from t_goods where id=1;
update t_goods set num=num-$mynum where id=1;



1.假設(shè)這個(gè)時(shí)候數(shù)量只有1,我們下單也是只有1.如果在并發(fā)的情況下,事務(wù)1查詢到還有一單準(zhǔn)備下單,但是這個(gè)時(shí)候事務(wù)2已經(jīng)提交了。訂單變成0.這個(gè)事務(wù)1在執(zhí)行update,就會(huì)造成事故。

2.解決問題方法1(悲觀鎖):就是利用for update對(duì)著個(gè)商品加鎖,事務(wù)完成之后釋放鎖。切記where條件的有索引,否則會(huì)鎖全表。
解決方法2(樂觀鎖):給數(shù)據(jù)庫表加上個(gè)version字段。然后SQL改寫:

select num,version from t_goods where id=1;
update t_goods set num=num-1,version=verison+1 where id=1 and version=${version}


Spring管理事務(wù)的方式

編程式事務(wù)

編程式事務(wù)就是利用手動(dòng)代碼編寫事務(wù)相關(guān)的業(yè)務(wù)邏輯,這種方式比較復(fù)雜、啰嗦,但是更加靈活可控制(個(gè)人比較喜歡)

public void testTransactionTemplate() {
 
  TransactionTemplate transactionTemplate = new TransactionTemplate(txManager); 
  transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); //設(shè)置事務(wù)隔離級(jí)別
  transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設(shè)置為required傳播級(jí)別
  ....
  transactionTemplate.execute(new TransactionCallbackWithoutResult() { 
      @Override 
      protected void doInTransactionWithoutResult(TransactionStatus status) {  //事務(wù)塊
         jdbcTemplate.update(INSERT_SQL, "test"); 
  }}); 
  
}



聲明式事務(wù)

1.為了避免我們每次都手動(dòng)寫代碼,利用Spring AOP的方式對(duì)每個(gè)方法代理環(huán)繞,利用xml配置避免了寫代碼。

<tx:advice id="txAdvice" transaction-manager="txManager"> 
<tx:attributes>  <!--設(shè)置所有匹配的方法,然后設(shè)置傳播級(jí)別和事務(wù)隔離-->
           <tx:method name="save*" propagation="REQUIRED" /> 
           <tx:method name="add*" propagation="REQUIRED" /> 
           <tx:method name="create*" propagation="REQUIRED" /> 
           <tx:method name="insert*" propagation="REQUIRED" /> 
           <tx:method name="update*" propagation="REQUIRED" /> 
           <tx:method name="merge*" propagation="REQUIRED" /> 
           <tx:method name="del*" propagation="REQUIRED" /> 
           <tx:method name="remove*" propagation="REQUIRED" /> 
           <tx:method name="put*" propagation="REQUIRED" /> 
           <tx:method name="get*" propagation="SUPPORTS" read-only="true" /> 
           <tx:method name="count*" propagation="SUPPORTS" read-only="true" /> 
          <tx:method name="find*" propagation="SUPPORTS" read-only="true" /> 
          <tx:method name="list*" propagation="SUPPORTS" read-only="true" /> 
          <tx:method name="*" propagation="SUPPORTS" read-only="true" /> 
       </tx:attributes> 
</tx:advice> 
<aop:config> 
       <aop:pointcut id="txPointcut" expression="execution(* org.transaction..service.*.*(..))" /> 
       <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" /> 
</aop:config>

同時(shí)也可以用注解的方式

<tx:annotation-driven transaction-manager="transactioManager" /><!--開啟注解的方式-->

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
   @AliasFor("transactionManager")
   String value() default "";
   @AliasFor("value")
   String transactionManager() default "";
   Propagation propagation() default Propagation.REQUIRED;//傳播級(jí)別
  
   Isolation isolation() default Isolation.DEFAULT;//事務(wù)隔離級(jí)別
   int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;//事務(wù)超時(shí)時(shí)間
   boolean readOnly() default false;//只讀事務(wù)
   Class<? extends Throwable>[] rollbackFor() default {};//拋出哪些異常 會(huì)執(zhí)行回滾
   String[] rollbackForClassName() default {};
   Class<? extends Throwable>[] noRollbackFor() default {};
   String[] noRollbackForClassName() default {};//不回滾的異常名稱
 
}
//transaction注解可以放在方法上或者類上

我們?cè)谶@里不對(duì)兩種事務(wù)編程做過多的講解


Spring事務(wù)傳播:

事務(wù)傳播行為:

Spring管理的事務(wù)是邏輯事務(wù),而且物理事務(wù)和邏輯事務(wù)最大差別就在于事務(wù)傳播行為,事務(wù)傳播行為用于指定在多個(gè)事務(wù)方法間調(diào)用時(shí),事務(wù)是如何在這些方法間傳播的,Spring共支持7種傳播行為 為了演示事務(wù)傳播行為,我們新建一張用戶表

EATE TABLE user (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `username` varchar(255) NOT NULL,
    `pwd` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=`InnoDB` AUTO_INCREMENT=10 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0;



Required:

必須有邏輯事務(wù),否則新建一個(gè)事務(wù),使用PROPAGATION_REQUIRED指定,表示如果當(dāng)前存在一個(gè)邏輯事務(wù),則加入該邏輯事務(wù),否則將新建一個(gè)邏輯事務(wù),如下圖所示;

測(cè)試的代碼如下,在account插入的地方主動(dòng)回滾

public int insertAccount(final String customer, final int money) {
    transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設(shè)置為required傳播級(jí)別
   int re= transactionTemplate.execute(new TransactionCallback<Integer>() {
        public Integer doInTransaction( TransactionStatus status) {
            int i = accountDao.insertAccount(customer, money);
            status.setRollbackOnly();//主動(dòng)回滾
            return i;
        }
    });
    return re;
}
 public int inertUser(final String username, final String password) {
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設(shè)置為required傳播級(jí)別
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                int i = userDao.inertUser(username, password);
                int hahha = accountService.insertAccount("hahha", 2222);
//                status.setRollbackOnly();
                System.out.println("user==="+i);
                System.out.println("account===="+hahha);
            }
        });
        return 0;
    }

按照required的邏輯,代碼執(zhí)行的邏輯如下:

在調(diào)用userService對(duì)象的insert方法時(shí),此方法用的是Required傳播行為且此時(shí)Spring事務(wù)管理器發(fā)現(xiàn)還沒開啟邏輯事務(wù),因此Spring管理器覺得開啟邏輯事務(wù)
在此邏輯事務(wù)中調(diào)用了accountService對(duì)象的insert方法,而在insert方法中發(fā)現(xiàn)同樣用的是Required傳播行為,因此直接使用該已經(jīng)存在的邏輯事務(wù);
返回userService,執(zhí)行完并關(guān)閉事務(wù)
所以在這種情況下,兩個(gè)事務(wù)屬于同一個(gè)事務(wù),一個(gè)回滾則兩個(gè)任務(wù)都回滾。

RequiresNew:

創(chuàng)建新的邏輯事務(wù),使用PROPAGATION_REQUIRES_NEW指定,表示每次都創(chuàng)建新的邏輯事務(wù)(物理事務(wù)也是不同的)如下圖所示:

Supports:

支持當(dāng)前事務(wù),使用PROPAGATION_SUPPORTS指定,指如果當(dāng)前存在邏輯事務(wù),就加入到該邏輯事務(wù),如果當(dāng)前沒有邏輯事務(wù),就以非事務(wù)方式執(zhí)行,如下圖所示:

NotSupported:

不支持事務(wù),如果當(dāng)前存在事務(wù)則暫停該事務(wù),使用PROPAGATION_NOT_SUPPORTED指定,即以非事務(wù)方式執(zhí)行,如果當(dāng)前存在邏輯事務(wù),就把當(dāng)前事務(wù)暫停,以非事務(wù)方式執(zhí)行。

Mandatory:

必須有事務(wù),否則拋出異常,使用PROPAGATION_MANDATORY指定,使用當(dāng)前事務(wù)執(zhí)行,如果當(dāng)前沒有事務(wù),則拋出異常(IllegalTransactionStateException)。當(dāng)運(yùn)行在存在邏輯事務(wù)中則以當(dāng)前事務(wù)運(yùn)行,如果沒有運(yùn)行在事務(wù)中,則拋出異常

Never

不支持事務(wù),如果當(dāng)前存在是事務(wù)則拋出異常,使用PROPAGATION_NEVER指定,即以非事務(wù)方式執(zhí)行,如果當(dāng)前存在事務(wù),則拋出異常(IllegalTransactionStateException)

Nested:

嵌套事務(wù)支持,使用PROPAGATION_NESTED指定,如果當(dāng)前存在事務(wù),則在嵌套事務(wù)內(nèi)執(zhí)行,如果當(dāng)前不存在事務(wù),則創(chuàng)建一個(gè)新的事務(wù),嵌套事務(wù)使用數(shù)據(jù)庫中的保存點(diǎn)來實(shí)現(xiàn),即嵌套事務(wù)回滾不影響外部事務(wù),但外部事務(wù)回滾將導(dǎo)致嵌套事務(wù)回滾。

Nested和RequiresNew的區(qū)別:

  1. RequiresNew每次都創(chuàng)建新的獨(dú)立的物理事務(wù),而Nested只有一個(gè)物理事務(wù);
  2. Nested嵌套事務(wù)回滾或提交不會(huì)導(dǎo)致外部事務(wù)回滾或提交,但外部事務(wù)回滾將導(dǎo)致嵌套事務(wù)回滾,而 RequiresNew由于都是全新的事務(wù),所以之間是無關(guān)聯(lián)的;
  3. Nested使用JDBC 3的保存點(diǎn)(save point)實(shí)現(xiàn),即如果使用低版本驅(qū)動(dòng)將導(dǎo)致不支持嵌套事務(wù)。

使用嵌套事務(wù),必須確保具體事務(wù)管理器實(shí)現(xiàn)的nestedTransactionAllowed屬性為true,否則不支持嵌套事務(wù),如DataSourceTransactionManager默認(rèn)支持,而HibernateTransactionManager默認(rèn)不支持,需要設(shè)置來開啟。

原文: MySQL事務(wù)隔離級(jí)別和Spring事務(wù)關(guān)系介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容