Spring事務(wù)

1. 數(shù)據(jù)庫(kù)事務(wù)特征

1.1 ACID特性

事務(wù) (Transaction) 是數(shù)據(jù)庫(kù)系統(tǒng)中一系列操作的一個(gè)邏輯單元,所有操作要么全部成功要么全部失敗。

事務(wù)是區(qū)分文件存儲(chǔ)系統(tǒng)Nosql 數(shù)據(jù)庫(kù)重要特性之一,其存在的意義是為了保證即使在并發(fā)情況下也能掙錢的執(zhí)行crud操作。怎樣才算是正確的呢?這時(shí)提出了事務(wù)需要保證的四個(gè)特性即ACID

  • A:原子性(atomicity)
    一個(gè)事務(wù) (transaction) 中的所有操作,要么全部完成,要么全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié)。事務(wù)在執(zhí)行過程中發(fā)生錯(cuò)誤,會(huì)被回滾 (Rollback) 到事務(wù)開始前的狀態(tài),就像這個(gè)事務(wù)從來(lái)沒有執(zhí)行過一樣。

    原子性表現(xiàn)為操作不能被分割。

  • C:一致性(consistency)
    在事務(wù)開始之前和事務(wù)結(jié)束以后,數(shù)據(jù)庫(kù)的完整性沒有被破壞。數(shù)據(jù)庫(kù)要一直處于一致的狀態(tài),事務(wù)開始前是一個(gè)一致狀態(tài),事務(wù)結(jié)束后是另一個(gè)一致狀態(tài),事務(wù)將數(shù)據(jù)庫(kù)從一個(gè)一致狀態(tài)轉(zhuǎn)移到另一個(gè)一致狀態(tài)。

  • I:隔離性(isolation)
    數(shù)據(jù)庫(kù)允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)其數(shù)據(jù)進(jìn)行讀寫和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致。事務(wù)隔離分為不同級(jí)別,包括讀未提交 (read uncommitted)、讀已提交(read committed)、可重復(fù)讀 (repeatable read) 和串行化 (Serializable)。

  • D:持久性(durability)
    事務(wù)外理結(jié)束后,對(duì)數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會(huì)丟失。

擴(kuò)展

WAL原則

InnoDB 的 ARIES 三原則 Write Ahead Logging (WAL)

  • 日志成功寫入后事務(wù)就不會(huì)丟失,后續(xù)由checkpoint機(jī)制來(lái)保證磁盤物理文件與redo log達(dá)到一致性;
  • 利用redo log來(lái)記錄變更后的數(shù)據(jù),即redo里記錄事務(wù)數(shù)據(jù)變更后的值;
  • 利用undo log來(lái)記錄變更前的數(shù)據(jù),即undo里記錄事務(wù)數(shù)據(jù)變更前的值,用于回滾和其他事務(wù)多版本讀。

并發(fā)事務(wù)控制

  • 單版本控制-鎖
    鎖用獨(dú)占的方式來(lái)保證在只有一個(gè)版本的情況下事務(wù)之間相互隔離。

    在MySQL事務(wù)中,鎖的實(shí)現(xiàn)與隔離級(jí)別有關(guān)系,在RR (Repeatable Read) 隔離級(jí)別下,MySQL為了解

    決幻讀的問題,以犧牲并行度為代價(jià),通過 Gap 鎖來(lái)防止數(shù)據(jù)的寫入,而這種鎖,因?yàn)槠洳⑿卸炔粔颍?/p>

    沖突很多,經(jīng)常會(huì)引起死鎖?,F(xiàn)在流行的 Row 模式可以避免很多沖突甚至死鎖問題,所以推薦默認(rèn)使用Row+

    RC (Read Committed) 模式的隔離級(jí)別,可以很大程度上提高數(shù)據(jù)庫(kù)的讀寫并行度。

  • 多版本控制 - MVCC

    在數(shù)據(jù)庫(kù)中,為了實(shí)現(xiàn)高并發(fā)的數(shù)據(jù)訪問,對(duì)數(shù)據(jù)進(jìn)待多版本處理,并通過事務(wù)的可見性來(lái)保證事務(wù)能看到自己應(yīng)該看到的數(shù)據(jù)版本。MVCC最大的好處是讀不加鎖,讀寫不沖突。

    每一次對(duì)數(shù)據(jù)庫(kù)的修改,都會(huì)在 Undo 日志中記錄當(dāng)前修改記錄的事務(wù)號(hào)及修改前數(shù)據(jù)狀態(tài)的存儲(chǔ)地址(即ROLL_PTR),以便在必要的時(shí)候可以回滾到老的數(shù)據(jù)版本。例如,一個(gè)讀事務(wù)查詢到當(dāng)前記錄,而最新的事務(wù)還未提交,根據(jù)原子性,讀事務(wù)看不到最新數(shù)據(jù)。

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

在高并發(fā)的情況下,要完全保證其ACID特性是非常困難的,除非把所有的事務(wù)串行化執(zhí)行,但帶來(lái)的負(fù)面的影響將是性能大打折扣。很多時(shí)候我們有些業(yè)務(wù)對(duì)事務(wù)的要求是不一樣的,所以數(shù)據(jù)庫(kù)中設(shè)計(jì)了四種隔離級(jí)別,供用戶基于業(yè)務(wù)進(jìn)行選擇。

數(shù)據(jù)庫(kù)默認(rèn)隔離級(jí)別:
  • Oracle中默認(rèn)級(jí)別:Read committed
  • MySQL中默認(rèn)級(jí)別:Repeatable read(在數(shù)據(jù)庫(kù)內(nèi)均表現(xiàn)為大寫)
# 查看 mysql 的默認(rèn)隔離級(jí)別
SELECT @@tx_isolation

# 設(shè)置為讀未提交
SET tx_isolation='read-uncommitted';

# 設(shè)置為讀已提交
SET tx_isolation='read-committed';

# 設(shè)置為可重復(fù)讀
SET tx_isolation='REPEATABLE-READ';

# 設(shè)置為串行化
SET tx_isolation='SERIALIZABLE'
隔離級(jí)別 臟讀(Dirty Read) 不可重復(fù)讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重復(fù)讀(Repeatable read) 不可能 不可能 可能
可串行化(Serializable) 不可能 不可能 不可能
臟讀
   一個(gè)事務(wù)讀取到另一個(gè)事務(wù)未提交的更新數(shù)據(jù)

session_1

# 設(shè)置為讀未提交
SET tx_isolation='read-uncommitted';

BEGIN;

INSERT INTO `tbl_employee` (`last_name`,`email`,  `gender`)
    VALUES ( '2222', '111111@qq.com', '1' );
# 1. 執(zhí)行到此處,執(zhí)行session_2

ROLLBACK;
# 3. 執(zhí)行到此處,再執(zhí)行session_2

COMMIT;  -- 無(wú)用

session_2

# 設(shè)置為讀未提交
SET tx_isolation='read-uncommitted';

SELECT * FROM `tbl_employee`;
# 2. 執(zhí)行完成,session_1 事務(wù)未提交,卻讀出了數(shù)據(jù);回到session_1執(zhí)行回滾
# 4. 此時(shí)查詢不到數(shù)據(jù),說(shuō)明第2步出現(xiàn)臟讀
不可重復(fù)讀
   在同一事務(wù)中,多次讀取同一數(shù)據(jù)返回的結(jié)果有所不同,換句話說(shuō),后續(xù)讀取可以讀到另一事務(wù)已提交的更新數(shù)據(jù)。相反,“可重復(fù)讀”在同一事務(wù)中多次讀取數(shù)據(jù)時(shí),能夠保證所讀數(shù)據(jù)一樣,也就是后續(xù)讀取不能讀到另一事務(wù)已提交的更新數(shù)據(jù)。

事務(wù)B修改數(shù)據(jù)導(dǎo)致當(dāng)前事務(wù)A前后讀取數(shù)據(jù)不一致,側(cè)重點(diǎn)在于事務(wù)B的修改

當(dāng)前事務(wù)讀到了其他事務(wù)修改的數(shù)據(jù)

session_1

# 設(shè)置為讀已提交
SET tx_isolation='read-committed';

BEGIN;

SELECT * FROM `tbl_employee`;
# 1. 執(zhí)行session_2中內(nèi)容

SELECT * FROM `tbl_employee`;

# 3. 和前面查詢的數(shù)據(jù)不一致
COMMIT;

session_2

# 設(shè)置為讀已提交
SET tx_isolation='read-committed';

UPDATE `tbl_employee` SET email = "222222@qq.com" WHERE id = 5;
# 2. 執(zhí)行session_1中內(nèi)容
幻讀
    查詢表中一條數(shù)據(jù),如果不存在就插入一條,并發(fā)的時(shí)候卻發(fā)現(xiàn),里面居然有兩條相同的數(shù)據(jù)。

    事務(wù)A修改表中數(shù)據(jù),此時(shí)事務(wù)B插入一條新數(shù)據(jù),事務(wù)A查詢發(fā)現(xiàn)表中還有沒修改的數(shù)據(jù),像是出現(xiàn)幻覺

    事務(wù)A讀到了事務(wù)B新增的數(shù)據(jù),導(dǎo)致結(jié)果不一致,側(cè)重點(diǎn)在于事務(wù)B新增數(shù)據(jù)

session_1

# 設(shè)置為可重復(fù)讀
SET tx_isolation='REPEATABLE-READ';

BEGIN;

SELECT * FROM `tbl_employee` WHERE last_name = "jack";

# 1. 此時(shí)在session_2事務(wù)插入數(shù)據(jù)

SELECT * FROM `tbl_employee` WHERE last_name = "jack";

INSERT INTO `tbl_employee` (`last_name`,`email`,  `gender`)
    VALUES ( 'jack', '333333333@qq.com', '1' );
    
SELECT * FROM `tbl_employee` WHERE last_name = "jack";  

UPDATE `tbl_employee` SET email = "444444444@qq.com" WHERE last_name = "jack";

SELECT * FROM `tbl_employee` WHERE last_name = "jack";

COMMIT;

session_2

# 設(shè)置為可重復(fù)讀
SET tx_isolation='REPEATABLE-READ';

INSERT INTO `tbl_employee` (`last_name`,`email`,  `gender`)
    VALUES ( 'jack', '1111111@qq.com', '1' );

2. Spring事務(wù)應(yīng)用及源碼分析

2.1 Spring事務(wù)相關(guān)API

Spring 事務(wù)是在數(shù)據(jù)庫(kù)事務(wù)的基礎(chǔ)上進(jìn)行封裝擴(kuò)展,其主要特性如下:

  • 支持原有的數(shù)據(jù)庫(kù)事務(wù)的隔離級(jí)別,加入了事務(wù)傳播的概念
  • 提供多個(gè)事務(wù)的合并或隔離的功能
  • 提供聲明式事務(wù),讓業(yè)務(wù)代碼與事務(wù)分離,事務(wù)變得更易用(AOP)

Spring 提供了事務(wù)相關(guān)接口:

  • TransactionDefinition

    事務(wù)定義:事務(wù)的隔離級(jí)別和事務(wù)的傳播行為

  • TransactionAttribute

    事務(wù)屬性,實(shí)現(xiàn)了對(duì)回滾規(guī)則的擴(kuò)展(外理異常)

  • PlatformTransactionManager

    事務(wù)管理器

  • TransactionStatus

    事務(wù)運(yùn)行時(shí)狀態(tài)

相關(guān)實(shí)現(xiàn)類

  • TransactionIntercenter

    事務(wù)攔截器,實(shí)現(xiàn)了 MethodInterceptor

  • TransactionAspectSupport

    事務(wù)切面支持,內(nèi)部類Transactionlnfo封裝了事務(wù)相關(guān)屬性
    TransactionAspectSupport.Transactionlnfo

2.2 編程式事務(wù)

public class SpringTransactionExample {
    private static String url = "jdbc:mysql://127.0.0.1:3306/test";
    private static String user = "root";
    private static String password = "root";
    
    public static void main(string[] args) {
      // 獲取數(shù)據(jù)源
      final Datasource ds = new DriverManagerDatasource(url, user, password);
      //編程式事務(wù)
      final TransactionTemplate template = new TransactionTemplate();
      // 設(shè)置事務(wù)管理器
      template.setTransactionManager(new DataSourceTransactionManager(ds));
        
      template.execute(new TransactionCa11back<0bject>() {
         @override
         public object doInTransaction(Transactionstatus status) {
             Connection conn = DatasourceUtils.getConnection(ds);
             object savePoint = null;
             try {
                 {
                    // 插入
                    PreparedStatement prepare = conn.preparestatement("insert INTO account (accountName,user,money) VALUES (?, ?, ?)");
                    preparp.setstring(1, "111");
                    prepare.setstring(2, "aaa");
                    prepare.setInt(3, 10000);
                    prepare.executeUpdate():;
                 }
                 //設(shè)置保存點(diǎn)
                savepoint = status.createSavepoint();
                {
                   //捕入
                   PreparedStatement prepare = conn.preparestatement("insert INTO account (accountName.user,money) VALUES (?, ?, ?)");
                    prepare.setstring(1,"222");
                    prepare.setstring(2, "bbb");
                    prepare.setInt(3, 10000);
                    prepare.executeupdate() ; 
                }
                {
                    //更新
                    Preparedstatement prepare = conn.preparestatement("UPDATE account SET money= money+100 where user=?");
                    prepare.setstring(1, "aaa");
                    prepare.executeUpdate();
                    //int i=1/0;
                }
             } catch (SQLException e) {
                e.printstackTrace();
             } catch (Exception e) {
                System.out.print1n("更新失敗");
                if (savePoint != null) [
                    status.ro11backTosavepoint(savePoint);
                } else {
                    status.setRollbackOnly();
                }  
             }
             return null;
       }
    });
}

2.3 聲明式事務(wù)

Xml配置

添加tx名字空間

xmlns:tx="http://www.springframework.org/schema/tx"

配置事務(wù)管理器和數(shù)據(jù)源

<bean id="datasource"
  class="org.springframework.jdbc.datasource.DriverManagerDatasource">
    <constructor-arg name="ur1" value="jdbc:mysq1://127 .0.0.1/test"/>
    <constructor-arg name="username" va]ue="root"/>
    <constructor-arg name="password" value="root"/>
</bean>
<bean id="txManager"
  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="datasource" ref="datasource"/>
</bean>

事務(wù)切面配置

<!-- 事物切面配置 -->
<tx:advice id="advice" transaction-manager="txManager"
    <tx:attributes>
        <tx:method name="update*" propagation-"REQUIRED" read-only="false" rollback-for="Java.1ang.Exception"/>
        <tx:method name="add*" propagation="REQUIRED" read-only="false"/>
    </tx:attributess>
</tx:advice>
<!-- 事物切入點(diǎn) -->
<aop:config>
    <aop:pointcut expression="execution(* bat.ke.qq.com.service.*.*(..))" id="txPointCut"/>
    <aop:advisor advice-ref="advice" pointcut-ref="txPointCut"/>
</aop:config>

@Transactional

<!-- 開啟事務(wù)控制的注解支持 -->
<tx:annotation-driven transaction-manager="txManager"/>

事務(wù)主機(jī)配置,作用于類,方法上

屬性名 說(shuō)明
name 當(dāng)在配置文件中有多個(gè)TransactionManager,可以用該屬性指定選擇哪個(gè)事務(wù)管理器
propagation 事務(wù)的傳播行為,默認(rèn)值為REOUIRED
isolation 事務(wù)的隔離度,默認(rèn)值采用DEFAULT
timeout 事務(wù)的超時(shí)時(shí)間,默認(rèn)值為-1。如果超過該時(shí)間限制但事務(wù)還沒有完成,則自動(dòng)回滾事務(wù)
read-only 指定事務(wù)是否為只讀事務(wù),默認(rèn)值為false;為了忽略那些不需要事務(wù)的方法,比如讀取數(shù)
據(jù),可以設(shè)置 read-only 為 true。
rollback-for 用于指定能夠觸發(fā)事務(wù)回滾的異常類型,如果有多個(gè)異常類型需要指定,各類型之間可以通
過逗號(hào)分隔
no-rollback-for 拋出 no-rollback-for 指定的異常類型,不回滾事務(wù)

Java Configuration

@EnableTransactionManagement

利用 TransactionManagementConfigurationSelector 向容器中注冊(cè)兩個(gè)組件

  • AutoProxyRegistrar

    給容器中注冊(cè)一個(gè) InfrastructureAdvisorAutoProxyCreator 的后置處理器,返回一個(gè)代理對(duì)象 (增強(qiáng)器),代理對(duì)象執(zhí)行方法利用攔截器鏈進(jìn)行調(diào)用

  • ProxyTransactionManagementConfiguration 是一個(gè) @Configuration

    • 給容器中注冊(cè)事務(wù)增強(qiáng)器 transactionAcvisor
    • AnnotationTransactionAttributesource 解析事務(wù)注解
    • 事務(wù)欄截器 transactionInterceptor

Spring的事務(wù)傳播機(jī)制

    多個(gè)事務(wù)方法相互調(diào)用時(shí),事務(wù)如何在這些方法之間進(jìn)行傳播,Spring中提供了 `7種` 不同的傳播特性,來(lái)保證事務(wù)的正常執(zhí)行
  • REQUIRED:默認(rèn)的傳播特性,如果當(dāng)前沒有事務(wù),則新建一個(gè)事務(wù),如果當(dāng)前存在事務(wù),則加入這個(gè)事務(wù)
  • SUPPORTS:當(dāng)前存在事務(wù),則加入當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),則以非事務(wù)的方式執(zhí)行
  • MANDATORY:當(dāng)前存在事務(wù),則加入當(dāng)前事務(wù),如果當(dāng)前事務(wù)不存在,則拋出異常
  • REQUIRED_NEW :創(chuàng)建一個(gè)新事務(wù),如果存在當(dāng)前事務(wù),則掛起該事務(wù)
  • NOT_SUPPORTED:以非事務(wù)方式執(zhí)行,如果存在當(dāng)前事務(wù),則掛起當(dāng)前事務(wù)
  • NEVER:不使用事務(wù),如果當(dāng)前事務(wù)存在,則拋出異常
  • NESTED:如果當(dāng)前事務(wù)存在,則在嵌套事務(wù)中執(zhí)行,否則REQUIRED的操作一樣

NESTED 和 REQUIRED_NEW 的區(qū)別:

REQUIRED_NEW 是新建一個(gè)事務(wù)并且新開始的這個(gè)事務(wù)與原有事務(wù)無(wú)關(guān),而 NESTED 則是當(dāng)前存在事務(wù)時(shí)會(huì)開啟一個(gè)嵌套事務(wù),在 NESTED 情況下,父事務(wù)回滾時(shí),子事務(wù)也會(huì)回滾,而 REQUIRED_NEW 情況下,原有事務(wù)回滾,不會(huì)影響新開啟的事務(wù)

NESTED 和 REQUIRED 的區(qū)別:

REQUIRED 情況下,調(diào)用方存在事務(wù)時(shí),則被調(diào)用方和調(diào)用方使用同一個(gè)事務(wù),那么被調(diào)用方出現(xiàn)異常時(shí),由于共用一個(gè)事務(wù),所以無(wú)論是否 catch異常,事務(wù)都會(huì)回滾,而在 NESTED 情況下,被調(diào)用方發(fā)生異常時(shí),調(diào)用方可以 catch其異常,這樣只有子事務(wù)回滾,父事務(wù)不會(huì)回滾

Spring事務(wù)的實(shí)現(xiàn)原理

    在使用Spring框架的時(shí)候,可以有兩種事務(wù)的實(shí)現(xiàn)方式,一種是編程式事務(wù),有用內(nèi)自己通過代碼來(lái)?yè)沃剖聞?wù)的處理邏輯,還有一種是聲明式事務(wù),通過 `@Transactional` 注解來(lái)實(shí)現(xiàn)。

    其實(shí)事務(wù)的操作本來(lái)應(yīng)該是由數(shù)據(jù)庫(kù)來(lái)進(jìn)行控制,但是為了方便用戶進(jìn)行業(yè)務(wù)邏輯的操作,Spring對(duì)事務(wù)功能進(jìn)行了擴(kuò)展實(shí)現(xiàn),一般我們很少會(huì)用編程式事務(wù),更多的是通過添加 @Transactional 注解來(lái)進(jìn)行實(shí)現(xiàn),當(dāng)添加此注解之后事務(wù)的自動(dòng)功能就會(huì)關(guān)閉,有Spring框架來(lái)幫助進(jìn)行控制。

    其實(shí)事務(wù)操作是AOP的一個(gè)核心體現(xiàn),當(dāng)一個(gè)方法添加 @Transactional 注解之后,Spring會(huì)基于這個(gè)類生成個(gè)代理對(duì)象,會(huì)將這個(gè)代理對(duì)象作為bean,當(dāng)使用這個(gè)代理對(duì)象的方法的時(shí)候,如果有事務(wù)處理,那么會(huì)先把事務(wù)的自動(dòng)提交給關(guān)系,然后去執(zhí)行具體的業(yè)務(wù)邏輯,如果執(zhí)行邏輯沒有出現(xiàn)異常,那么代理邏輯就會(huì)直接提交如果出現(xiàn)任何異常情況,那么直接進(jìn)行回滾操作,當(dāng)然用戶可以控制對(duì)哪些異常進(jìn)行回滾操作。

Spring事務(wù)失效場(chǎng)景

  • bean對(duì)象沒有被Spring容器管理

  • 方法的訪間修飾符不是public

  • 切點(diǎn)是否配置正確

  • 自身調(diào)用問題

    因?yàn)閠his不是代理對(duì)象,可以配置 expose-proxy="true",就可以通過 AopContext.currentProx()獲取
    到當(dāng)前類的代理對(duì)象

    <!-- expose-proxy="true" 類內(nèi)部可以獲取到當(dāng)前類的代理對(duì)象 -->
    <aop:aspectj-autoproxy expose-proxy="true"/>
    
    @EnableAspectJAutoProxy(exposeProxy = true)
    

    也可以注入當(dāng)前bean

  • 數(shù)據(jù)源沒有配置事務(wù)管理器

  • 數(shù)據(jù)庫(kù)不支持事務(wù)

  • 導(dǎo)常被捕獲

  • 異常類型錯(cuò)誤或者配置錯(cuò)誤

    默認(rèn)只支持RuntimeException 和 Error,不支持檢查異常

    想要支持檢查一異常需配置rollbackFor

    @Transactional(rollbackFor = Exception.class)
    
?著作權(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)容