spring5 源碼深度解析—– @Transactional註解的聲明式事物介紹(100%理解事務)

  • 2019 年 10 月 12 日
  • 筆記

面的幾個章節已經分析了spring基於@AspectJ的源碼,那麼接下來我們分析一下Aop的另一個重要功能,事物管理。

事務的介紹

1.資料庫事物特性

  • 原子性
    多個資料庫操作是不可分割的,只有所有的操作都執行成功,事物才能被提交;只要有一個操作執行失敗,那麼所有的操作都要回滾,資料庫狀態必須回復到操作之前的狀態
  • 一致性
    事物操作成功後,資料庫的狀態和業務規則必須一致。例如:從A賬戶轉賬100元到B賬戶,無論資料庫操作成功失敗,A和B兩個賬戶的存款總額是不變的。
  • 隔離性
    當並發操作時,不同的資料庫事物之間不會相互干擾(當然這個事物隔離級別也是有關係的)
  • 持久性
    事物提交成功之後,事物中的所有數據都必須持久化到資料庫中。即使事物提交之後資料庫立刻崩潰,也需要保證數據能能夠被恢復。

2.事物隔離級別

當資料庫並發操作時,可能會引起臟讀、不可重複讀、幻讀、第一類丟失更新、第二類更新丟失等現象。

  • 臟讀
    事物A讀取事物B尚未提交的更改數據,並做了修改;此時如果事物B回滾,那麼事物A讀取到的數據是無效的,此時就發生了臟讀。
  • 不可重複讀
    一個事務執行相同的查詢兩次或兩次以上,每次都得到不同的數據。如:A事物下查詢賬戶餘額,此時恰巧B事物給賬戶里轉賬100元,A事物再次查詢賬戶餘額,那麼A事物的兩次查詢結果是不一致的。
  • 幻讀
    A事物讀取B事物提交的新增數據,此時A事物將出現幻讀現象。幻讀與不可重複讀容易混淆,如何區分呢?幻讀是讀取到了其他事物提交的新數據,不可重複讀是讀取到了已經提交事物的更改數據(修改或刪除)

對於以上問題,可以有多個解決方案,設置資料庫事物隔離級別就是其中的一種,資料庫事物隔離級別分為四個等級,通過一個表格描述其作用。

隔離級別 臟讀 不可重複讀 幻象讀
READ UNCOMMITTED 允許 允許 允許
READ COMMITTED 臟讀 允許 允許
REPEATABLE READ 不允許 不允許 允許
SERIALIZABLE 不允許 不允許 不允許

3.Spring事物支援核心介面

 

  • TransactionDefinition–>定義與spring兼容的事務屬性的介面
public interface TransactionDefinition {      // 如果當前沒有事物,則新建一個事物;如果已經存在一個事物,則加入到這個事物中。      int PROPAGATION_REQUIRED = 0;      // 支援當前事物,如果當前沒有事物,則以非事物方式執行。      int PROPAGATION_SUPPORTS = 1;      // 使用當前事物,如果當前沒有事物,則拋出異常。      int PROPAGATION_MANDATORY = 2;      // 新建事物,如果當前已經存在事物,則掛起當前事物。      int PROPAGATION_REQUIRES_NEW = 3;      // 以非事物方式執行,如果當前存在事物,則掛起當前事物。      int PROPAGATION_NOT_SUPPORTED = 4;      // 以非事物方式執行,如果當前存在事物,則拋出異常。      int PROPAGATION_NEVER = 5;      // 如果當前存在事物,則在嵌套事物內執行;如果當前沒有事物,則與PROPAGATION_REQUIRED傳播特性相同      int PROPAGATION_NESTED = 6;      // 使用後端資料庫默認的隔離級別。      int ISOLATION_DEFAULT = -1;      // READ_UNCOMMITTED 隔離級別      int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;      // READ_COMMITTED 隔離級別      int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;      // REPEATABLE_READ 隔離級別      int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;      // SERIALIZABLE 隔離級別      int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;      // 默認超時時間      int TIMEOUT_DEFAULT = -1;      // 獲取事物傳播特性      int getPropagationBehavior();      // 獲取事物隔離級別      int getIsolationLevel();      // 獲取事物超時時間      int getTimeout();      // 判斷事物是否可讀      boolean isReadOnly();      // 獲取事物名稱      @Nullable      String getName();  }

  1. Spring事物傳播特性表:
傳播特性名稱 說明
PROPAGATION_REQUIRED 如果當前沒有事物,則新建一個事物;如果已經存在一個事物,則加入到這個事物中
PROPAGATION_SUPPORTS 支援當前事物,如果當前沒有事物,則以非事物方式執行
PROPAGATION_MANDATORY 使用當前事物,如果當前沒有事物,則拋出異常
PROPAGATION_REQUIRES_NEW 新建事物,如果當前已經存在事物,則掛起當前事物
PROPAGATION_NOT_SUPPORTED 以非事物方式執行,如果當前存在事物,則掛起當前事物
PROPAGATION_NEVER 以非事物方式執行,如果當前存在事物,則拋出異常
PROPAGATION_NESTED

如果當前存在事物,則在嵌套事物內執行;

如果當前沒有事物,則與PROPAGATION_REQUIRED傳播特性相同

  1. Spring事物隔離級別表:
事務隔離級別  臟讀  不可重複讀  幻讀
 讀未提交(read-uncommitted)  是  是  是
 不可重複讀(read-committed)  否   是  是
 可重複讀(repeatable-read)  否   否   是
 串列化(serializable)  否   否   否 

mysql默認的事務隔離級別為 可重複讀repeatable-read

  3.PlatformTransactionManager–>Spring事務基礎結構中的中心介面

public interface PlatformTransactionManager {      // 根據指定的傳播行為,返回當前活動的事務或創建新事務。      TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;      // 就給定事務的狀態提交給定事務。      void commit(TransactionStatus status) throws TransactionException;      // 執行給定事務的回滾。      void rollback(TransactionStatus status) throws TransactionException;  }

Spring將事物管理委託給底層的持久化框架來完成,因此,Spring為不同的持久化框架提供了不同的PlatformTransactionManager介面實現。列舉幾個Spring自帶的事物管理器:

事物管理器 說明
org.springframework.jdbc.datasource.DataSourceTransactionManager 提供對單個javax.sql.DataSource事務管理,用於Spring JDBC抽象框架、iBATIS或MyBatis框架的事務管理
org.springframework.orm.jpa.JpaTransactionManager 提供對單個javax.persistence.EntityManagerFactory事務支援,用於集成JPA實現框架時的事務管理
org.springframework.transaction.jta.JtaTransactionManager 提供對分散式事務管理的支援,並將事務管理委託給Java EE應用伺服器事務管理器

 

  • TransactionStatus–>事物狀態描述
  1. TransactionStatus介面
public interface TransactionStatus extends SavepointManager, Flushable {      // 返回當前事務是否為新事務(否則將參與到現有事務中,或者可能一開始就不在實際事務中運行)      boolean isNewTransaction();      // 返回該事務是否在內部攜帶保存點,也就是說,已經創建為基於保存點的嵌套事務。      boolean hasSavepoint();      // 設置事務僅回滾。      void setRollbackOnly();      // 返回事務是否已標記為僅回滾      boolean isRollbackOnly();      // 將會話刷新到數據存儲區      @Override      void flush();      // 返回事物是否已經完成,無論提交或者回滾。      boolean isCompleted();  }

  1. SavepointManager介面
public interface SavepointManager {      // 創建一個新的保存點。      Object createSavepoint() throws TransactionException;      // 回滾到給定的保存點。      // 注意:調用此方法回滾到給定的保存點之後,不會自動釋放保存點,      // 可以通過調用releaseSavepoint方法釋放保存點。      void rollbackToSavepoint(Object savepoint) throws TransactionException;      // 顯式釋放給定的保存點。(大多數事務管理器將在事務完成時自動釋放保存點)      void releaseSavepoint(Object savepoint) throws TransactionException;  }

Spring編程式事物

CREATE TABLE `account` (    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',    `balance` int(11) DEFAULT NULL COMMENT '賬戶餘額',    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='--賬戶表'

  • 實現
 1 import org.apache.commons.dbcp.BasicDataSource;   2 import org.springframework.dao.DataAccessException;   3 import org.springframework.jdbc.core.JdbcTemplate;   4 import org.springframework.jdbc.datasource.DataSourceTransactionManager;   5 import org.springframework.transaction.TransactionDefinition;   6 import org.springframework.transaction.TransactionStatus;   7 import org.springframework.transaction.support.DefaultTransactionDefinition;   8   9 import javax.sql.DataSource;  10  11 /**  12  * Spring編程式事物  13  * @author: Chenhao  14  * @create: 2019-10-08 11:41  15  */  16 public class MyTransaction {  17  18     private JdbcTemplate jdbcTemplate;  19     private DataSourceTransactionManager txManager;  20     private DefaultTransactionDefinition txDefinition;  21     private String insert_sql = "insert into account (balance) values ('100')";  22  23     public void save() {  24  25         // 1、初始化jdbcTemplate  26         DataSource dataSource = getDataSource();  27         jdbcTemplate = new JdbcTemplate(dataSource);  28  29         // 2、創建物管理器  30         txManager = new DataSourceTransactionManager();  31         txManager.setDataSource(dataSource);  32  33         // 3、定義事物屬性  34         txDefinition = new DefaultTransactionDefinition();  35         txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);  36  37         // 3、開啟事物  38         TransactionStatus txStatus = txManager.getTransaction(txDefinition);  39  40         // 4、執行業務邏輯  41         try {  42             jdbcTemplate.execute(insert_sql);  43             //int i = 1/0;  44             jdbcTemplate.execute(insert_sql);  45             txManager.commit(txStatus);  46         } catch (DataAccessException e) {  47             txManager.rollback(txStatus);  48             e.printStackTrace();  49         }  50  51     }  52  53     public DataSource getDataSource() {  54         BasicDataSource dataSource = new BasicDataSource();  55         dataSource.setDriverClassName("com.mysql.jdbc.Driver");  56         dataSource.setUrl("jdbc:mysql://localhost:3306/my_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8");  57         dataSource.setUsername("root");  58         dataSource.setPassword("chenhao1991@");  59         return dataSource;  60     }  61  62 }

  • 測試類及結果
public class MyTest {      @Test      public void test1() {          MyTransaction myTransaction = new MyTransaction();          myTransaction.save();      }  }

運行測試類,在拋出異常之後手動回滾事物,所以資料庫表中不會增加記錄。

基於@Transactional註解的聲明式事物

其底層建立在 AOP 的基礎之上,對方法前後進行攔截,然後在目標方法開始之前創建或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。通過聲明式事物,無需在業務邏輯程式碼中摻雜事務管理的程式碼,只需在配置文件中做相關的事務規則聲明(或通過等價的基於標註的方式),便可以將事務規則應用到業務邏輯中。

  • 介面
import org.springframework.transaction.annotation.Propagation;  import org.springframework.transaction.annotation.Transactional;    /**   * 賬戶介面   * @author: ChenHao   * @create: 2019-10-08 18:38   */  @Transactional(propagation = Propagation.REQUIRED)  public interface AccountServiceImp {      void save() throws RuntimeException;  }

  • 實現
import org.springframework.jdbc.core.JdbcTemplate;    /**   * 賬戶介面實現   * @author: ChenHao   * @create: 2019-10-08 18:39   */  public class AccountServiceImpl implements AccountServiceImp {        private JdbcTemplate jdbcTemplate;        private static String insert_sql = "insert into account(balance) values (100)";          @Override      public void save() throws RuntimeException {          System.out.println("==開始執行sql");          jdbcTemplate.update(insert_sql);          System.out.println("==結束執行sql");            System.out.println("==準備拋出異常");          throw new RuntimeException("==手動拋出一個異常");      }        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {          this.jdbcTemplate = jdbcTemplate;      }  }

  • 配置文件
<?xml version="1.0" encoding="UTF-8"?>  <beans xmlns="http://www.springframework.org/schema/beans"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xmlns:tx="http://www.springframework.org/schema/tx"         xsi:schemaLocation="http://www.springframework.org/schema/beans          http://www.springframework.org/schema/beans/spring-beans.xsd          http://www.springframework.org/schema/tx          http://www.springframework.org/schema/tx/spring-tx.xsd">        <!--開啟tx註解-->      <tx:annotation-driven transaction-manager="transactionManager"/>        <!--事物管理器-->      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">          <property name="dataSource" ref="dataSource"/>      </bean>        <!--數據源-->      <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">          <property name="driverClassName" value="com.mysql.jdbc.Driver"/>          <property name="url" value="jdbc:mysql://localhost:3306/my_test?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>          <property name="username" value="root"/>          <property name="password" value="chenhao1991@"/>      </bean>        <!--jdbcTemplate-->      <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">          <property name="dataSource" ref="dataSource"/>      </bean>        <!--業務bean-->      <bean id="accountService" class="com.chenhao.aop.AccountServiceImpl">          <property name="jdbcTemplate" ref="jdbcTemplate"/>      </bean>    </beans>

  • 測試
import org.junit.Test;  import org.springframework.context.ApplicationContext;  import org.springframework.context.support.ClassPathXmlApplicationContext;    /**   * @author: ChenHao   * @create: 2019-10-08 18:45   */  public class MyTest {        @Test      public void test1() {          // 基於tx標籤的聲明式事物          ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");          AccountServiceImp studentService = ctx.getBean("accountService", AccountServiceImp.class);          studentService.save();      }  }

  • 測試
==開始執行sql  ==結束執行sql  ==準備拋出異常    java.lang.RuntimeException: ==手動拋出一個異常        at com.lyc.cn.v2.day09.AccountServiceImpl.save(AccountServiceImpl.java:24)      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)      at java.lang.reflect.Method.invoke(Method.java:498)

測試方法中手動拋出了一個異常,Spring會自動回滾事物,查看資料庫可以看到並沒有新增記錄。

注意:默認情況下Spring中的事務處理只對RuntimeException方法進行回滾,所以,如果此處將RuntimeException替換成普通的Exception不會產生回滾效果。

接下來我們就分析基於@Transactional註解的聲明式事物的的源碼實現。