Spring整合事務管理
不用每次 db操作 都要 開啟事務 提交事務之類 了
抽取出來
事務管理方式:
程式設計式的(麻煩)
宣告式的事務管理:
不同的框架機制 有不同的 TransactionManager JDBC Mybatis
建表:
create DATABASE spring; use spring; CREATE table book( isbn VARCHAR(50) PRIMARY KEY, book_name VARCHAR(100), price int ); CREATE TABLE book_stock( isbnVARCHAR(50) PRIMARY KEY, stock int, CHECK(stock>0) ); CREATE TABLE account( username VARCHAR(50) PRIMARY key, balance int, CHECK(balance>0) );
MySQL不支援 檢查約束
配置事務管理器:
<!-- 配置事務管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> <!-- 管理資料來源 --> </bean> <!-- 啟用事務註解 --> <tx:annotation-driven transaction-manager="transactionManager"/>
然後加註解於方法上:
@Transactional
事務的傳播行為:
當事務方法被另一個事務方法呼叫時,必須制定事務應該如何傳播,事務如何傳給被呼叫的那個哈哈。 例如: 方法可能繼續在現有事務中執行,也可開啟一個新事務,並在自己的事務中執行。
事務的傳播行為可以由傳播屬性指定。Spirng 定義了7種傳播行為
REQUIRED 傳播行為
當 bookService 的 purchase() 方法被另一個事務方法 checkout() 呼叫時, 它預設會在現有的事務內執行. 這個預設的傳播行為就是 REQUIRED. 因此在 checkout() 方法的開始和終止邊界內只有一個事務. 這個事務只在 checkout() 方法結束的時候被提交, 結果使用者一本書都買不了
事務傳播屬性可以在 @Transactional 註解的 propagation 屬性中定義
REQUIRES_NEW 傳播行為(被呼叫的事務)
另一種常見的傳播行為是 REQUIRES_NEW. 它表示該方法必須啟動一個新事務, 並在自己的事務內執行. 如果有事務在執行, 就應該先掛起它.
事務的隔離級別:
事務併發時候 容易出現 髒讀 不可重複度 幻讀
當同一個應用程式或者不同應用程式中的多個事務在同一個資料集上併發執行時, 可能會出現許多意外的問題
併發事務所導致的問題可以分為下面三種類型:
髒讀: 對於兩個事物 T1, T2, T1 讀取了已經被 T2 更新但 還沒有被提交的欄位. 之後, 若 T2 回滾, T1讀取的內容就是臨時且無效的.
不可重複讀: 對於兩個事物 T1, T2, T1 讀取了一個欄位, 然後 T2 更新了該欄位. 之後, T1再次讀取同一個欄位, 值就不同了.
幻讀: 對於兩個事物 T1, T2, T1 從一個表中讀取了一個欄位, 然後 T2 在該表中插入了一些新的行. 之後, 如果 T1 再次讀取同一個表, 就會多出幾行.
事務的屬性:
noRollbackFor={UserAccountException.class}) //指定哪些異常不需要需要回滾
readOnly=true //指定事務是否只讀 如果只是讀取資料庫的話 可以不用加鎖 幫助資料庫引擎優化
timeout 指定強制回滾之前事務可以佔用的時間 即便是不超異常 但是如果磨磨唧唧的沒有處理完 也會回滾
總結:事務的隔離級別 事務的傳播行為 事務的回滾屬性(對哪些異常進行回滾 事務的只讀屬性 事務的佔用時間)
介面:
package com.atguigu.spring.tx; public interface BookShopDao { //根據書號獲取書的單價 public int findBookPriceByIsbn(String isbn); //更新數的庫存. 使書號對應的庫存 - 1 public void updateBookStock(String isbn); //更新使用者的賬戶餘額: 使 username 的 balance - price public void updateUserAccount(String username, int price); }
package com.atguigu.spring.tx; public interface BookShopService { public void purchase(String username, String isbn); }
package com.atguigu.spring.tx; import java.util.List; public interface Cashier { public void checkout(String username, List<String> isbns); }
實現類:
package com.atguigu.spring.tx; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository("bookShopDao") //基於註解的 public class BookShopDaoImpl implements BookShopDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public int findBookPriceByIsbn(String isbn) { String sql = "SELECT price FROM book WHERE isbn = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, isbn); } @Override public void updateBookStock(String isbn) { //檢查書的庫存是否足夠, 若不夠, 則丟擲異常 String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?"; int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn); if(stock == 0){ throw new BookStockException("庫存不足!"); } String sql = "UPDATE book_stock SET stock = stock -1 WHERE isbn = ?"; jdbcTemplate.update(sql, isbn); } @Override public void updateUserAccount(String username, int price) { //驗證餘額是否足夠, 若不足, 則丟擲異常 String sql2 = "SELECT balance FROM account WHERE username = ?"; int balance = jdbcTemplate.queryForObject(sql2, Integer.class, username); if(balance < price){ throw new UserAccountException("餘額不足!"); } String sql = "UPDATE account SET balance = balance - ? WHERE username = ?"; jdbcTemplate.update(sql, price, username); } }
package com.atguigu.spring.tx; 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("bookShopService") public class BookShopServiceImpl implements BookShopService { @Autowired private BookShopDao bookShopDao; //新增事務註解 //1.使用 propagation 指定事務的傳播行為, 即當前的事務方法被另外一個事務方法呼叫時 //如何使用事務, 預設取值為 REQUIRED, 即使用呼叫方法的事務 //REQUIRES_NEW: 事務自己的事務, 呼叫的事務方法的事務被掛起. //2.使用 isolation 指定事務的隔離級別, 最常用的取值為 READ_COMMITTED //3.預設情況下 Spring 的宣告式事務對所有的執行時異常進行回滾. 也可以通過對應的 //屬性進行設定. 通常情況下去預設值即可. //4.使用 readOnly 指定事務是否為只讀. 表示這個事務只讀取資料但不更新資料, //這樣可以幫助資料庫引擎優化事務. 若真的事一個只讀取資料庫值的方法, 應設定 readOnly=true //5.使用 timeout 指定強制回滾之前事務可以佔用的時間. // @Transactional(propagation=Propagation.REQUIRES_NEW, // isolation=Isolation.READ_COMMITTED, // noRollbackFor={UserAccountException.class}) //指定哪些異常不需要需要回滾 @Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED, //隔離級別 讀已提交 readOnly=false, timeout=3) @Override public void purchase(String username, String isbn) { try { Thread.sleep(5000); } catch (InterruptedException e) {} //1. 獲取書的單價 int price = bookShopDao.findBookPriceByIsbn(isbn); //2. 更新數的庫存 bookShopDao.updateBookStock(isbn); //3. 更新使用者餘額 bookShopDao.updateUserAccount(username, price); } }
package com.atguigu.spring.tx; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service("cashier") public class CashierImpl implements Cashier { @Autowired private BookShopService bookShopService; @Transactional //當前事務 呼叫另外一個事務方法 當前的這個事務如何進行傳播的 @Override public void checkout(String username, List<String> isbns) { for(String isbn: isbns){ bookShopService.purchase(username, isbn); } } }
異常類:
package com.atguigu.spring.tx; public class BookStockException extends RuntimeException{ /** * */ private static final long serialVersionUID = 1L; public BookStockException() { super(); // TODO Auto-generated constructor stub } public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); // TODO Auto-generated constructor stub } public BookStockException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub } public BookStockException(String message) { super(message); // TODO Auto-generated constructor stub } public BookStockException(Throwable cause) { super(cause); // TODO Auto-generated constructor stub } }
package com.atguigu.spring.tx; public class UserAccountException extends RuntimeException{ /** * */ private static final long serialVersionUID = 1L; public UserAccountException() { super(); // TODO Auto-generated constructor stub } public UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); // TODO Auto-generated constructor stub } public UserAccountException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub } public UserAccountException(String message) { super(message); // TODO Auto-generated constructor stub } public UserAccountException(Throwable cause) { super(cause); // TODO Auto-generated constructor stub } }
測試:
package com.atguigu.spring.tx; import static org.junit.Assert.*; import java.util.Arrays; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTransactionTest { private ApplicationContext ctx = null; private BookShopDao bookShopDao = null; private BookShopService bookShopService = null; private Cashier cashier = null; { ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); bookShopDao = ctx.getBean(BookShopDao.class); bookShopService = ctx.getBean(BookShopService.class); cashier = ctx.getBean(Cashier.class); } @Test //測試事務傳播行為 public void testTransactionlPropagation(){ cashier.checkout("AA", Arrays.asList("1001", "1002")); } @Test public void testBookShopService(){ bookShopService.purchase("AA", "1001"); } @Test public void testBookShopDaoUpdateUserAccount(){ bookShopDao.updateUserAccount("AA", 200); } @Test public void testBookShopDaoUpdateBookStock(){ bookShopDao.updateBookStock("1001"); } @Test public void testBookShopDaoFindPriceByIsbn() { System.out.println(bookShopDao.findBookPriceByIsbn("1001")); } }
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: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.atguigu.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> <!-- 啟用事務註解 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
db.properties
jdbc.user=root
jdbc.password=1230
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql:///spring
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
目錄結構: