1. 程式人生 > >Spring事務控制--Spring中的事務處理

Spring事務控制--Spring中的事務處理

事務回顧

1)什麼是事務?

事務是邏輯上的一組操作,組成這組操作的各個邏輯單元,要麼一起成功,要麼一起失敗。

2)事務的特性(ACID)

  • 原子性(Atomicity):事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要麼全部完成,要麼完全不起作用。

  • 一致性(Consistency):一旦事務完成(不管成功還是失敗),系統必須確保它所建模的業務處於一致的狀態,而不會是部分完成部分失敗。在現實中的資料不應該被破壞。

  • 隔離性(Isolation):可能有許多事務會同時處理相同的資料,因此每個事務都應該與其他事務隔離開來,防止資料損壞。

  • 永續性(Durability):一旦事務完成,無論發生什麼系統錯誤,它的結果都不應該受到影響,這樣就能從任何系統崩潰中恢復過來。通常情況下,事務的結果被寫到持久化儲存器中。

3)TrancactionDefinition:事務定義資訊

事務定義資訊有:

  • 隔離級別

  • 傳播行為

  • 超時資訊

  • 是否只讀

4)如果不考慮隔離性會引發安全性問題

隔離級別:定義了一個事務可能受其他併發事務影響的程度。
併發事務引起的問題:典型的應用程式中,多個事務併發執行,經常會操作相同的資料來完成各自的任務。併發雖然是必須的,但可能會導致以下的問題。

  • 髒讀(Dirty reads)——髒讀發生在一個事務讀取了另一個事務改寫但尚未提交的資料時。如果改寫在稍後被回滾了,那麼第一個事務獲取的資料就是無效的。

  • 不可重複讀(Nonrepeatable read)——不可重複讀發生在一個事務執行相同的查詢兩次或兩次以上,但是每次都得到不同的資料時。這通常是因為另一個併發事務在兩次查詢期間進行了更新。

  • 幻讀(Phantom read)——幻讀與不可重複讀類似。它發生在一個事務(T1)讀取了幾行資料,接著另一個併發事務(T2)插入了一些資料時。在隨後的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄。

5)解決讀問題:設定事務的隔離級別

  • 未提交讀:髒讀,不可重複讀,虛讀都有可能發生

  • 已提交讀:避免髒讀。但是不可重複讀和虛讀都有可能發生

  • 可重複讀:避免髒讀和不可重複讀。但是虛讀有可能發生

  • 序列化的:避免以上所有讀問題

mysql資料庫的預設隔離級別就是可重複讀,Oracle預設是已提交讀

6)事務的傳播行為

PROPAGION_XXX:事務的傳播行為。

  • 保證在同一個事務中
    PROPAGION_REQUIRED:required , 必須。支援當前事務,如果不存在,就新建一個(預設)
    PROPAGION_SUPPORTS:supports ,支援。支援當前事務,如果不存在,就不使用事務
    PROPAGION_MANDATORY:mandatory ,強制。支援當前事務,如果不存在,就丟擲異常

  • 保證沒有在同一個事務中
    PROPAGION_REQUIRES_NEW:requires_new,必須新的。如果有事務存在,掛起當前事務,建立一個新的事務
    PROPAGION_NOT_SUPPORTED:not_supported ,不支援。以非事務方式執行,如果有事務存在,掛起當前事務
    PROPAGION_NEVER:never,從不。以非事務方式執行,如果有事務存在,丟擲異常
    PROPAGION_NESTED:nested ,巢狀。如果當前事務存在,則巢狀事務執行

關於事務的傳播行為我會另起一篇詳細介紹,最好要理解,實在不理解也沒關係,隨著時間的推移,程式碼的進步會慢慢理解的。

7)事務超時

為了使應用程式很好地執行,事務不能執行太長的時間。因為事務可能涉及對後端資料庫的鎖定,所以長時間的事務會不必要的佔用資料庫資源。事務超時就是事務的一個定時器,在特定時間內事務如果沒有執行完畢,那麼就會自動回滾,而不是一直等待其結束。

8)只讀

這是事務的第三個特性,是否為只讀事務。如果事務只對後端的資料庫進行該操作,資料庫可以利用事務的只讀特性來進行一些特定的優化。通過將事務設定為只讀,你就可以給資料庫一個機會,讓它應用它認為合適的優化措施。


Spring進行事務管理的常用API

1)PlatformTransactionManager:平臺事務管理器

Spring進行事務操作時候,主要使用一個PlatformTransactionManager介面,它表示事務管理器,即真正管理事務的物件。
Spring並不直接管理事務,通過這個介面,Spring為各個平臺如JDBC、Hibernate等都提供了對應的事務管理器,也就是將事務管理的職責委託給Hibernate或者JTA等持久化機制所提供的相關平臺框架的事務來實現。
Spring針對不同的持久化框架,提供了不同PlatformTransactionManager介面的實現類:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager :使用 Spring JDBC或iBatis 進行持久化資料時使用

  • org.springframework.orm.hibernate3.HibernateTransactionManager :使用 Hibernate版本進行持久化資料時使用

2)Spring的這組介面是如何進行事務管理的

平臺事務管理器根據事務定義的資訊進行事務的管理,事務管理的過程中產生一些狀態,將這些狀態記錄到TrancactionStatus裡面。

3)TrancactionStatus:事務的狀態

在上面 PlatformTransactionManager 介面有一個方法getTransaction(),這個方法返回的是 TransactionStatus物件,然後程式根據返回的物件來獲取事務狀態,然後進行相應的操作。
而 TransactionStatus 這個介面的內容如下:
在這裡插入圖片描述
這個介面描述的是一些處理事務提供簡單的控制事務執行和查詢事務狀態的方法,在回滾或提交的時候需要應用對應的事務狀態。


Spring中進行事務操作–案例

Spring進行事務操作的方式

  • 程式設計式事務管理
  • 宣告式事務管理
    • 基於xml配置檔案方式
    • 基於註解方式

搭建案例環境

例子就是模擬銀行轉賬,首先要搭建好轉賬的環境。

1)搭建資料庫環境

## 建立表
CREATE TABLE `NewTable` (
`id`  int(4) NOT NULL AUTO_INCREMENT ,
`name`  varchar(7) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`balance`  decimal(11,0) NOT NULL DEFAULT 0 ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;
## 插入資料
INSERT INTO `spring_jdbc`.`account` (`id`, `name`, `balance`) VALUES ('1', 'zs', '1000');
INSERT INTO `spring_jdbc`.`account` (`id`, `name`, `balance`) VALUES ('2', 'ls', '1000');

2)建立一個web專案,匯入jar包

在這裡插入圖片描述

3)在Spring配置檔案中開啟元件掃描,配置資料庫連線池環境以及jdbcTemplate

<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: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.2.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.2.xsd">
   	<!--指定註解掃描包路徑-->
	<context:component-scan base-package="com.oak"/>

   	<!-- 匯入資原始檔 -->
  	<context:property-placeholder location="classpath:dbutil.properties"/>
  	
  	<!-- 配置dbcp連線池引數 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
   destroy-method="close">
	    <property name="driverClassName" value="${driver}" />
	    <property name="url"
	        value="${url}" />
	    <property name="username" value="${user}" />
	    <property name="password" value="${password}" />
	    <!-- 連線池啟動時的初始值 -->
	    <property name="initialSize" value="${initsize}" />
	    <!-- 連線池的最大值 -->
	    <property name="maxActive" value="${maxsize}" />
	    <!-- 最大空閒值。當經過一個高峰時間後,連線池可以慢慢將已經用不到的連線慢慢釋放一部分,一直減少到maxIdle為止 -->
	    <property name="maxIdle" value="${maxIdle}" />
	    <!-- 最小空閒值。當空閒的連線數少於閥值時,連線池就會預申請去一些連線,以免洪峰來時來不及申請 -->
	    <property name="minIdle" value="${minIdle}" />
	</bean>
	
	<!-- 配置 Spring 的 jdbcTemplate -->
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"/>    
	</bean>
</beans>

4)建立dao層轉賬的具體實現(介面以及實現類)

  • dao介面AccountDao
public interface AccountDao {
	//減少餘額
	void lessenBalance(int id,double balance);
	//增加餘額
	void addBalance(int id,double balance);
}
  • dao實現類AccountDaoImpl
@Repository
public class AccountDaoImpl implements AccountDao{
	@Autowired
	private JdbcTemplate jdbcTemplate;
	@Override
	public void lessenBalance(int id, double balance) {
		jdbcTemplate.update("update account set balance-=? where id=?",balance,id);
	}

	@Override
	public void addBalance(int id, double balance) {
		jdbcTemplate.update("update account set balance+=? where id=?",balance,id);
	}
}

5)編寫service層業務實現(介面和實現類)

  • AccountService介面
public interface AccountService {
	//實現轉賬的業務方法
	void accountBalance(int lessenId,int addId,double balance);
}
  • AccountServiceImpl實現類
@Service
public class AccountServiceImpl implements AccountService{
	@Autowired
	AccountDao accountDao;
	@Override
	public void accountBalance(int lessenId, int addId, double balance) {
		//某個賬號減少金額
		accountDao.lessenBalance(lessenId, balance);
		//走個賬號增加金額
		accountDao.addBalance(addId, balance);
	}
}

6)在測試類中程式設計accountTest測試方法

@Test
public void testAccount(){
	ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
	AccountService accService=ctx.getBean("accountServiceImpl",AccountService.class);
	//從id為1的使用者轉賬給id為2的使用者500元
	accService.accountBalance(1, 2, 500);
}

執行測試檢視資料庫:
在這裡插入圖片描述

7)轉賬中出現的問題

加入在轉賬的過程中出現了一些異常,例如伺服器宕機,銀行斷電等,那這是就會出現一個問題,一個賬號的錢轉了,另一賬號卻沒收到錢,如下,修改accountBalance方法,模擬出現異常:

	public void accountBalance(int lessenId, int addId, double balance) {
		//某個賬號減少金額
		accountDao.lessenBalance(lessenId, balance);
		//模擬出現異常
		int a=5/0;
		//走個賬號增加金額
		accountDao.addBalance(addId, balance);
	}
}

測試檢視資料庫:

在這裡插入圖片描述

檢視結果,zs轉了500,但是ls沒收到,這時應該怎麼解決這個問題呢?就可使用事務來解決。


Spring進行事務操作

Spring進行事務操作方式

  • 程式設計式事務管理(瞭解就行,不要求掌握,這裡就不做案例了)
  • 宣告式事務管理
    • 基於xml配置檔案方式
    • 基於註解方式

Spring的宣告式事務管理——XML方式:思想就是AOP

基於xml配置檔案的方式來進行宣告式事務的操作,不需要進行手動編寫程式碼,通過一段配置完成事務管理。下面我們在以上案例的基礎上實現它。

1)配置事務管理器

我們已經講過Spring針對不同的持久化框架,提供了不同PlatformTransactionManager介面的實現類:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager :使用 Spring JDBC或iBatis 進行持久化資料時使用

  • org.springframework.orm.hibernate3.HibernateTransactionManager :使用 Hibernate版本進行持久化資料時使用

所以要在Spring配置檔案中加入以下配置:

<!-- 1.配置事務的管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 指定要對哪個資料庫進行事務操作 -->
    <property name="dataSource" ref="dataSource"></property>
</bean>

2)配置事務的增強,指定對哪個事務管理器進行增強

需要在xml中引入名稱空間

xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd

配置事務的增強

<!-- 2.配置事務的增強,指定對哪個事務管理器進行增強 -->
<tx:advice id="txadvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--
	            表示來配置你要增強的方法的匹配的一個規則,
	            注意:只須改方法的命名規則,其他都是固定的!
            propagation:事務的傳播行為。
        -->
        <tx:method name="account*" propagation="REQUIRED"></tx:method>
        <!-- <tx:method name="insert*" propagation="REQUIRED"></tx:method> -->
    </tx:attributes>
</tx:advice>

3)配置切入點和切面

<!-- 3.配置切入點和切面(最重要的一步) -->
<aop:config>
    <!-- 切入點 -->
   	<aop:pointcut expression="execution(* com.oak.service.AccountService.*(..))" id="pointcut"/>
   	<!-- 切面,即表示把哪個增強用在哪個切入點上 -->
    <aop:advisor advice-ref="txadvice" pointcut-ref="pointcut"/>
</aop:config>

4)直接測試上個案例中的testAccount方法

先把資料庫中的資料恢復到初始的相同餘額狀態。
在這裡插入圖片描述
直接執行測試方法,執行時異常,檢視資料庫中的資料,並沒有減少,事務被回滾了
在這裡插入圖片描述


Spring的宣告式事務的註解方式

基於註解方式來進行宣告式事務的操作會更加簡單,在實際開發中我們也會用的比較多,我們基於以上案例繼續完成

1)配置事務管理器(同上1)

<!-- 1.配置事務的管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 指定要對哪個資料庫進行事務操作 -->
    <property name="dataSource" ref="dataSource"></property>
</bean>

2)註釋掉事務的其他配置,開啟事務註解

<!-- 2.開啟事務的註解 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

完成之後的配置檔案問:

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

   	<!--指定註解掃描包路徑-->
	<context:component-scan base-package="com.oak"/>

   	<!-- 匯入資原始檔 -->
  	<context:property-placeholder location="classpath:dbutil.properties"/>
  	<!-- 配置dbcp連線池引數 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
   destroy-method="close">
	    <property name="driverClassName" value="${driver}" />
	    <property name="url"
	        value="${url}" />
	    <property name="username" value="${user}" />
	    <property name="password" value="${password}" />
	    <!-- 連線池啟動時的初始值 -->
	    <property name="initialSize" value="${initsize}" />
	    <!-- 連線池的最大值 -->
	    <property name="maxActive" value="${maxsize}" />
	    <!-- 最大空閒值。當經過一個高峰時間後,連線池可以慢慢將已經用不到的連線慢慢釋放一部分,一直減少到maxIdle為止 -->
	    <property name="maxIdle" value="${maxIdle}" />
	    <!-- 最小空閒值。當空閒的連線數少於閥值時,連線池就會預申請去一些連線,以免洪峰來時來不及申請 -->
	    <property name="minIdle" value="${minIdle}" />
	</bean>
	<!-- 配置 Spring 的 jdbcTemplate -->
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"/>    
	</bean>
	
	<!-- 1.配置事務的管理器 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	    <!-- 指定要對哪個資料庫進行事務操作 -->
	    <property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<!-- 2.開啟事務的註解 -->
	<tx:annotation-driven transaction-manager="transactionManager">	</tx:annotation-driven>
</beans>

3)在具體使用事務的方法所在的類上面添加註解:@Transactional

在AccountServiceImpl上加上該註解

@Service//業務層註解
@Transactional//事務控制註解
public class AccountServiceImpl implements AccountService{
	@Autowired//依賴注入
	AccountDao accountDao;
	@Override
	public void accountBalance(int lessenId, int addId, double balance) {
		//某個賬號減少金額
		accountDao.lessenBalance(lessenId, balance);
		//模擬出現異常
		int a=5/0;
		//走個賬號增加金額
		accountDao.addBalance(addId, balance);
	}
}

4)直接測試

直接測試上個案例的測試方法,資料庫中的資料沒有發生變法。
在這裡插入圖片描述