Spring的聲明式事務管理及事務的屬性講解

本篇博客利用例子來講解爲什麼使用事務以及事務的一些屬性和配置,耐心看完會有很大的幫助!!不對的地方也希望大家共同指出。

1. 事務概述

1. 在Java EE企業級開發的時候,爲了在每次操作完成的同時,也保證數據的完整性,就需要引入數據庫事物的概念。事務是什麼?簡單的來說就是一組由邏輯上緊密關聯而合併成一個整體的多個數據庫操作,通過事物的操作,我們可以保證數據的在每次訪問或修改時都能保持一個相對準確的結果。事物的操作要麼全部執行成功,要麼全部執行失敗

2. 事務的經典四大特性:

(1)原子性:事務的原子性要求事務中的所有操作要麼都執行成功,要麼都不成功。

(2)一致性:事務中不管涉及到多少個操作,都必須保證事務執行之前數據是正確的,事務執行之後數據仍然是正確的。

(3)隔離性:事務的隔離性要求多個事務在併發執行過程中不會互相干擾。

(4)持久性:事務執行完成後,對數據的修改永久的保存下來,不會因各種系統錯誤或其他意外情況而受到影響。

2. 聲明式事務管理 

1. Spring提供了優秀的聲明式事務管理,相比於之前的編程式事務管理(將事務代碼寫在業務模塊中,代碼冗餘),而Spring聲明式事務管理全程不用寫任何java代碼,只需要簡單的配置即可完成事務聲明。Spring的聲明式事務管理將管理代碼從業務方法中分類出來,以聲明的方式來實現事務管理(底層其實就是使用AOP思想)。Spring在不同的事務管理API之上定義了一個抽象層,通過配置的方式使其生效,從而讓應用程序開發人員不必瞭解事務管理API的底層實現細節,就可以使用Spring的事務管理機制。

2. Spring提供的事務管理器是從不同的事務管理API中抽象出了一整套事務管理機制,讓事務管理代碼從特定的事務技術中獨立出來。開發人員通過配置的方式進行事務管理,而不必瞭解其底層是如何實現的。Spring的核心事務管理抽象是PlatformTransactionManager,它底層有三個實現方法提供我們使用:DataSourceTransactionManager、JtaTransactionManager、HibernateTransactionManager。我們一般操作JDBC就使用DataSourceTransactionManager即可。

3. 數據測試聲明式事務管理 

3.1 想法:我們編寫一個需求來演示事務的功能以及事物的一些概念。首先我們模擬用戶購買書籍的過程,創建三張表,分別爲book表(書號,書名)、book_stock表(書號,庫存)、account表(用戶名,餘額)。我們通過代碼模擬購書流程,模擬用戶餘額不足或者書籍庫存不足這些情況,通過每一次的操作觀察數據庫每個表裏的數據更改與否,來引入事務管理的功能。

3.2 首先我們建表(mysql): 

CREATE TABLE book (
  isbn VARCHAR (50) PRIMARY KEY,
  book_name VARCHAR (100),
  price INT
) ;

CREATE TABLE book_stock (
  isbn VARCHAR (50) PRIMARY KEY,
  stock INT,
) ;

CREATE TABLE account (
  username VARCHAR (50) PRIMARY KEY,
  balance INT,
) ;

INSERT INTO `test`.`book` (`isbn`,`book_name`, `price`) VALUES ("001",'白夜行', 60);
INSERT INTO `test`.`book` (`isbn`,`book_name`, `price`) VALUES ("002",'圍城', 30);
INSERT INTO `test`.`book_stock` (`isbn`,`stock`) VALUES ("001",10);
INSERT INTO `test`.`book_stock` (`isbn`,`stock`) VALUES ("002",10);
INSERT INTO `test`.`account` (`username`,`balance`) VALUES ("wei",100);

3.3 配置xml配置文件:sping-tx.xml,配置之前我們需要先寫一個外部數據源文件db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456

導入依賴:

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>compile</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>

        <!-- mysql驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.jpattern/jporm-jdbctemplate -->
        <dependency>
            <groupId>com.jpattern</groupId>
            <artifactId>jporm-jdbctemplate</artifactId>
            <version>4.4.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alexkasko.springjdbc/springjdbc-iterable -->
        <dependency>
            <groupId>com.alexkasko.springjdbc</groupId>
            <artifactId>springjdbc-iterable</artifactId>
            <version>1.0.3</version>
        </dependency>
    </dependencies>

然後就是我們的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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--    組件掃描-->
    <context:component-scan base-package="com.wei.spring.tx.annotation"></context:component-scan>

<!--    數據源-->
    <context:property-placeholder location="classpath:db.properties"/>
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"></property>
        <property name="jdbcUrl" value="${jdbc.url}"></property>
        <property name="user" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>


    <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
        <constructor-arg ref="dataSource"></constructor-arg>
    </bean>

<!--    事務管理器-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>


<!--    開啓事務註解
            transaction-manager:用來指定事務管理器,如果事務管理器的id是transactionManager,可以省略不指定
    -->

    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>

3.4 編寫BookShopDao接口,來聲明我們所用的方法:顧客如果買書成功,我們首先需要將書的信息價格查出來,然後對應將書的庫存 -1,對應用戶的餘額 - 書的價格,這些信息都需要隨着買書成功得到相應的更新,所以我聲明這三個方法。

public interface BookShopDao {
    //根據書號查詢書的價格
    public int findPriceByIsbn(String isbn);

    //更新書的庫存
    public void updateStock(String isbn);

    //更新用戶的餘額
    public void updateUserAccount(String username,Integer price);
}

3.5 編寫BookShopDao的實現類BookShopDaoImpl,就是要把我們三個方法進行實現,把我們的方法寫完整。查詢書的價格、更新書的庫存,更新用戶賬戶餘額。但是要注意書的庫存以及用戶賬戶餘額都有不足的時候,所以我們自定義了倆個異常:庫存不足異常(BookStockException)以及餘額不足(UserAccountException)異常,並且輸出不足提示。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class BookShopDaoImpl implements BookShopDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    //根據書號查詢書的價格
    public int findPriceByIsbn(String isbn) {
        String sql = "select price from book where isbn = ?";
        return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
    }
    //更新書的庫存
    public void updateStock(String isbn) {
    //判斷庫存是否足夠
        String sql = "select stock from book_stock where isbn = ?";
        Integer stock = jdbcTemplate.queryForObject(sql, Integer.class, isbn);

        if (stock <= 0 ){
            throw new BookStockException("庫存不足!!");
        }
        sql = "update book_stock set stock = stock - 1 where isbn = ?";
        jdbcTemplate.update(sql,isbn);


    }
    //更新用戶的餘額
    public void updateUserAccount(String username, Integer price) {
        String sql ="select balance from account where username = ? ";
        Integer balance = jdbcTemplate.queryForObject(sql, Integer.class,username);

        if (balance < price){
            throw  new UserAccountException("餘額不足!!!");
        }

        sql = "update account set balance = balance - ? where username = ? ";
//        sql = "update account set balance = balance - ?  where username = ? ";
        jdbcTemplate.update(sql,price,username);
    }
}

倆個異常類(實現裏面的構造器)

public class BookStockException extends RuntimeException {
    public BookStockException() {
    }

    public BookStockException(String message) {
        super(message);
    }

    public BookStockException(String message, Throwable cause) {
        super(message, cause);
    }

    public BookStockException(Throwable cause) {
        super(cause);
    }

    public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
public class UserAccountException extends RuntimeException {
    public UserAccountException() {
    }

    public UserAccountException(String message) {
        super(message);
    }

    public UserAccountException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserAccountException(Throwable cause) {
        super(cause);
    }

    public UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

3.6 編寫BookStockService,來寫一個buyBook的方法,來調用我們的dao層的方法完成買書的過程。


public interface BookStockService {
    public void byBook(String username,String isbn);
}

3.7 編寫BookStockService的實現類BookStockServiceImpl。當我們買一本書時,首先要查詢到書的價格,然後更新更新書的庫存,更新用戶的餘額,都滿足就可以完成買書操作。

@Service
public class BookStockServiceImpl implements BookStockService {

    @Autowired
    private BookShopDao bookShopDao;

    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }
}

3.8 然後就是編寫測試類進行買書。

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

import java.util.ArrayList;

public class testTx {
    private BookShopDao bookShopDao;
    private BookStockService bookStockService;

    @Before
    public void init(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-tx.xml");
        bookShopDao = ctx.getBean("bookShopDaoImpl", BookShopDao.class);
        bookStockService = ctx.getBean("bookStockServiceImpl", BookStockService.class);
    }


    @Test
    public void testtx1(){
        bookStockService.byBook("wei","001");
    }


}

我們運行可以發現。001這本書的庫存少了一本,用戶的賬戶餘額也對應的少了第一本書的價格,剩餘40元,那就說明我再一次購買第一本書的時候,會報錯,因爲我的餘額已經不足,我們再買一本,來觀察。運行發現,首先程序會報錯:com.wei.spring.tx.annotation.UserAccountException: 餘額不足!!!,然後我們查看數據庫數據:發現第一本書的庫存變成了8,就是又減少了一本,但是我餘額卻沒有減少,因爲已近不夠買一本書的價錢了,但是書卻減少了,這個在我們的生活中是肯定不能出現的,這就是程序得確定,所以我們需要引入事務的概念,然我們運行程序時來保證我們數據的完整性。

3.8 爲方法添加事務:我們只需要在buyBook方法上加@Transactional註解,就爲我們的方法加上了事務。

@Service
public class BookStockServiceImpl implements BookStockService {

    @Autowired
    private BookShopDao bookShopDao;

    @Transactional
    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }
}

我們將數據庫裏的庫存和餘額都還原,我們再進行測試。第一次當然也是購買成功,第二次首先還是會報錯:餘額不足,但是數據庫發現庫存這次沒減少,說明事物起到了作用。事務的配置保證了我們每次訪問數據庫的一致性和準確性。事務就這麼簡單嗎?當然不是,事務中還可以配置很多配置,接下來我們在通過一個例子來講解。

3.9 我們編寫一個方法來模擬同時購買多本書,我們已經配置過得事務屬性還會出現什麼樣的情況?

public interface Cashier {
    //結賬,模擬買多本書
    public void checkOut(String username, List<String> isbns);
}

3.10 編寫Cashier的實現類CashierImpl

@Service
public class CashierImpl implements Cashier{

    @Autowired
    BookStockService bookStockService;
    
    public void checkOut(String username, List<String> isbns) {
        for (String isbn : isbns) {
            bookStockService.byBook(username,isbn);
        }
    }
}

3.11 我們接着測試我們checkOut模擬同時購買多本書。在我們的測試類中加入一個junit測試模塊,運行前我們將數據庫賬戶餘額改爲80,也就是說我們要測試同時購買倆本書,但是買一本錢夠,買兩本不夠的情況。

    @Test
    public void testCashier(){
        ArrayList<String> isbns = new ArrayList<>();
        isbns.add("001");
        isbns.add("002");
        cashier.checkOut("wei",isbns);

    }

測試我們發現,第一本書庫存減少了一本,餘額也減少了對應的錢數,但是我們第二本書沒有購買成功,控制檯報錯餘額不足,但這樣的情況類似於我們tb添加購物車的商品一起付款時,你的錢不夠,卻拿你的錢付了能買成功的商品,買不成功的就顯示餘額不足,這樣的情況我們也不允許發生,這樣是不對的。爲了將這倆個操作同時成功或者同時失敗,我們就需要將同時購買的方法也加上事務@Transactional註解

@Service
public class CashierImpl implements Cashier{

    @Autowired
    BookStockService bookStockService;
    @Transactional
    public void checkOut(String username, List<String> isbns) {
        for (String isbn : isbns) {
            bookStockService.byBook(username,isbn);
        }
    }
}

這樣我們把數據還原測試發現,這樣同時購買兩本書因爲餘額不夠,都會夠買不成功。

3.11 如果我在加入@Transactional註解時,想要完成 “如果餘額夠一本書的價格就買一本,剩下的購買不成功”,我們就需要引入事物的傳播行爲的概念。

事務的傳播行爲(Propagation):事務與事務之間是有傳播性的,一個事物被另一個事務方法調用時,當前的事務如何使用事務,這個需要在@Transactional註解中聲明Propagation:也就是事務的傳播行爲。

事務的傳播行爲有倆種,分別爲Propagation.REQUIRED和Propagation.REQUIRES_NEW。Propagation.REQUIRES是默認值,也就是我們不配置就是默認值。這倆個配置都是什麼意思?

Propagation.REQUIRED:默認值,使用調用者的事務。那上面的例子說明,我們如果不設置也就是使用這個默認傳播行爲,購買倆本書的時候都不會成功,也就是使用chekOut這個方法的事務,前面購買一本的時候成功了因爲餘額充足,但是在購買第二本的時候餘額不夠,就會購買失敗,也就是chekOut這個事務失敗,而事務都具有回滾特性,所以出現失敗直接回滾,導致第一本購買的也被回滾,最後結果出現倆本都購買不成功的現象。
Propagation.REQUIRES_NEW:將調用者的事務掛起,重新開啓事務來使用。還是那前面同時購買倆本的例子。配置了這個屬性新的方法執行時會開啓一個新的事務,也就是checkOut方法和之前購買一本的方法分別啓動倆個事務管理器,雖然第二本購買的時候出現了購買失敗,但是他只會回滾自己的事務,導致第二本購買失敗,第一本的事務被掛起,不受影響,所以會出現一本成功一本失敗。

配置代碼如下:  @Transactional(propagation = Propagation.REQUIRES_NEW)

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }

 3.12 事務的隔離級別(isolation):如果兩個事務Transaction01和Transaction02併發執行,爲了使它們不會相互影響,避免各種併發問題。一個事務與其他事務隔離的程度稱爲隔離級別。

事務有四種隔離級別:

1.讀未提交(READ UNCOMMITTED):一個事務進行修改操作,修改的結果還沒提交卻別第二個事務的查詢操作查詢到,這樣會出現的問題是:髒讀 (解決辦法:讀已提交)。


2.讀已提交(READ COMMITTED): 一個事物在進行修改操作,當數據還未提價,事務二進行讀取操作,讀完之後事務一進行提交,事務二再讀的時候讀回來的就是新的值,倆次讀取值不一致,這樣存在問題是:不可重複讀:主要描述修改操作(解決辦法:可重複讀)。


4.可重複讀(REPEATABLE READ):一個事務在對數據進行修改操作,還沒提交時,事務二進行讀取,事務二沒辦法讀取到任何數據,當第二次再去讀取時發現事務一已經將數據更改並且提交上去,事務二倆次讀取的數據不一致,這樣就存在問題:幻讀:主要描述插入操作(解決辦法:串行化 )mysql默認隔離級別。


8.串行化(SERIALIZABLE) :當一個事務在操作時其他事務必須等待,排隊一個一個進行操作,這樣存在的問題是:效率低。

具體怎麼配置事務的隔離級別如下:

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }

3.13 事務的回滾與不回滾:默認情況下,Spring捕獲到RuntimeException或Error時回滾,而捕獲到編譯時異常不回滾。

事務的回滾異常有四種設置屬性:可以設置捕獲什麼異常時進行回滾或者不回滾

rollbackFor:指定遇到時必須進行回滾的異常類型,可以爲多個
rollbackForClassName:通過一個字符串來指定異常進行回滾
noRollbackFor:指定遇到時不回滾的異常類型,可以爲多個
noRollbackClassName:通過一個字符串來指定異常進行不回滾

具體配置代碼如下:

    @Transactional(noRollbackFor = {UserAccountException.class})
    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }

3.14 事務的只讀設置

readOnly:
     true:只讀。代表着只會對數據庫進行讀取操作,不會有修改的操作。如果確保當前的事務只有讀取操作,就有必要設置只讀,可以幫助數據庫引擎優化事務。
     false:非只讀。代表不僅會有讀取數據操作還會有修改操作,就會有加鎖操作

具體設置代碼如下:

    @Transactional(readOnly = false)
    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }

 3.15 事務的超時設置:

超時事務屬性:事務在強制回滾之前可以保持多久。這樣可以防止長期運行的事務佔用資源。值得一提得是:事務的超時設置需要進行嚴格的計算,不可以隨便填寫。

具體設置代碼如下:

    @Transactional(timeout = 3)
    public void byBook(String username, String isbn) {
        Integer price = bookShopDao.findPriceByIsbn(isbn);

        bookShopDao.updateStock(isbn);

        bookShopDao.updateUserAccount(username,price);
    }

 注意:這是通過註解的方法來配置事務,配置事務還可以通過xml文件來配置,但是使用註解的方式比價好理解也比較好用,推薦使用註解方式來配置事務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章