1. 程式人生 > >Spring4深入理解----事務(宣告式事務和xml配置事務,事務傳播屬性,事務其他屬性(隔離級別&回滾&只讀&過期))

Spring4深入理解----事務(宣告式事務和xml配置事務,事務傳播屬性,事務其他屬性(隔離級別&回滾&只讀&過期))

  •事務管理是企業級應用程式開發中必不可少的技術用來確保資料的完整性和一致性.   •事務就是一系列的動作,它們被當做一個單獨的工作單元.這些動作要麼全部完成,要麼全部不起作用   •事務的四個關鍵屬性(ACID)     –原子性(atomicity):事務是一個原子操作,由一系列動作組成.事務的原子性確保動作要麼全部完成要麼完全不起作用.     –一致性(consistency):一旦所有事務動作完成,事務就被提交.資料和資源就處於一種滿足業務規則的一致性狀態中.     –隔離性(isolation):可能有許多事務會同時處理相同的資料,因此每個事物都應該與其他事務隔離開來,防止資料損壞
.     –永續性(durability):一旦事務完成,無論發生什麼系統錯誤,它的結果都不應該受到影響.通常情況下,事務的結果被寫到持久化儲存器中. Spring 中的事務管理
    •作為企業級應用程式框架, Spring 在不同的事務管理 API 之上定義了一個抽象層.而應用程式開發人員不必瞭解底層的事務管理API,就可以使用Spring的事務管理機制.     •Spring 既支援程式設計式事務管理,也支援宣告式的事務管理.     •程式設計式事務管理: 將事務管理程式碼嵌入到業務方法中來控制事務的提交和回滾.在程式設計式管理事務時,必須在每個事務操作中包含額外的事務管理程式碼
.     •宣告式事務管理: 大多數情況下比程式設計式事務管理更好用.將事務管理程式碼從業務方法中分離出來, 以宣告的方式來實現事務管理.事務管理作為一種橫切關注點,可以通過AOP方法模組化.Spring 通過 SpringAOP 框架支援宣告式事務管理. Spring 中的事務管理器
    •Spring 從不同的事務管理API中抽象了一整套的事務機制.開發人員不必瞭解底層的事務API,就可以利用這些事務機制.有了這些事務機制, 事務管理程式碼就能獨立於特定的事務技術了.     •Spring 的核心事務管理抽象是 Interface PlatFormTransactionManager
它為事務管理封裝了一組獨立於技術的方法.無論使用Spring的哪種事務管理策略(程式設計式或宣告式),事務管理器都是必須的.

1.宣告式事務

1).用事務通知宣告式地管理事務

    •事務管理是一種橫切關注點     •為了在 Spring2.x 中啟用宣告式事務管理, 可以通過txSchema 中定義的 <tx:advice> 元素宣告事務通知,為此必須事先將這個Schema定義新增到<beans>根元素中去.     •聲明瞭事務通知後, 就需要將它與切入點關聯起來.由於事務通知是在<aop:config>元素外部宣告的,所以它無法直接與切入點產生關聯.所以必須<aop:config> 元素中宣告一個增強器通知與切入點關聯起來.     •由於 SpringAOP 是基於代理的方法, 所以只能增強公共方法.因此,只有公有方法才能通過 Spring AOP 進行事務管理.

建表在最後面,需要注意的是本例項沒有用Hibernate或相關的框架,用的是JDBC處理事務,下面這個是基於註解 的

Dao層:

public interface BookShopDao {
    //根據書號獲取書的單價
    int findBookPriceByIsbn(String isbn);

    //更新數的庫存. 使書號對應的庫存 - 1
   void updateBookStock(String isbn);

    //更新使用者的賬戶餘額: 使 username 的 balance - price
    void updateUserAccount(String username, int price);
}
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public int findBookPriceByIsbn(String isbn) {
        String sql = "SELECT price FROM book WHERE isbn = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
    }

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

    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);
    }
}
定義兩個自定義異常
public class BookStockException extends RuntimeException{
    private static final long serialVersionUID = 1L;
    public BookStockException() {
        super();
    }

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

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

    public BookStockException(String message) {
        super(message);
    }
    public BookStockException(Throwable cause) {
        super(cause);
    }
}
public class UserAccountException extends RuntimeException{
    private static final long serialVersionUID = 1L;

    public UserAccountException() {
        super();
    }

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

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

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

    public UserAccountException(Throwable cause) {
        super(cause);
    }
}
Service層
public interface BookShopService {
    void purchase(String username, String isbn);
}
@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)
    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);
    }

}
public class SpringTransactionTest {

	private ApplicationContext ctx = null;
	private BookShopDao bookShopDao = null;
	private BookShopService bookShopService = null;
	private Cashier cashier = null;
	
	{
		ctx = new ClassPathXmlApplicationContext("Spring4_JDBC/applicationContext-tx.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"));
	}

}
配置檔案
<?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/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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

       <context:component-scan base-package="Spring4_JDBC.tx"/>
       <!--匯入資原始檔-->
       <context:property-placeholder location="classpath:Spring4_JDBC/db.properties"/>
       <!--配置C3P0資料來源-->
       <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
              <property name="user" value="${jdbc.user}"/>
              <property name="password" value="${jdbc.password}"/>
              <property name="driverClass" value="${jdbc.driverClass}"/>
              <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>

              <property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
              <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
       </bean>

       <!-- 配置Spring 的JdbcTemplate-->
       <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
              <property name="dataSource" ref="dataSource"/>
       </bean>

       <!-- 1. 配置事務管理器,管理JDBC的 -->
       <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
              <property name="dataSource" ref="dataSource"/>
       </bean>

       <!-- 2. 配置事務屬性 -->
       <tx:advice id="txAdvice" transaction-manager="transactionManager">
              <tx:attributes>
                     <!-- 根據方法名指定事務的屬性 -->
                     <tx:method name="purchase" propagation="REQUIRES_NEW"/>
                     <tx:method name="get*" read-only="true"/>
                     <tx:method name="find*" read-only="true"/>
                     <tx:method name="*"/>
              </tx:attributes>
       </tx:advice>

       <!-- 3. 配置事務切入點, 以及把事務切入點和事務屬性關聯起來 -->
       <aop:config>
              <aop:pointcut expression="execution(* Spring4_JDBC.tx.*.*(..))"
                            id="txPointCut"/>
              <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
       </aop:config>
</beans>
2).用 @Transactional註解宣告式地管理事務
    •除了在帶有切入點,通知和增強器的Bean配置檔案中宣告事務外,Spring 還允許簡單地用 @Transactional註解來標註事務方法.     •為了將方法定義為支援事務處理的, 可以為方法新增@Transactional 註解. 根據SpringAOP 基於代理機制, 只能標註公有方法.     •可以在方法或者類級別上新增 @Transactional註解.當把這個註解應用到類上時,這個類中的所有公共方法都會被定義成支援事務處理的.     •Bean 配置檔案中只需要啟用<tx:annotation-driven>元素,併為之指定事務管理器就可以了.     •如果事務處理器的名稱是 transactionManager,就可以在<tx:annotation-driven>元素中省略transaction-manager屬性.這個元素會自動檢測該名稱的事務處理器.

3).事務傳播屬性

    •當事務方法被另一個事務方法呼叫時,必須指定事務應該如何傳播.例如:方法可能繼續在現有事務中執行,也可能開啟一個新事務,並在自己的事務中執行.     •事務的傳播行為可以由傳播屬性指定.Spring 定義了 7 種類傳播行為. 4).Spring 支援的事務傳播行為

REQUIRED 傳播行為
    •bookServicepurchase()方法被另一個事務方法checkout()呼叫時,它預設會在現有的事務內執行.這個預設的傳播行為就是REQUIRED.因此在checkout()方法的開始和終止邊界內只有一個事務.這個事務只在checkout()方法結束的時候被提交,結果使用者一本書都買不了     •事務傳播屬性可以在@Transactional註解的propagation屬性中定義

REQUIRES_NEW 傳播行為

    •另一種常見的傳播行為是 REQUIRES_NEW.它表示該方法必須啟動一個新事務,並在自己的事務內執行.如果有事務在執行,就應該先掛起它.

4).事務其他屬性(隔離級別&回滾&只讀&過期).

併發事務所導致的問題

    •當同一個應用程式或者不同應用程式中的多個事務在同一個資料集上併發執行時,可能會出現許多意外的問題     •併發事務所導致的問題可以分為下面三種類型:       –髒讀: 對於兩個事物T1,T2, T1  讀取了已經被T2更新但還沒有被提交的欄位. 之後,T2回滾,T1讀取的內容就是臨時且無效的.       –不可重複讀:對於兩個事物T1,T2, T1  讀取了一個欄位,然後T2更新了該欄位.之後,T1再次讀取同一個欄位, 值就不同了.       –幻讀:對於兩個事物T1,T2, T1  從一個表中讀取了一個欄位,然後T2在該表中插入了一些新的行.之後,如果T1再次讀取同一個表,就會多出幾行. 事務的隔離級別
    •從理論上來說, 事務應該彼此完全隔離,以避免併發事務所導致的問題.然而,那樣會對效能產生極大的影響,因為事務必須按順序執行.     •在實際開發中, 為了提升效能,事務會以較低的隔離級別執行.     •事務的隔離級別可以通過隔離事務屬性指定

設定隔離事務屬性

    •@Transactional註解宣告式地管理事務時可以在@Transactionalisolation屬性中設定隔離級別.     •Spring2.x 事務通知中, 可以在配置檔案中<tx:method>元素中指定隔離級別 設定回滾事務屬性
    •預設情況下只有未檢查異常(RuntimeExceptionError型別的異常)會導致事務回滾.而受檢查異常不會.     •事務的回滾規則可以通過 @Transactional註解的rollbackFornoRollbackFor屬性來定義.這兩個屬性被宣告為Class[]型別的,因此可以為這兩個屬性指定多個異常類.       –rollbackFor遇到時必須進行回滾       –noRollbackFor:一組異常類,遇到時必須不回滾 設定回滾屬性即可以用註解 ,也可以用xml配置檔案,同上

超時和只讀屬性

    •由於事務可以在行和表上獲得鎖因此長事務會佔用資源,並對整體效能產生影響.     •如果一個事物只讀取資料但不做修改,資料庫引擎可以對這個事務進行優化.     •超時事務屬性: 事務在強制回滾之前可以保持多久.這樣可以防止長期執行的事務佔用資源.timeout
    •只讀事務屬性: 表示這個事務只讀取資料但不更新資料,這樣可以幫助資料庫引擎優化事務.readOnly
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,
       CHECK(stock > 0)       
);

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

INSERT INTO book (isbn, book_name,price) VALUES("1001","Java",100),("1002","Oracle",70);
INSERT INTO account(username,balance) VALUES("AA",160);
INSERT INTO book_stock(isbn,stock)VALUES("1001",4),("1002",8);