1. 程式人生 > 其它 >Spring事務管理:ACID的概念,Spring事務管理核心介面,基於XML方式的宣告式事務、基於註解(Annotation)方式的宣告式事務

Spring事務管理:ACID的概念,Spring事務管理核心介面,基於XML方式的宣告式事務、基於註解(Annotation)方式的宣告式事務

一、事務的概念可以描述為具有以下四個關鍵屬性,也就是ACID

  • 原子性(Atomicity):事務應該當作一個單獨單元的操作,這意味著整個序列操作要麼是成功,要麼是失敗;

  • 一致性(Consistency):這表示資料庫的引用完整性的一致性,表中唯一的主鍵等;

  • 隔離性(Isolation):可能同時處理很多有相同的資料集的事務,每個事務應該與其他事務隔離,以防止資料損壞;

  • 永續性(Durability):一個事務一旦完成全部操作,這個事務的結果必須是永久性的,不能因系統故障而從資料庫中刪除。

二、Spring 事務管理的核心介面有三個

1. PlatformTransactionManager:平臺事務管理器,主要具有以下三個方法

  • TransactionStatus getTransaction(TransactionDefinition definition); 用於獲取事務狀態資訊
  • void commit(TransactionStatus status); 用於提交事務
  • void rollback(TransactionStatus status); 用於回滾事務

PlatformTransactionManager介面只是代表事務管理的介面,常見的幾個實現類如下:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager 用於配置JDBC資料來源的事務管理器
  • org.springframework.orm.hibernate4.HibernateTransactionManager 用於配置Hibernate的事務管理器
  • org.springframework.transaction.jta.JtaTransactionManager 用於配置全域性事務管理器

2. TransactionDefinition:事務定義

TransactionDefinition介面是事務定義(描述)的物件,該物件中定義了事務規則,並提供了獲取事務相關資訊的方法,具體如下表所示:

方法 說明
String getName( ); 獲取事務物件名稱
int getIsolationLevel( ); 獲取事務的隔離級別
int getPropagationBehavior( ); 獲取事務的傳播行為
int getTimeout( ); 獲取事務的超時時間
boolean isReadOnly( ); 獲取事務是否只讀

上述方法中,事務的傳播行為是指在同一個方法中,不同操作前後所使用的事務傳播行為有很多種,具體如下圖所示:

在事務管理過程中,傳播行為可以控制是否需要建立事務以及如何建立事務,通常情況下,資料的查詢不會影響原資料的改變,所以不需要進行事務管理,而對於資料的插入、更新和刪除操作,必須進行事務管理。如果沒有指定事務的傳播行為,Spring預設傳播行為是REQUIRED。

3. TransactionStatus:事務狀態

它描述某一時間點上事務在狀態資訊,具體如下圖所示:

三、Spring 支援兩種型別的事務管理

  • 程式設計式事務管理 :這意味著你在程式設計中管理事務,它給你極大的靈活性,但卻很難維護。本篇不對此種方式的實現展開討論。
  • 宣告式事務管理 :這意味著你可以從業務程式碼中分離事務管理,它可以使用XML方式或註解方式來管理事務。本篇僅對XML方式和註解方式實現事務展開討論。

四、基於XML方式的宣告式事務

1. 建立表(MySQL資料庫)

create table account(id int primary key auto_increment,username varchar(50),balance double);

2. 建立實體類

package com.itheima.jdbc;

public class Account {
    private Integer id; // 賬戶id
    private String username; // 使用者名稱
    private Double balance; // 賬戶餘額

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    public String toString() {
        return "Account [id=" + id + ", " + "username=" + username + ", balance=" + balance + "]";
    }
}

3. 建立介面

package com.itheima.jdbc;

import java.util.List;

public interface AccountDao {
    // 新增
    public int addAccount(Account account);
    // 更新
    public int updateAccount(Account account);
    // 刪除
    public int deleteAccount(int id);
    // 通過id查詢
    public Account findAccountById(int id);
    // 查詢所有賬戶
    public List<Account> findAllAccount();
    // 轉賬
    public void transfer(String outUser,String inUser,Double money);
}

4. 建立實現類

package com.itheima.jdbc;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public class AccountDaoImpl implements AccountDao {
    // 宣告JdbcTemplate屬性及其setter方法
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 新增賬戶
    public int addAccount(Account account) {
        // 定義SQL
        String sql = "insert into account(username,balance) value(?,?)";
        // 定義陣列來存放SQL語句中的引數
        Object[] obj = new Object[] { account.getUsername(), account.getBalance() };
        // 執行新增操作,返回的是受SQL語句影響的記錄條數
        int num = this.jdbcTemplate.update(sql, obj);
        return num;
    }

    // 更新賬戶
    public int updateAccount(Account account) {
        // 定義SQL
        String sql = "update account set username=?,balance=? where id = ?";
        // 定義陣列來存放SQL語句中的引數
        Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() };
        // 執行新增操作,返回的是受SQL語句影響的記錄條數
        int num = this.jdbcTemplate.update(sql, params);
        return num;
    }

    // 刪除賬戶
    public int deleteAccount(int id) {
        // 定義SQL
        String sql = "delete  from account where id = ? ";
        // 執行新增操作,返回的是受SQL語句影響的記錄條數
        int num = this.jdbcTemplate.update(sql, id);
        return num;
    }

    // 通過id查詢賬戶資料資訊
    public Account findAccountById(int id) {
        // 定義SQL語句
        String sql = "select * from account where id = ?";
        // 建立一個新的BeanPropertyRowMapper物件
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 將id繫結到SQL語句中,並通過RowMapper返回一個Object型別的單行記錄
        return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
    }

    // 查詢所有賬戶資訊
    public List<Account> findAllAccount() {
        // 定義SQL語句
        String sql = "select * from account";
        // 建立一個新的BeanPropertyRowMapper物件
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 執行靜態的SQL查詢,並通過RowMapper返回結果
        return this.jdbcTemplate.query(sql, rowMapper);
    }

    /**
     *  轉賬
     *  inUser:收款人
     *  outUser:匯款人
     *  money:收款金額
    */
    public void transfer(String outUser, String inUser, Double money) {
        // 收款時,收款使用者的餘額=現有餘額+所匯金額
        this.jdbcTemplate.update("update account set balance = balance +? "
                + "where username = ?",money, inUser);
        // 模擬系統執行時的突發性問題
        int i = 1/0;
        // 匯款時,匯款使用者的餘額=現有餘額-所匯金額
        this.jdbcTemplate.update("update account set balance = balance-? "
                + "where username = ?",money, outUser);
    }
}

5. 建立配置檔案(applicationContext.xml)

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
    http://www.springframework.org/schema/tx 
    http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <!-- 1.配置資料來源 -->
    <bean id="dataSource" 
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!--資料庫驅動 -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <!--連線資料庫的url -->
        <property name="url" value="jdbc:mysql://localhost:3306/xuejia" />
        <!--連線資料庫的使用者名稱 -->
        <property name="username" value="root" />
        <!--連線資料庫的密碼 -->
        <property name="password" value="admin" />
   </bean>
   <!-- 2.配置JDBC模板 -->
   <bean id="jdbcTemplate" 
          class="org.springframework.jdbc.core.JdbcTemplate">
         <!-- 預設必須使用資料來源 -->
         <property name="dataSource" ref="dataSource" />
   </bean>
   <!--3.定義id為accountDao的Bean -->
   <bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl">
         <!-- 將jdbcTemplate注入到AccountDao例項中 -->
         <property name="jdbcTemplate" ref="jdbcTemplate" />
   </bean>    
   <!-- 4.事務管理器,依賴於資料來源 -->
   <bean id="transactionManager" class=
   "org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
   </bean>    
   <!-- 5.編寫通知:對事務進行增強(通知),需要編寫對切入點和具體執行事務細節 -->
   <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- name:*表示任意方法名稱 -->
            <tx:method name="*" propagation="REQUIRED" 
                           isolation="DEFAULT" read-only="false" />
        </tx:attributes>
    </tx:advice>
    <!-- 6.編寫aop,讓spring自動對目標生成代理,需要使用AspectJ的表示式 -->
    <aop:config>
        <!-- 切入點 -->
        <aop:pointcut expression="execution(* com.itheima.jdbc.*.*(..))"
            id="txPointCut" />
        <!-- 切面:將切入點與通知整合 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
    </aop:config>
</beans>

6. 建立測試程式

package com.itheima.jdbc;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

//測試類
public class TransactionTest {
    @Test
    public void xmlTest() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 獲取AccountDao例項
        AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao");
        // 增加兩個使用者
        Account account1 = new Account();
        account1.setUsername("Jack");
        account1.setBalance(1000.00);
        int num1 = accountDao.addAccount(account1);
        
        Account account2 = new Account();
        account2.setUsername("Rose");
        account2.setBalance(500.00);
        int num2 = accountDao.addAccount(account2);

        // 呼叫例項中的轉賬方法
        accountDao.transfer("Jack", "Rose", 100.0);
        
        // 輸出提示資訊
        System.out.println("轉賬成功!");
    }
}

7. 執行

系統扔出異常:java.lang.ArithmeticException: / by zero

8. 修改程式,刪除實現類中的如下程式碼

        // 模擬系統執行時的突發性問題
        int i = 1/0;

9. 再次執行,結果如下

轉賬成功!

10. 查詢資料庫表中的資料

注意,表中出現兩個Jack,兩個Rose,並且他們的餘額都發生改變。因為addAccount執行了兩次,每次增加兩個人。

五、基於註解宣告式的事務

對上面的程式做如下修改:

1. 修改介面實現類

package com.itheima.jdbc;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public class AccountDaoImpl implements AccountDao {
    // 宣告JdbcTemplate屬性及其setter方法
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 新增賬戶
    public int addAccount(Account account) {
        // 定義SQL
        String sql = "insert into account(username,balance) value(?,?)";
        // 定義陣列來存放SQL語句中的引數
        Object[] obj = new Object[] { account.getUsername(), account.getBalance() };
        // 執行新增操作,返回的是受SQL語句影響的記錄條數
        int num = this.jdbcTemplate.update(sql, obj);
        return num;
    }

    // 更新賬戶
    public int updateAccount(Account account) {
        // 定義SQL
        String sql = "update account set username=?,balance=? where id = ?";
        // 定義陣列來存放SQL語句中的引數
        Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() };
        // 執行新增操作,返回的是受SQL語句影響的記錄條數
        int num = this.jdbcTemplate.update(sql, params);
        return num;
    }

    // 刪除賬戶
    public int deleteAccount(int id) {
        // 定義SQL
        String sql = "delete  from account where id = ? ";
        // 執行新增操作,返回的是受SQL語句影響的記錄條數
        int num = this.jdbcTemplate.update(sql, id);
        return num;
    }

    // 通過id查詢賬戶資料資訊
    public Account findAccountById(int id) {
        // 定義SQL語句
        String sql = "select * from account where id = ?";
        // 建立一個新的BeanPropertyRowMapper物件
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 將id繫結到SQL語句中,並通過RowMapper返回一個Object型別的單行記錄
        return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
    }

    // 查詢所有賬戶資訊
    public List<Account> findAllAccount() {
        // 定義SQL語句
        String sql = "select * from account";
        // 建立一個新的BeanPropertyRowMapper物件
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 執行靜態的SQL查詢,並通過RowMapper返回結果
        return this.jdbcTemplate.query(sql, rowMapper);
    }

    @Transactional(propagation = Propagation.REQUIRED, 
            isolation = Isolation.DEFAULT, readOnly = false)
    public void transfer(String outUser, String inUser, Double money) {
        // 收款時,收款使用者的餘額=現有餘額+所匯金額
        this.jdbcTemplate.update("update account set balance = balance +? "
                + "where username = ?",money, inUser);
        // 模擬系統執行時的突發性問題
        int i = 1/0;
        // 匯款時,匯款使用者的餘額=現有餘額-所匯金額
        this.jdbcTemplate.update("update account set balance = balance-? "
                + "where username = ?",money, outUser);
    }
}

2. 新建配置檔案(applicationContext-annotation.xml)

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
    http://www.springframework.org/schema/tx 
    http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <!-- 1.配置資料來源 -->
    <bean id="dataSource" 
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!--資料庫驅動 -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <!--連線資料庫的url -->
        <property name="url" value="jdbc:mysql://localhost:3306/xuejia" />
        <!--連線資料庫的使用者名稱 -->
        <property name="username" value="root" />
        <!--連線資料庫的密碼 -->
        <property name="password" value="admin" />
    </bean>
    <!-- 2.配置JDBC模板 -->
    <bean id="jdbcTemplate" 
            class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 預設必須使用資料來源 -->
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!--3.定義id為accountDao的Bean -->
    <bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl">
        <!-- 將jdbcTemplate注入到AccountDao例項中 -->
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>
    <!-- 4.事務管理器,依賴於資料來源 -->
    <bean id="transactionManager" class=
     "org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>    
    <!-- 5.註冊事務管理器驅動 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

3. 修改測試程式

package com.itheima.jdbc;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

//測試類
public class TransactionTest {
    @Test
    public void annotationTest() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext-annotation.xml");
        // 獲取AccountDao例項
        AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao");
        // 呼叫例項中的轉賬方法
        accountDao.transfer("Jack", "Rose", 100.0);
        // 輸出提示資訊
        System.out.println("轉賬成功!");
    }
}

4.執行

系統扔出異常:java.lang.ArithmeticException: / by zero

5. 修改程式,刪除實現類中的如下程式碼

        // 模擬系統執行時的突發性問題
        int i = 1/0;

6. 再次執行,結果如下

轉賬成功!

7. 查詢資料庫表中的資料

注意,表中出現兩個Jack,兩個Rose,並且他們的餘額再次發生改變。

本文參考:《Java EE企業級應用開發教程》