Spring4 事務管理

Spring4 事務管理

本章是Spring4 教程中的最後一章,也是很是重要的一章。若是說學習IOC是知識的入門,那學習事務管理就是知識的提高。本章篇幅可能有一丟丟長,也有一丟丟難,須要讀者細細品味。主要從三個方面開始:事務簡介,基於註解的事務管理 和基於xml的事務管理。java


準備環境

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

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的事務管理來解決這種問題。
咱們是誰???萬能的程序員
圖片描述spring


基於註解的事務管理

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

<?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>

接下來是事務的業務代碼,全部類都放在了一個目錄下,沒別的緣由,就是由於懶。
20170927104230756數據庫

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

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(重點,知識點說明app

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");  
    }  
}

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

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()方法打印的結果:用戶餘額不減小,庫存也不減小
20170926183628059

當用戶餘額50元準備購買兩本總價爲90的書,但餘額只夠買一本書,書的庫存充足的狀況,測試propagationTransaction()方法打印的結果:若用 REQUIRES_NEW則兩本中能夠買一本;若用REQUIRED則一本都買不了。(事務的傳播性有7種,這裏主要介紹經常使用的REQUIRED和REQUIRES_NEW)
20170927094402928
20170927094445263

當用戶餘額10元不夠買售價40元的書,書的庫存充足的狀況。測試noRollbackForTransaction()方法打印的結果:用戶餘額沒有減小,但商品庫存減小了,說明事務沒有回滾。
20170926184345738
20170926184326602
細細品味後,其實也很簡單。事務就是爲了保證數據的一致性。出了問題就把以前修改過的數據回滾。


基於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 的教程也就結束了。感謝您的觀看!!!

相關文章
相關標籤/搜索