1. 程式人生 > 實用技巧 >Spring的事務抽象

Spring的事務抽象

Spring提供了一致的事務管理抽象,該抽象能實現為不同的事務API提供一致的程式設計模型。無視我們使用jdbc、hibernate、mybatis哪種方式來操作資料,無視事務是jta事務還是jdbc事務。

事務

事務(transaction),一般是指要做的或所做的事情。在計算機術語中是指訪問或者更新資料庫中各項資料項的一個程式執行單元(unit)。事務通常由高階資料庫操作語言或程式語言書寫的使用者程式的執行所引起,並用begin transactionend transaction語句來界定。

為什麼需要事務

事務是為了解決資料安全操作提出的解決方案,事務的控制實際上就是控制資料的安全訪問於隔離。舉一個簡單的例子:如果我們去銀行轉賬,A賬戶將自己的1000元轉賬給B,那麼業務實現的邏輯首先是將A的餘額減少1000,然後往B的餘額增加100,假如這個過程中出現意外,導致過程中斷,A已經扣款成功,B還沒來及增加,就會導致B損失了1000元,所以必須做出控制,要求A賬戶轉帳業務撤銷。這才能保證業務的正確性,完成這個操走就需要事務,將A賬戶資金減少和B賬戶資金增加放到同一個事務裡,要麼全部執行成功,要麼全部撤銷,這樣就保證了資料的安全性。

事務的四大特性

  1. 原子性:事務是資料庫的邏輯工作單位,而且必須是原子工作單位,對於其資料修改,要麼全部執行,要麼全部不執行。
  2. 一致性:事務在完成時,必須是所有的資料都保持一致狀態。在相關資料庫中,所有規則都必須應用於事務的修改,以保持所有資料的完整性。
  3. 隔離型:一個事務的執行不能被其他事務所影響。
  4. 永續性:一個事務一旦提交,事務的操作便永久性的儲存在db中。即便是在資料庫系統遇到故障的情況下也不會丟失。

Java事務型別

在Java中事務型別有三種:jdbc事務、jta事務以及容器事務

jdbc事務

在jdbc中處理事務,都是通過connection完成,在同一事務中所有的操作,都在使用同一個connection物件完成,jdbc預設是開啟事務的,並且預設完成提交操作。而在jdbc中有三種事務有關的操作:

  1. setAutoCommit:設定是否自動提交事務,如果為true則表示自動提交,每一個sql獨立存在一個事務,如果設定為false,則需要手動commit進行提交
  2. commit:手動提交事務
  3. rollback:手動回滾結束事務

使用jdbc事務的基本步驟如下:

@Test
public void testTX(){
    String url = "jdbc:mysql://127.0.0.1:3306/test";
    String username = "root";
    String password = "1234";

    String sourceUserId = "leo";
    String desUserId = "xnn";
    int money = 500;

    Connection connection = null;
    try {
        //1.載入資料庫驅動程式
        Class.forName("com.mysql.jdbc.Driver");
        //獲得資料庫連線
        connection = DriverManager.getConnection(url, username, password);
        //開啟事務
        connection.setAutoCommit(false);//如果為true的話,sql語句會分別執行,修改資料庫;如果為false的話,會啟用事務

        //多條資料操作資料
        Statement sql = connection.createStatement();
        sql.executeUpdate("UPDATE user_info SET balance = balance-" + money + " WHERE user_id = '" + sourceUserId+"'");
        sql.executeUpdate("UPDATE user_info SET balance = balance+" + money + " WHERE user_id = '" + desUserId+"'");
        //提交事務
        connection.commit();

    } catch (SQLException e) {
        e.printStackTrace();
        try{
            //回滾
            connection.rollback();
        }catch (SQLException ex){
        }

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

從程式碼中我們可以看出來jdbc事務的缺點:

  1. 冗長、複雜
  2. 需要顯示事務控制
  3. 需要顯示處理受檢查異常

並且jdbc僅僅是為了完成事務操作提供了基礎的API支援,通過操作jdbc我們可以將多個sql語句放到同一個事務中,保證acid特性,但是當遇到跨庫跨表的sql,簡單的jdbc事務就無法滿足了,基於這種問題,jta事務出現了。

jta事務

jta(Java transaction API)提供了跨資料庫連線的事務管理能力。jta事務管理則由jta容器實現。

jta的構成

在jta中有幾個重要的概念:

  1. 高層應用事務界定介面,供事務客戶界定事務邊界
  2. X/Open XA協議(資源之間的一種標準化的介面)的標準Java對映 ,它可以使事務性的資源管理器參與由外部事務管理器控制的事務中
  3. 高層事務管理器介面,允許應用程式為其管理的程式界定事務的邊界範圍

jta中的重要介面

jta的重要介面主要位於javax.transaction包中

  1. UserTransaction:讓應用程式得以控制事務的開始、掛起、提交與回滾等操作,由java或者ejb元件呼叫
  2. TransactionManager:用於應用服務管理事務的狀態
  3. Transaction:用於控制執行事務操作
  4. XAResource:用於在分散式事務環境下,協調事務管理器和資源管理器的工作
  5. XID:用來為事務標示的java對映id

需要注意的是前三個介面僅存在於javaee.jar中,在javase中並不存在。

jta事務程式設計的基本步驟

//配置JTA事務,建立對應的資料來源
//1.建立事務:通過建立UserTransaction類的例項來開始一個事務
 Context ctx = new InitialContext(p) ;
 UserTransaction trans = (UserTransaction) ctx.lookup("javax. Transaction.UserTransaction");
//開始事務
trans.begin();
//找到資料來源,進行繫結
DataSource ds = (DataSource) ctx.lookup("mysqldb");
//建立資料庫連線
Connection mycon = ds.getConnection();
//執行了sql操作
stmt.executeUpdate(sqlS);
//提交事務
trans.commit();
//關閉連線
mycon.close();

jta事務的優缺點

可以看出,jta的優點很明顯,提供了分散式下的事務解決方案,並且執行嚴格的acid操作,但是標準的jta事務在日常開發中並不常用,其原因就是jta的缺點導致的,例如jta的實現相當複雜,JTA UserTransaction需要從JNDI獲取,即我們如果要實現JTA一般情況下也需要實現JNDI,並且JTA只是個簡易的容器,使用複雜,在靈活的需求下很難實現程式碼複用,因為我們需要一個能給我們進行完成容器事務操作的框架

Spring的事務與事務抽象

Spring給我們封裝了一套事務機制,並且提供了完善的事務抽象,將事務所需要的步驟進行抽象劃分,並以程式設計的方式提供一個標準API,如下:

try{
 
//1.開啟事務
//2.執行資料庫操作
 
//3.提交事務
 
}catch(Exception ex){
 
//處理異常
//4.回滾事務
 
}finally{
 
//關閉連線,資源清理
}

Spring事務抽象的核心介面

Spring的抽象事務模型基於介面PlatformTransactionManager,該介面有不同的多種實現,每一種實現都有對應的一個特定的資料訪問技術,大體如下:

PlatformTransactionManager及其相關屬性

事務管理器介面PlatformTransactionManager通過getTransaction方法來得到事務,引數為TransactionDefinition類,這個類定義事務類的基本屬性:

  1. 傳播行為
  2. 隔離規則
  3. 回滾規則
  4. 事務超時設定
  5. 事務是否只讀
public interface TransactionDefinition {
    int getPropagationBehavior(); // 返回事務的傳播行為
    int getIsolationLevel(); // 返回事務的隔離級別,事務管理器根據它來控制另外一個事務可以看到本事務內的哪些資料
    int getTimeout();  // 返回事務必須在多少秒內完成
    boolean isReadOnly(); // 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是隻讀的
}

其中最重要的是事務的傳播行為以及隔離規則

事務的七種傳播行為如下所示:

傳播性 描述
PROPAGATION_REQUIRED 0 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中
PROPAGATION_SUPPORTS 1 事務可有可無。有就支援當前事務,沒有就以非事務方式執行
PROPAGATION_MANDATORY 2 支援當前事務,如果當前沒有事務,就丟擲異常
PROPAGATION_REQUIRES_NEW 3 無論是否有事務都得新建個事務,如果當前存在事務,把當前事務掛起
PROPAGATION_NOT_SUPPORTED 4 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起
PROPAGATION_NEVER 5 以非事務方式執行,如果當前存在事務,則丟擲異常
PROPAGATION_NESTED 6 如果一個活動的事務存在,則執行在一個巢狀的事務中. 如果沒有活動事務, 則按TransactionDefinition.PROPAGATION_REQUIRED 屬性執行

事務的隔離級別如下所示:

在看事務隔離級別前需要先了解下什麼是髒讀、不可重複讀、幻讀

  • 髒讀:髒讀就是指當一個事務正在訪問資料庫,並且對資料進行了修改,而這種修改還沒有提交到資料庫中,這時,另外一個事務獲取資料進行操作,結果將剛剛未提交的資料獲取到了
  • 不可重複讀:不可重複讀是指在一個事務內,多次讀同一資料,前後讀取的結果不一致。在事務A還沒結束時,另外一個事務B也訪問該同一資料。那麼,在事務A中的兩次讀取資料的過程中,由於事務B對當前資料進行修改操作,導致事務A兩次讀取的資料不一致,因此稱為是不可重複讀
  • 幻讀:幻讀是指當事務不是獨立執行時發生的一種現象,例如事務A對錶中的一個數據進行了修改,這種修改涉及到表中的全部資料行。同時事務B也修改了這個表中的資料,這種修改是向表中插入一行新資料。那麼就會發生操作事務A的使用者發現表中還存在沒有修改的資料行,就好像發生了幻覺一樣。

為了解決這些問題,事務的隔離級別就出現了,對應的效果如下:

隔離性 髒讀 不可重複讀 幻讀
DEFAULT -1 使用資料庫設定的隔離級別
READ_UNCOMMITTED 1 ✔️ ✔️ ✔️
READ_COMMITTED 2 ✖️ ✔️ ✔️
REPEATABLE_READ 3 ✖️ ✖️ ✔️
SERIALIZABLE 4 ✖️ ✖️ ✖️

Spring和資料庫都有事務隔離級別。Spring預設隔離級別是按照資料庫的隔離級別處理的。

與不同資料訪問技術對應的PlatformTransactionManager實現

TransactionManager類 資料庫訪問技術
DataSourceTransactionManager 在僅使用JDBC時適用
HibernateTransactionManager 在適用Hibernate而沒有適用JPA時適用。同時在實現時還可能使用JDBC
JtaTransactionManager 在使用全域性事務時適用

可以看到Spring並不是提供了完整的事務操作API,而是提供了多種事務管理器,將事務的職責託管給了Hibernate、JTA等持久化機制平臺框架來實現,而僅僅提供一個通用的事務管理器介面org``.``springframework``.``transaction.PlatformTransactionManager,並且給各大持久化事務平臺框架提供了對應的事務管理器,用來限制其通用行為,但是具體事務實現將由各大平臺自己去實現:

Public interface PlatformTransactionManager{  
       // 由TransactionDefinition得到TransactionStatus物件
       TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
       // 提交
       Void commit(TransactionStatus status) throws TransactionException;  
       // 回滾
       Void rollback(TransactionStatus status) throws TransactionException;  
}

在Spring中使用事務

Spring中使用事務,可以直接在業務中以程式設計的方式使用;也可以通過註解以宣告式形式使用。

程式設計式事務

Spring提供了TransactionTemplate工具類可以很方便的使用程式設計式事務。預設情況下TransactionTemplate使用的是DataSourceTransactionManager。

package com.lucky.spring;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Created by zhangdd on 2020/7/26
 */
@SpringBootApplication
@Slf4j
public class Application implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    TransactionTemplate transactionTemplate;

    @Override
    public void run(String... args) throws Exception {

        log.info("default transaction manage:{}", transactionTemplate.getTransactionManager().getClass().getSimpleName());

        log.info("count before transaction:{}", getCount());
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                jdbcTemplate.execute("insert into product(name,description) values ('spring boot in action','書籍 spring boot in action') ");
                log.info("count in transaction:{}", getCount());
                transactionStatus.setRollbackOnly();
            }
        });
        log.info("count after transaction:{}", getCount());

    }

    private long getCount() {
        return (long) jdbcTemplate.queryForList("select count(*) as cnt from product")
                .get(0).get("cnt");
    }
}

列印結果如下:

2020-07-26 18:33:38.839  INFO 41176 --- [           main] com.lucky.spring.Application             : default transaction manage:DataSourceTransactionManager
2020-07-26 18:33:38.842  INFO 41176 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-07-26 18:33:39.051  INFO 41176 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-07-26 18:33:39.071  INFO 41176 --- [           main] com.lucky.spring.Application             : count before transaction:7
2020-07-26 18:33:39.082  INFO 41176 --- [           main] com.lucky.spring.Application             : count in transaction:8
2020-07-26 18:33:39.087  INFO 41176 --- [           main] com.lucky.spring.Application             : count after transaction:7

從列印結果中可以看到:

  1. TransactionTemplate預設使用的是DataSourceTransactionManager
  2. TransactionTemplate通過execute方法完成了事務的操作

宣告式事務


如上圖所示Spring的宣告式事務是建立在AOP之上的。其本質是對方法前後進行攔截,然後在目標方法開始之前建立或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。宣告式事務最大的優點就是不需要通過程式設計的方式管理事務,這樣就不需要在業務邏輯程式碼中摻雜事務管理的程式碼,只需在配置檔案中做相關的事務規則宣告(或通過基於@Transactional註解的方式),便可以將事務規則應用到業務邏輯中

使用@Transactional註解

@Transactional 可以作用於介面、介面方法、類以及類方法上。當作用於類上時,該類的所有 public 方法將都具有該型別的事務屬性,同時,我們也可以在方法級別使用該註解來覆蓋類級別的定義。

雖然 @Transactional 註解可以作用於介面、介面方法、類以及類方法上,但是 Spring 建議不要在介面或者介面方法上使用該註解,因為這隻有在使用基於介面的代理時它才會生效。另外, @Transactional 註解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。如果你在 protected、private 或者預設可見性的方法上使用 @Transactional 註解,這將被忽略,也不會丟擲任何異常。

@Transactional的rollbackFor屬性可以設定一個 Throwable 的陣列,用來表明如果方法丟擲這些異常,則進行事務回滾。預設情況下如果不配置rollbackFor屬性,那麼事務只會在遇到RuntimeException的時候才會回滾。

package com.lucky.spring.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by zhangdd on 2020/7/26
 */
@Service
public class ProductServiceImpl implements ProductService {


    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    //通過設定@Transactional就開啟了事務
    @Transactional
    public void insertRecord() {
        jdbcTemplate.execute("insert into product(name,description) values ('spring boot in action 1','書籍 spring boot in action 1') ");
    }

    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public void insertThenRollback() throws RuntimeException {
        jdbcTemplate.execute("insert into product(name,description) values ('spring boot in action 2','書籍 spring boot in action 2') ");
        throw new RuntimeException();
    }

    /**
     * 呼叫帶有事務註解的方法
     * 這種形式 事務不會回滾,即資料會插入到資料庫裡
     *
     * @throws RuntimeException
     */
    @Override
    public void invokeInsertThenRollback() throws RuntimeException {
        insertThenRollback();
    }

    /**
     * 呼叫的普通方法發生了異常
     * <p>
     * 資料會回滾
     */
    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public void insertRecordWhenCrash() {
        jdbcTemplate.execute("insert into product(name,description) values ('spring boot in action 3','書籍 spring boot in action 3') ");
        crash();
    }

    private void crash() throws RuntimeException {
        throw new RuntimeException();
    }
}

  • @Transactional是以代理的形式完成的事務。invokeInsertThenRollback()方法沒有被@Transactional修飾,所以即使內部呼叫insertThenRollback()這個方法事務也沒有生效。