Spring4 事務管理


Spring4 事務管理

本章是Spring4 教程中的最后一章,也是非常重要的一章。如果說學習IOC是知識的入門,那學習事務管理就是知識的提升。本章篇幅可能有一丟丟長,也有一丟丟難,需要讀者細細品味。主要從三個方面開始:事務簡介,基於注解的事務管理 和基於xml的事務管理。


准備環境

mysql文件,兩張表:一個用戶表,字段有帳號和余額。一個商品表,字段有sku,售價和庫存。

DROP TABLE IF EXISTS `user`;  
CREATE TABLE `user` (  
  `id` bigint(20) NOT NULL,  
  `account` varchar(255) NOT NULL,  
  `balance` float DEFAULT NULL COMMENT '用戶余額',  
  PRIMARY KEY (`id`)  
) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  
-- ----------------------------  
-- Records of user  
-- ----------------------------  
INSERT INTO user VALUES ('1', 'itdragon', '100');  
DROP TABLE IF EXISTS `product`;  
CREATE TABLE `product` (  
  `id` bigint(20) NOT NULL,  
  `sku` varchar(255) NOT NULL COMMENT '商品的唯一標識',  
  `price` float NOT NULL COMMENT '商品價格',  
  `stock` int(11) NOT NULL COMMENT '商品庫存',  
  PRIMARY KEY (`id`)  
) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  
-- ----------------------------  
-- Records of product  
-- ----------------------------  
INSERT INTO product VALUES ('1', 'java', '40', '10');  
INSERT INTO product VALUES ('2', 'spring', '50', '10');  

事務簡介

工作中應該經常聽到:"這是一個事務,你要保證它數據的一致性,在這里加個注解吧!"。於是我們就稀里糊塗地用,好像也沒出什么問題。
因為加上注解,說明該方法支持事務的處理。事務就是一系列的動作,這一系列的動作要么都成功,要么都失敗。所以你才會覺得沒出什么問題。管理事務是應用程序開發必不可少的技術,用來確保數據的完整性和一致性,特別是和錢有關系的事務。
事務有四個關鍵屬性:
** 原子性 **:一系列的動作,要么都成功,要么都失敗。
** 一致性 **:數據和事務狀態要保持一致。
** 隔離性 **:為了防止數據被破壞,每個事務之間都存在隔離性。
** 持久性 **:一旦事務完成, 無論發生什么系統錯誤, 它的結果都不應該受到影響。

我們用例子更好地說明事務:
A給B轉賬,A出賬500元,B因為某種原因沒有成功進賬。若A出賬的500元不回滾到A賬戶余額中,就會出現數據的不完整性和不一致性的問題。
本章模擬用戶購買商品的場景。商場的下單邏輯是:先發貨后設置用戶余額。如果用戶余額充足,商品庫存充足的情況,是沒有什么問題的。但若余額不足卻購買商品,庫存減少了,扣除用戶余額時會因為余額不足而拋出異常,到最后用戶余額並沒有減少,商品庫存卻減少了,顯然是不合理的。現在我們用Spring的事務管理來解決這種問題。


基於注解的事務管理

核心文件 applicationContext.xml。既然用到注解,就需要配置自動掃描包context:component-scan,還需要配置JdbcTempalte。最后要配置事務管理器和啟動事務注解 tx:annotation-driven
JDBC配置的事務管理器的class指定路徑是DataSourceTransactionManager,
Hibernate配置的事務管理器的class指定路徑是HibernateTransactionMannger 。兩個的用法都是一樣,只是配置事務管理時class指定的路徑不同罷了。這是因為 Spring 在不同的事務管理上定義了一個抽象層。我們無需了解底層的API,就可以使用Spring的事務管理。

<?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/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd  
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">  
      
    <context:component-scan base-package="com.itdragon.spring"></context:component-scan>  
      
    <!-- 導入資源文件 -->  
    <context:property-placeholder location="classpath:db.properties"/>  
      
    <!-- 配置 C3P0 數據源 -->  
    <bean id="dataSource"  
        class="com.mchange.v2.c3p0.ComboPooledDataSource">  
        <property name="user" value="${jdbc.user}"></property>  
        <property name="password" value="${jdbc.password}"></property>  
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>  
        <property name="driverClass" value="${jdbc.driverClass}"></property>  
  
        <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>  
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>  
    </bean>  
      
    <!-- 配置 Spirng 的 JdbcTemplate -->  
    <bean id="jdbcTemplate"   
        class="org.springframework.jdbc.core.JdbcTemplate">  
        <property name="dataSource" ref="dataSource"></property>  
    </bean>  
      
    <!-- 配置 NamedParameterJdbcTemplate, 該對象可以使用具名參數, 其沒有無參數的構造器, 所以必須為其構造器指定參數 -->  
    <bean id="namedParameterJdbcTemplate"  
        class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">  
        <constructor-arg ref="dataSource"></constructor-arg>      
    </bean>  
      
    <!-- 配置事務管理器 -->  
    <bean id="transactionManager"   
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource"></property>  
    </bean>  
      
    <!-- 啟用事務注解  如果配置的事務管理器的id就是transactionManager , 這里是可以省略transaction-manager -->  
    <tx:annotation-driven transaction-manager="transactionManager"/>  
      
</beans>  

接下來是事務的業務代碼,所有類都放在了一個目錄下,沒別的原因,就是因為懶。

核心是消費事務類 PurchaseService。介紹事務注解@Transactional的語法。
其次是批量消費事務類BatchPurchaseService。用於配合PurchaseService測試事務的傳播性。
然后是事務測試類TransactionTest。主要負責測試和詳細解釋事務語法。
最后是自定義異常類。是為了測試事務的回滾屬性。
PurchaseService(重點,注解語法),測試時,將注解逐一放開。

import java.util.List;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
import org.springframework.transaction.annotation.Isolation;  
import org.springframework.transaction.annotation.Propagation;  
import org.springframework.transaction.annotation.Transactional;  
  
@Service  
public class PurchaseService {  
      
    @Autowired  
    private ShopDao shopDao;  
      
    /** 
     * 模擬用戶購買商品,測事務回滾 
     * 最基本用法,直接在方法或者類上使用注解@Transactional。值得注意的是:只能在公共方法上使用 
     * 對應的測試方法是 basicTransaction() 
     */  
    @Transactional  
    /** 
     * 事務的傳播 propagation=Propagation.REQUIRED 
     * 常用的有兩種 REQUIRED,REQUIRES_NEW 
     * 對應的測試方法是  propagationTransaction() 
     */  
//  @Transactional(propagation=Propagation.REQUIRED)  
    /** 
     * 事務的隔離性 
     * 將事務隔離起來,減少在高並發的場景下發生 臟讀,幻讀和不可重復讀的問題 
     * 默認值是READ_COMMITTED 只能避免臟讀的情況。 
     * 不好演示,沒有對應的測試方法。 
     */  
//  @Transactional(isolation=Isolation.READ_COMMITTED)  
    /** 
     * 回滾事務屬性 
     * 默認情況下聲明式事務對所有的運行時異常進行回滾,也可以指定某些異常回滾和某些異常不回滾。(意義不大) 
     * noRollbackFor 指定異常不回滾 
     * rollbackFor 指定異常回滾 
     */  
//  @Transactional(noRollbackFor={UserException.class, ProductException.class})  
    /** 
     * 超時和只讀屬性 
     * 超時:在指定時間內沒有完成事務則回滾。可以減少資源占用。參數單位是秒 
     * 如果超時,則提示錯誤信息: 
     * org.springframework.transaction.TransactionTimedOutException: Transaction timed out 
     * 只讀屬性:指定事務是否為只讀. 若事務只讀數據則有利於數據庫引擎優化事務。  
     * 因為該事務有修改數據的操作,若設置只讀true,則提示錯誤信息 
     * nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed 
     * 對應的測試方法是 basicTransaction() 
     */  
//  @Transactional(timeout=5, readOnly=false)  
    public void purchase(String account, String sku) {  
        //1. 獲取書的單價  
        float price = shopDao.getBookPriceBySku(sku);  
          
        //2. 更新數的庫存  
        shopDao.updateBookStock(sku);  
          
        //3. 更新用戶余額  
        shopDao.updateUserBalance(account, price);  
        // 測試超時用的  
        /*try { 
            Thread.sleep(6000); 
        } catch (InterruptedException e) { 
        }*/  
    }  
      
}  

批量消費事務類BatchPurchaseService

import java.util.List;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
import org.springframework.transaction.annotation.Transactional;  
  
@Service  
public class BatchPurchaseService {  
      
    @Autowired  
    private PurchaseService purchaseService;  
      
    // 批量采購書籍,事務里面有事務  
    @Transactional  
    public void batchPurchase(String username, List<String> skus) {  
        for (String sku : skus) {  
            purchaseService.purchase(username, sku);  
        }  
    }  
  
}  

事務測試類TransactionTest(重點,知識點說明

import java.util.Arrays;  
import org.junit.Test;  
import org.springframework.context.ApplicationContext;  
import org.springframework.context.support.ClassPathXmlApplicationContext;  
  
public class TransactionTest {  
  
    private ApplicationContext ctx = null;  
    private PurchaseService purchaseService = null;  
    private BatchPurchaseService batchPurchaseService = null;  
      
    {  
        ctx = new ClassPathXmlApplicationContext("applicationContext.xml");  
        purchaseService = (PurchaseService) ctx.getBean("purchaseService");  
        batchPurchaseService = (BatchPurchaseService) ctx.getBean("batchPurchaseService");  
    }  
      
    /** 
     * 用戶買一本書 
     * 基本用法-事務回滾 
     * 把@Transactional 注釋。假設當前用戶余額只有10元。單元測試后,用戶余額沒有變,spring的庫存卻減少了。賺了!!! 
     * 把@Transactional 注釋打開。假設當前用戶余額只有10元。單元測試后,用戶余額沒有變,spring的庫存也沒有減少。這就是回滾。 
     * 回滾:按照業務邏輯,先更新庫存,再更新余額。現在是庫存更新成功了,但在余額邏輯拋出異常。最后數據庫的值都沒有變。也就是庫存回滾了。 
     */  
    @Test  
    public void basicTransaction() {  
        System.out.println("^^^^^^^^^^^^^^^^^@Transactional 最基本的使用方法");  
        purchaseService.purchase("itdragon", "spring");  
    }  
      
    /** 
     * 用戶買多本書 
     * 事務的傳播性 -大事務中,有小事務,小事務的表現形式 
     * 用@Transactional, 當前用戶余額50,是可以買一本書的。運行結束后,數據庫中用戶余額並沒有減少,兩本書的庫存也都沒有減少。 
     * 用@Transactional(propagation=Propagation.REQUIRED), 運行結果是一樣的。 
     * 把REQUIRED 換成 REQUIRES_NEW 再運行 結果還是一樣。。。。。 
     * 為什么呢???? 因為我弄錯了!!!!! 
     * 既然是事務的傳播性,那當然是一個事務傳播給另一個事務。 
     * 需要新增一個事務類批量購買 batchPurchase事務, 包含了purchase事務。 
     * 把 REQUIRED 換成 REQUIRES_NEW 運行的結果是:用戶余額減少了,第一本書的庫存也減少了。 
     * REQUIRED:如果有事務在運行,當前的方法就在這個事務內運行。否則,就啟動一個新的事務,並在自己的事務內運行。大事務回滾了,小事務跟着一起回滾。 
     * REQUIRES_NEW:當前的方法必須啟動新事務,並在自己的事務內運行。如果有事務在運行,應該將它掛起。大事務雖然回滾了,但是小事務已經結束了。 
     */  
    @Test  
    public void propagationTransaction() {  
        System.out.println("^^^^^^^^^^^^^^^^^@Transactional(propagation) 事務的傳播性");  
        batchPurchaseService.batchPurchase("itdragon", Arrays.asList("java", "spring"));  
    }  
      
    /** 
     * 測試異常不回滾,故意超買(不常用) 
     * 當前用戶余額10元,買了一本價值40元的java書。運行結束后,余額沒有少,java書的庫存減少了(賺了!)。因為設置指定異常不回滾! 
     * 指定異常回滾就不測了。 
     */  
    @Test  
    public void noRollbackForTransaction() {  
        System.out.println("^^^^^^^^^^^^^^^^^@Transactional(noRollbackFor) 設置回滾事務屬性");  
        purchaseService.purchase("itdragon", "java");  
    }  
}  

業務處理接口以及接口實現類

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.jdbc.core.JdbcTemplate;  
import org.springframework.stereotype.Repository;  
  
@Repository("shopDao")  
public class ShopDaoImpl implements ShopDao {  
      
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
    @Override  
    public float getBookPriceBySku(String sku) {  
        String sql = "SELECT price FROM product WHERE sku = ?";  
        /** 
         * 第二個參數要用封裝數據類型,如果用float.class,會提示 Type mismatch affecting row number 0 and column type 'FLOAT':  
         * Value [40.0] is of type [java.lang.Float] and cannot be converted to required type [float] 錯誤 
         */  
        return jdbcTemplate.queryForObject(sql, Float.class, sku);  
    }  
  
    @Override  
    public void updateBookStock(String sku) {  
        // step1 防超賣,購買前先檢查庫存。若不夠, 則拋出異常  
        String sql = "SELECT stock FROM product WHERE sku = ?";  
        int stock = jdbcTemplate.queryForObject(sql, Integer.class, sku);  
        System.out.println("^^^^^^^^^^^^^^^^^商品( " + sku + " )可用庫存 : " + stock);  
        if(stock == 0){  
            throw new ProductException("庫存不足!再看看其他產品吧!");  
        }  
        // step2 更新庫存  
        jdbcTemplate.update("UPDATE product SET stock = stock -1 WHERE sku = ?", sku);  
    }  
  
    @Override  
    public void updateUserBalance(String account, float price) {  
        // step1 下單前驗證余額是否足夠, 若不足則拋出異常  
        String sql = "SELECT balance FROM user WHERE account = ?";  
        float balance = jdbcTemplate.queryForObject(sql, Float.class, account);  
        System.out.println("^^^^^^^^^^^^^^^^^您當前余額 : " + balance + ", 當前商品價格 : " + price);  
        if(balance < price){  
            throw new UserException("您的余額不足!不支持購買!");  
        }  
        // step2 更新用戶余額  
        jdbcTemplate.update("UPDATE user SET balance = balance - ? WHERE account = ?", price, account);  
        // step3 查看用於余額  
        System.out.println("^^^^^^^^^^^^^^^^^您當前余額 : " + jdbcTemplate.queryForObject(sql, Float.class, account));  
    }  
  
} 

最后兩個自定義的異常類

public class UserException extends RuntimeException{  
  
    private static final long serialVersionUID = 1L;  
  
    public UserException() {  
        super();  
    }  
  
    public UserException(String message) {  
        super(message);  
    }  
  
}  
public class ProductException extends RuntimeException{  
  
    private static final long serialVersionUID = 1L;  
  
    public ProductException() {  
        super();  
    }  
  
    public ProductException(String message) {  
        super(message);  
    }  
  
}  

當用戶余額10元不夠買售價為50的書,書的庫存充足的情況。測試basicTransaction()方法打印的結果:用戶余額不減少,庫存也不減少

當用戶余額50元准備購買兩本總價為90的書,但余額只夠買一本書,書的庫存充足的情況,測試propagationTransaction()方法打印的結果:若用 REQUIRES_NEW則兩本中可以買一本;若用REQUIRED則一本都買不了。(事務的傳播性有7種,這里主要介紹常用的REQUIRED和REQUIRES_NEW)

當用戶余額10元不夠買售價40元的書,書的庫存充足的情況。測試noRollbackForTransaction()方法打印的結果:用戶余額沒有減少,但商品庫存減少了,說明事務沒有回滾。

細細品味后,其實也很簡單。事務就是為了保證數據的一致性。出了問題就把之前修改過的數據回滾。


基於xml的事務管理

如果你理解了基於注解的事務管理,那基於xml的事務管理就簡單多了。由於篇幅已經太長了,這里我長話短說。
首先把上面的java類中的所有IOC注解,@Transactional注解和@Autowired去掉。被@Autowired修飾的屬性,還需要另外生成setter方法。
然后配置applicationContext.xml文件。將啟動事務注解的代碼刪掉。將之前用自動掃描包的IOC注解和@Autowired注解的代碼都配置bean(IOC知識),然后 配置事務屬性,最后 配置事務切入點(AOP知識),這是系列博客,不懂的可以看前面幾章。

<?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"  
    xmlns:aop="http://www.springframework.org/schema/aop"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd  
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd  
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">  
          
    <!-- 導入資源文件 -->  
    <context:property-placeholder location="classpath:db.properties"/>  
      
    <!-- 配置 C3P0 數據源 -->  
    <bean id="dataSource"  
        class="com.mchange.v2.c3p0.ComboPooledDataSource">  
        <property name="user" value="${jdbc.user}"></property>  
        <property name="password" value="${jdbc.password}"></property>  
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>  
        <property name="driverClass" value="${jdbc.driverClass}"></property>  
  
        <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>  
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>  
    </bean>  
      
    <!-- 配置 Spirng 的 JdbcTemplate -->  
    <bean id="jdbcTemplate"   
        class="org.springframework.jdbc.core.JdbcTemplate">  
        <property name="dataSource" ref="dataSource"></property>  
    </bean>  
      
    <!-- 配置 NamedParameterJdbcTemplate, 該對象可以使用具名參數, 其沒有無參數的構造器, 所以必須為其構造器指定參數 -->  
    <bean id="namedParameterJdbcTemplate"  
        class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">  
        <constructor-arg ref="dataSource"></constructor-arg>      
    </bean>  
      
    <bean id="shopDao" class="com.itdragon.spring.my.transactionxml.ShopDaoImpl">  
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>  
    </bean>  
    <bean id="purchaseService" class="com.itdragon.spring.my.transactionxml.PurchaseService">  
        <property name="shopDao" ref="shopDao"></property>  
    </bean>  
    <bean id="batchPurchaseService" class="com.itdragon.spring.my.transactionxml.BatchPurchaseService">  
        <property name="purchaseService" ref="purchaseService"></property>  
    </bean>  
      
    <!-- 配置事務管理器 -->  
    <bean id="transactionManager"   
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource"></property>  
    </bean>  
      
    <!-- 配置事務屬性 -->  
    <tx:advice id="txAdvice" transaction-manager="transactionManager">  
        <tx:attributes>  
            <!-- 根據方法名指定事務的屬性 -->  
            <tx:method name="purchase"   
                propagation="REQUIRES_NEW"   
                timeout="3"   
                read-only="false"/>  
            <tx:method name="batchPurchase"/>  
        </tx:attributes>  
    </tx:advice>  
      
    <!-- 配置事務切入點 -->  
    <aop:config>  
        <aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.PurchaseService.purchase(..))"   
            id="pointCut"/>  
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>  
    </aop:config>  
    <aop:config>  
        <aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.BatchPurchaseService.batchPurchase(..))"   
            id="batchPointCut"/>  
        <aop:advisor advice-ref="txAdvice" pointcut-ref="batchPointCut"/>  
    </aop:config>  
</beans> 

代碼親測可用。有什么錯誤地方可以指出。

到這里Spring4 的教程也就結束了。感謝您的觀看!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM