本章是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>
接下來是事務的業務代碼,全部類都放在了一個目錄下,沒別的緣由,就是由於懶。
數據庫
核心是消費事務類 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()方法打印的結果:用戶餘額不減小,庫存也不減小
當用戶餘額50元準備購買兩本總價爲90的書,但餘額只夠買一本書,書的庫存充足的狀況,測試propagationTransaction()方法打印的結果:若用 REQUIRES_NEW則兩本中能夠買一本;若用REQUIRED則一本都買不了。(事務的傳播性有7種,這裏主要介紹經常使用的REQUIRED和REQUIRES_NEW)
當用戶餘額10元不夠買售價40元的書,書的庫存充足的狀況。測試noRollbackForTransaction()方法打印的結果:用戶餘額沒有減小,但商品庫存減小了,說明事務沒有回滾。
細細品味後,其實也很簡單。事務就是爲了保證數據的一致性。出了問題就把以前修改過的數據回滾。
若是你理解了基於註解的事務管理,那基於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 的教程也就結束了。感謝您的觀看!!!