1. 程式人生 > 程式設計 >《Java架構築基》——漫談Spring事務處理機制

《Java架構築基》——漫談Spring事務處理機制

1. 認識事務

大家所瞭解的事務Transaction,它是一些列嚴密操作動作,要麼都操作完成,要麼都回滾撤銷。Spring事務管理基於底層資料庫本身的事務處理機制。資料庫事務的基礎,是掌握Spring事務管理的基礎。這篇總結下Spring事務。 事務具備ACID四種特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔離性)和Durability(永續性)的英文縮寫。

1.1 原子性(Atomicity)

事務最基本的操作單元,要麼全部成功,要麼全部失敗,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

1.2 一致性(Consistency)

事務的一致性指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。如果事務成功地完成,那麼系統中所有變化將正確地應用,系統處於有效狀態。如果在事務中出現錯誤,那麼系統中的所有變化將自動地回滾,系統返回到原始狀態。

1.3 隔離性(Isolation)

指的是在併發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間。由併發事務所做的修改必須與任何其他併發事務所做的修改隔離。事務檢視資料更新時,資料所處的狀態要麼是另一事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會檢視到中間狀態的資料。

1.4 永續性(Durability)

指的是隻要事務成功結束,它對資料庫所做的更新就必須永久儲存下來。即使發生系統崩潰,重新啟動資料庫系統後,資料庫還能恢復到事務成功結束時的狀態。

2. 事務的傳播特性

事務傳播行為就是多個事務方法呼叫時,如何定義方法間事務的傳播。Spring定義了7中傳播行為:

  • propagation_requierd:如果當前沒有事務,就新建一個事務,如果已存在一個事務中,加入到這個事務中,這是Spring預設的選擇。
  • propagation_supports:支援當前事務,如果沒有當前事務,就以非事務方法執行。
  • propagation_mandatory:使用當前事務,如果沒有當前事務,就丟擲異常。
  • propagation_required_new:新建事務,如果當前存在事務,把當前事務掛起。
  • propagation_not_supported:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
  • propagation_never:以非事務方式執行操作,如果當前事務存在則丟擲異常。
  • propagation_nested:如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與propagation_required類似的操作。

3. 事務的隔離級別

  • read uncommited:是最低的事務隔離級別,它允許另外一個事務可以看到這個事務未提交的資料。
  • read commited:保證一個事物提交後才能被另外一個事務讀取。另外一個事務不能讀取該事物未提交的資料。
  • repeatable read:這種事務隔離級別可以防止髒讀,不可重複讀。但是可能會出現幻象讀。它除了保證一個事務不能被另外一個事務讀取未提交的資料之外還避免了以下情況產生(不可重複讀)。
  • serializable:這是花費最高代價但最可靠的事務隔離級別。事務被處理為順序執行。除了防止髒讀,不可重複讀之外,還避免了幻象讀
  • 髒讀、不可重複讀、幻象讀概念說明:
    • a.髒讀:指當一個事務正字訪問資料,並且對資料進行了修改,而這種資料還沒有提交到資料庫中,這時,另外一個事務也訪問這個資料,然後使用了這個資料。因為這個資料還沒有提交那麼另外一個事務讀取到的這個資料我們稱之為髒資料。依據髒資料所做的操作肯能是不正確的。
    • b.不可重複讀:指在一個事務內,多次讀同一資料。在這個事務還沒有執行結束,另外一個事務也訪問該同一資料,那麼在第一個事務中的兩次讀取資料之間,由於第二個事務的修改第一個事務兩次讀到的資料可能是不一樣的,這樣就發生了在一個事物內兩次連續讀到的資料是不一樣的,這種情況被稱為是不可重複讀。
    • c.幻象讀:一個事務先後讀取一個範圍的記錄,但兩次讀取的紀錄數不同,我們稱之為幻象讀(兩次執行同一條 select 語句會出現不同的結果,第二次讀會增加一資料行,並沒有說這兩次執行是在同一個事務中)

4. 事務幾種實現方式

  • 程式設計式事務管理對基於POJO的應用來說是唯一選擇。我們需要在程式碼中呼叫beginTransaction()、commit()、rollback()等事務管理相關的方法,這就是程式設計式事務管理。
  • 基於 TransactionProxyFactoryBean的宣告式事務管理
  • 基於 @Transactional 的宣告式事務管理
  • 基於Aspectj AOP配置事務

5. 舉例說明事務不同實現

程式設計式事務基本已經OUT了,所有就省略了,主要回顧 下宣告式事務。 以使用者購買股票為例 新建使用者物件、股票物件、以及dao、service層

/**
 * 賬戶物件
 *
 */
public class Account {

    private int accountid;
    private String name;
    private int balance;


    public int getAccountid() {
        return accountid;
    }
    public void setAccountid(int accountid) {
        this.accountid = accountid;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getBalance() {
        return balance;
    }
    public void setBalance(int balance) {
        this.balance = balance;
    }
}
複製程式碼
/**
 * 股票物件
 *
 */
public class Stock {
 
	private int stockid;
	private String name;
	private Integer count;
	
	public Stock() {
		super();
	}
	 
	public Stock(int stockid,String name,Integer count) {
		super();
		this.stockid = stockid;
		this.name = name;
		this.count = count;
	}
 
	public int getStockid() {
		return stockid;
	}
 
	public void setStockid(int stockid) {
		this.stockid = stockid;
	}
 
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Integer getCount() {
		return count;
	}
	public void setCount(Integer count) {
		this.count = count;
	}
	
}
複製程式碼

DAO層

public interface AccountDao {
 
	void addAccount(String name,double money);
	
	void updateAccount(String name,double money,boolean isbuy);
	
}
複製程式碼
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
 
	@Override
	public void addAccount(String name,double money) {
		String sql = "insert account(name,balance) values(?,?);";
		this.getJdbcTemplate().update(sql,name,money);
		
	}
 
	@Override
	public void updateAccount(String name,boolean isbuy) {
		String sql = "update account set balance=balance+? where name=?";
		if(isbuy)
			sql = "update account set balance=balance-? where name=?";
		this.getJdbcTemplate().update(sql,money,name);
	}
	
}
複製程式碼
public interface StockDao {
	
	void addStock(String sname,int count);
	
	void updateStock(String sname,int count,boolean isbuy);
 
}
public class StockDaoImpl extends JdbcDaoSupport implements StockDao {
 
	@Override
	public void addStock(String sname,int count) {
		String sql = "insert into stock(name,count) values(?,?)";
		this.getJdbcTemplate().update(sql,sname,count);
	}
 
	@Override
	public void updateStock(String sname,boolean isbuy) {
		String sql = "update stock set count = count-? where name = ?";
		if(isbuy)
			sql = "update stock set count = count+? where name = ?";
		this.getJdbcTemplate().update(sql,count,sname);
	}
	
}
複製程式碼

Service

public interface BuyStockService {
 
	public void addAccount(String accountname,double money);
	
	public void addStock(String stockname,int amount);
	
	public void buyStock(String accountname,String stockname,int amount) throws BuyStockException;
	
}
複製程式碼
public class BuyStockServiceImpl implements BuyStockService{
	
	private AccountDao accountDao;
	private StockDao stockDao;
	
	@Override
	public void addAccount(String accountname,double money) {
		accountDao.addAccount(accountname,money);
	}
 
	@Override
	public void addStock(String stockname,int amount) {
		stockDao.addStock(stockname,amount);
	}
 
	@Override
	public void buyStock(String accountname,int amount) throws BuyStockException {
		boolean isBuy = true;
		accountDao.updateAccount(accountname,isBuy);
		if(isBuy==true){
			throw new BuyStockException("購買股票發生異常");
		}
			stockDao.updateStock(stockname,amount,isBuy);
	}
 
	public AccountDao getAccountDao() {
		return accountDao;
	}
 
	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}
 
	public StockDao getStockDao() {
		return stockDao;
	}
 
	public void setStockDao(StockDao stockDao) {
		this.stockDao = stockDao;
	}
	
}
複製程式碼

自定義異常類

public class BuyStockException extends Exception {
 
	public BuyStockException() {
		super();
	}
 
	public BuyStockException(String message) {
		super(message);
	}
 
}
複製程式碼

(1)基於 TransactionProxyFactoryBean的宣告式事務管理

<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-aop-4.2.xsd
        ">

    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 註冊資料來源 C3P0 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
        <property name="driverClass" value="${jdbc.driverClass}"></property>
        <property name="jdbcUrl"  value="${jdbc.url}"></property>
        <property name="user"  value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <bean id="accountDao" class="com.zwd.spring.transaction.daoImpl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="stockDao" class="com.zwd.spring.transaction.daoImpl.StockDaoImpl">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="buyStockService" class="com.zwd.spring.transaction.serviceImpl.BuyStockServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
        <property name="stockDao" ref="stockDao"></property>
    </bean>


    <!-- 事務管理器 -->

    <bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 事務代理工廠 -->
    <!-- 生成事務代理物件 -->
    <bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="myTracnsactionManager"></property>
        <property name="target" ref="buyStockService"></property>
        <property name="transactionAttributes">
            <props>
                <!-- 主要 key 是方法
                    ISOLATION_DEFAULT  事務的隔離級別
                    PROPAGATION_REQUIRED  傳播行為
                -->
                <prop key="add*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED</prop>
                <!-- -Exception 表示發生指定異常回滾,+Exception 表示發生指定異常提交 -->
                <prop key="buyStock">ISOLATION_DEFAULT,PROPAGATION_REQUIRED,-BuyStockException</prop>
            </props>
        </property>

    </bean>


</beans>  
複製程式碼

通過結果和觀察資料庫資料變化,可以看出我們宣告的異常回滾發生了效果。

(2)基於 @Transactional 的宣告式事務管理

其他類不做改變,只改變購買股票介面實現類和配置檔案

public class BuyStockServiceImpl implements BuyStockService{
 
	private AccountDao accountDao;
	private StockDao stockDao;
	
	@Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED)
	@Override
	public void addAccount(String accountname,money);
		
	}
 
	@Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED)
	@Override
	public void addStock(String stockname,amount);
		
	}
 
	public BuyStockServiceImpl() {
		// TODO Auto-generated constructor stub
	}
	
	@Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED,rollbackFor=BuyStockException.class)
	@Override
	public void buyStock(String accountname,isBuy);
		
	}
 
	public AccountDao getAccountDao() {
		return accountDao;
	}
 
	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}
 
	public StockDao getStockDao() {
		return stockDao;
	}
 
	public void setStockDao(StockDao stockDao) {
		this.stockDao = stockDao;
	}
	
}
複製程式碼
	<context:property-placeholder location="classpath:jdbc.properties"/>
	
	<!-- 註冊資料來源 C3P0 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
		 <property name="driverClass" value="${jdbc.driverClass}"></property>
		 <property name="jdbcUrl"  value="${jdbc.url}"></property>
         <property name="user"  value="${jdbc.username}"></property>
         <property name="password" value="${jdbc.password}"></property>
	</bean>
	
	<bean id="accountDao" class="com.zwd.spring.transaction.daoImpl.AccountDaoImpl">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	
	<bean id="stockDao" class="com.zwd.spring.transaction.daoImpl.StockDaoImpl">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	
	<bean id="buyStockService" class="tcom.zwd.spring.transaction.serviceImpl.BuyStockServiceImpl">
		<property name="accountDao" ref="accountDao"></property>
		<property name="stockDao" ref="stockDao"></property>
	</bean>
	
	
	<!-- 事務管理器 -->
	<bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<!-- 啟用事務註解 -->
	<tx:annotation-driven transaction-manager="myTracnsactionManager"/>
複製程式碼

可以看出,使用@Transactional註解的方式配置檔案要簡單的多,將事務交給事務註解驅動。它有個缺陷是他會把所有的連線點都作為切點將事務織入進去, 顯然只需要在buyStock()方法織入事務即可。下面看看最後一種實現,它就可以精準的織入到指定的連線點

(3)基於Aspectj AOP配置事務

public class BuyStockServiceImpl implements BuyStockService{
 
	private AccountDao accountDao;
	private StockDao stockDao;
	
	@Override
	public void addAccount(String accountname,money);
		
	}
 
	@Override
	public void addStock(String stockname,amount);
		
	}
 
	public BuyStockServiceImpl() {
		// TODO Auto-generated constructor stub
	}
	
	@Override
	public void buyStock(String accountname,isBuy);
		
	}
 
	public AccountDao getAccountDao() {
		return accountDao;
	}
 
	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}
 
	public StockDao getStockDao() {
		return stockDao;
	}
 
	public void setStockDao(StockDao stockDao) {
		this.stockDao = stockDao;
	}
	
}
複製程式碼
	<context:property-placeholder location="classpath:jdbc.properties"/>
	
	<!-- 註冊資料來源 C3P0 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
		 <property name="driverClass" value="${jdbc.driverClass}"></property>
		 <property name="jdbcUrl"  value="${jdbc.url}"></property>
         <property name="user"  value="${jdbc.username}"></property>
         <property name="password" value="${jdbc.password}"></property>
	</bean>
	
	<bean id="accountDao" class="com.zwd.spring.transaction.daoImpl.AccountDaoImpl">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	
	<bean id="stockDao" class="com.zwd.spring.transaction.daoImpl.StockDaoImpl">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	
	<bean id="buyStockService" class="com.zwd.spring.transaction.serviceImpl.BuyStockServiceImpl">
		<property name="accountDao" ref="accountDao"></property>
		<property name="stockDao" ref="stockDao"></property>
	</bean>
	
	
	<!-- 事務管理器 -->
	<bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<tx:advice id="txAdvice" transaction-manager="myTracnsactionManager">
		<tx:attributes>
			<!-- 為連線點指定事務屬性 -->
			<tx:method name="add*" isolation="DEFAULT" propagation="REQUIRED"/>
			<tx:method name="buyStock" isolation="DEFAULT" propagation="REQUIRED" rollback-for="BuyStockException"/>
		</tx:attributes>
	</tx:advice>
	
	<aop:config>
		<!-- 切入點配置 -->
		<aop:pointcut expression="execution(* *..serviceImpl.*.*(..))" id="point"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="point"/>
	</aop:config>
複製程式碼