(四)Spring中的事務管理

TyCoding發表於2018-07-05

全有或全無的操作稱為事務。事務允許你將幾個操作組合成一個要麼發生要麼不發生的工作單元。我們可以用四個詞來表示事務:
> **原子性:** 原子性確保事務中的所有操作全部發生或全部不發生。(所有操作成功,事務也就成功;任意一個操作失敗,事務就失敗並回滾)。
> **一致性:** 一旦事務完成,系統必須確保它所建模的業務處於一直狀態。
> **隔離性:** 事務允許多個使用者對相同的資料進行操作,所以所有的操作應該相隔離。
> **永續性:** 一旦事務完成,事務的結果應該持久化。

<!--more-->
在介紹Spring的事務管理之前,我們首先要介紹一下Spring的JDBC模板技術。
# Spring框架的JDBC模板技術
Spring框架中提供了很多模板類來實現簡化程式設計。比如最基本的JDBC模板(Spring框架提供了`JdbcTemplate`類)
我們以以一個簡單的案例來演示如何使用Spring框架提供的JDBC模板類

1. 建立資料表結構
```
create database springlearn;
use springlearn;
create table t_account(
  id int primary key auto_increment,
  name varchar(100),
  money double
);
```
2. 方式一:通過new `DriverManagerDataSource`來插入資料
```java
@Test
public void run(){
    //建立連線池,使用Spring框架內建的連線池
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql:///springlearn");
    dataSource.setUsername("root");
    dataSource.setPassword("root");
    //建立模板類
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    //完成資料的新增
    jdbcTemplate.update("insert into t_account values (null,?,?)","TyCoding",1000);
}
```
此時已經在表中新增了一行資料。
**注意:**
  1. `jdbc:mysql:///`等價於`jdbc:mysql://localhost:3306`
  2. 此時我們先不要插入中文,可能會出現亂碼情況。

##  使用Spring框架來管理模板類
上面的案例中我們使用的是new的方式來建立的jdbc模板類,下面我們用Spring來管理這些模板類
1. `spring.xml`
```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: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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

    <context:component-scan base-package="spring_2"/>
    <!-- Spring的資料庫連線池 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///springlearn"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
    <!-- Spring的模板類 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 將連線池物件注入到模板類中 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>
```
**注意:**
  類似上面我們使用new的方式建立物件,步驟仍是先建立連線池物件(此處使用的Spring內建的連線池物件),然後建立模板類物件(並將連線池物件注入到模板類物件中)

2. `Test測試類`
```java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class Demo4Test {
    @Resource(name="jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Test
    public void run2(){
        jdbcTemplate.update("insert into t_account values(null,?,?)","TyCoding2",1000);
    }
}
```
**注意:**
  1. Spring整合了JUnit測試,所以我們這裡使用了Spring的註解方法載入配置檔案`spring.xml`
      * `@RunWith()`用於獲取測試類物件
      * `@ContextConfiguration()`用於載入配置檔案
  2. 大家還記得前面我們已經介紹了Spring的注入物件方式,其中`@Resource`和`@Autowired`註解都可以實現將一個Java物件交給Spring,通過載入Spring上下文來注入此物件。因為此時我們在Java類中注入的Bean物件名是`jdbcTemplate`,而`spring.xml`中也配置了名字是`jdbcTemplate`Bean的屬性,那麼在Java類中的`jdbcTemplate`物件就擁有了一些屬性。增加name屬性僅是為了縮小查詢Bean的範圍。
  3. 綜上我們注入了Bean物件,並載入了配置檔案,就可以直接執行相關sql語句了


此時已經插入了兩條資料

# Spring框架的事務管理
上面我們簡單的介紹了Spring框架如何通過模板類執行SQL語句的,下面我們就分析一下Spring對事務的管理:
上面我們已經介紹了事務的四個特性。
> 原子性
> 一致性
> 隔離性
> 永續性
下面我們仍要了解一些名詞概念
1. Spring的事務分類:
> 編碼式事務
> 宣告式事務

2. 事務的屬性
> 傳播行為: 傳播行為定義了客戶端與被調方法之間的事務邊界。(Spring定義了7中不同的傳播行為)
> 隔離級別:定義一個事務可能受到其他併發事務的影響程度。
    * 髒讀:一個事務讀取了另一個事務尚改寫但尚未提交的資料
    * 不可重複讀:一個事務執行相同的查詢多次,每次得到不同資料。(併發訪問造成的)
    * 幻讀:類似不可重複讀。一個事務讀取時遇到另一個事務的插入,則這個事務就會讀取到一些原本不存在的記錄。
> 只讀:事務只讀時,資料庫就可以對其進行一些特定的優化。
> 事務超時:事務執行時間過長。
> 回滾原則:定義那些異常會導致事務回滾。(預設情況只有執行時異常才事務回滾)

3. Spring框架的事務管理相關的類和API
> 1.PlatformTransactionManager介面:平臺事務管理器(真正管理事務的類)
>       該介面的實現類:
>           1.`DataSourceTransactionManager`:用在當使用Spring的JDBC模板類或Mybatis框架時
>           2.`HibernateTransactionManager`:使用Hibernate框架時
> 2.TransactionDefinition介面:事務定義資訊(事務隔離級別、傳播行為、超時、只讀)
> 3.TransactionStatus介面:事務的狀態


## 宣告式事務
Spring有兩種事務,但是我們這裡只介紹宣告式事務,編碼式事務不常用,所以這裡不再介紹。
在案例之前我們瞭解一下2種常用連線池的不同寫法:
**2種連線池**
1. **c3p0**
```xml
   <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <!--配置連結屬性-->
            <property name="driverClass" value="${jdbc.driver}"/>
            <property name="jdbcUrl" value="${jdbc.url}"/>
            <property name="user" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.username}"/>
     </bean>
```
2. **druid** 阿里的連線池
```xml
   <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <!--配置連結屬性-->
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
    </bean>
```
注意:採用`${jdbc.xx}`的寫法前提是在外邊定義了`xx.properties`檔案,並在`spring.xml`中引入了該配置檔案
<br/>
### 在XML中定義事務
下面開始我們的案例
1. 首先我們要引入Spring提供的`tx`名稱空間
```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: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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
```
**注意**
不但需要`tx`的名稱空間,還需要`aop`的名稱空間,以為Spring的宣告式事務式的支援是通過Spring AOP框架實現的。
通過一個XML片段瞭解一下`<tx:advice>`是如何宣告事務性策略的:
```xml
<tx:advice id="txAdvice">
  <tx:attributes>
    <tx:method name="save*" propagation="REQUIRED"/>
    <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
  </tx:attributes>
</tx:advice>
```
**解釋:**
`<tx:advice>`實現宣告事務性策略,所有的事務配置都在改元素下定義。`<tx:method>`元素為name屬性指定的方法定義事務引數。我們看一下`<tx:method>`的屬性:
隔離級別|含義
-|:-:|-:
isolation|指定事務的隔離級別
propagation|定義事務的傳播規則
read-only|指定事務為只讀
回滾規則:<br>rollback-for<br>no-rollback-for|rollback-for指定了事務對於那些檢查型異常應當回滾而不提交<br/>no-rollback-for指定事務對於那些異常應當繼續執行而不回滾
timeout|對於長時間執行的事務定義超時時間

當使用`<tx:advice>`來宣告事務時,我們還需要一個事務管理器(常用`DataSourceTransactionManager`),然後使用`transaction-manager`屬性指定事務管理器的id:
```xml
<tx:advice id="txAdvice" transaction-manager="txManager">
  ...
</tx:advice>
```

`<tx:advice>`僅定義了AOP通知,用於把事務邊界通知給方法。但這些只是事務通知,而不是完成的事務性切面。所以我們還需要使用`<aop:config>`定義一個通知器(advisor)
```xml
<aop:config>
  <aop:advisor pointcut="execution(* xx.xx.xxx(..))" advice-ref="txAdvice">
</aop:config>
```

看了上面的解釋你是否有一些思路了呢?下面我們通過案例(轉賬)來體會一下:
1. 目錄結構:

初始資料:

2. `AccountService.java`
```java
package spring_2;
public interface AccountService {
    void pay(String out, String in, double money);
}
```
3. `AccountServiceImp.java`
```java
package spring_2;
import javax.annotation.Resource;
public class AccountServiceImp implements AccountService {
    @Autowired
    private AccountDao accountDao;

    public void pay(String out, String in, double money) {
        //扣錢
        accountDao.outMoney(out,money);
        //模擬異常
        //int a = 10/0;
        //加錢
        accountDao.inMoney(in,money);
    }
}
```
4. `AccountDao.java`
```java
package spring_2;
public interface AccountDao {
    void outMoney(String out,double money);
    void inMoney(String in,double money);
}
```
5. `AccoundDaoImp.java`
```java
package spring_2;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
public class AccountDaoImp extends JdbcDaoSupport implements AccountDao {
    public void outMoney(String out,double money){
        this.getJdbcTemplate().update("update t_account set money = money - ? where name = ?",money,out);
    }
    public void inMoney(String in,double money){
        this.getJdbcTemplate().update("update t_account set money = money + ? where name = ?",money,in);
    }
}
```
6. `spring.xml`
```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: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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

    <!-- 使用c3p0的連線池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///springlearn"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

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

    <!-- 注入Bean -->
    <bean id="accountService" class="spring_2.AccountServiceImp"/>
    <bean id="accountDao" class="spring_2.AccountDaoImp">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事務增強 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--
                name            :繫結事務的方法名,可以使用萬用字元,可以配置多個
                propagation     :傳播行為
                isolation       :隔離級別
                read-only       :是否只讀
                timeout         :超時資訊
                rollback-for    :發生哪些異常回滾
                no-rollback-for :發生哪些異常不回滾
            -->
            <tx:method name="pay" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置AOP切面代理 -->
    <aop:config>
        <!-- 如果是自己寫的切面,使用<aop:aspect>標籤;如果是系統的,用<aop:advisor> -->
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* spring_2.AccountServiceImp.pay(..))"/>
    </aop:config>
</beans>
```
7. `Test測試類`
```java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class AccountTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void run3(){
        accountService.pay("TyCoding","tutu",100);
    }
}
```




### 定義註解驅動的事務
我們不但可以使用XML定義事務驅動,還可以用使用註解`@Transaction`
秩序要改動`spring.xml`和`AccountServiceImp.java`即可以大大簡化程式碼,如下:
1. `AccountServiceImp.java`
```java
@Transactional
public class AccountServiceImp implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void pay(String out, String in, double money) {
        //扣錢
        accountDao.outMoney(out,money);
        //int a = 10/0;
        //加錢
        accountDao.inMoney(in,money);
    }
}
```
2. `spring.xml`
```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: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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

    <context:component-scan base-package="spring_2"/>

    <!-- 使用c3p0的連線池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///springlearn"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

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

    <!-- 注入Bean -->
    <bean id="accountService" class="spring_2.AccountServiceImp"/>
    <bean id="accountDao" class="spring_2.AccountDaoImp">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--&lt;!&ndash; 配置事務增強 &ndash;&gt;
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            &lt;!&ndash;
                name            :繫結事務的方法名,可以使用萬用字元,可以配置多個
                propagation     :傳播行為
                isolation       :隔離級別
                read-only       :是否只讀
                timeout         :超時資訊
                rollback-for    :發生哪些異常回滾
                no-rollback-for :發生哪些異常不回滾
            &ndash;&gt;
            <tx:method name="pay" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    &lt;!&ndash; 配置AOP切面代理 &ndash;&gt;
    <aop:config>
        &lt;!&ndash; 如果是自己寫的切面,使用<aop:aspect>標籤;如果是系統的,用<aop:advisor> &ndash;&gt;
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* spring_2.AccountServiceImp.pay(..))"/>
    </aop:config>-->
</beans>
```
如圖所示,我們只需要寫一個`<tx:annotation-driven>`即可代替如上的XML配置,它允許在最有意義的位置宣告事務規則:在事務性方法上。

`<tx:annotation-driven>`元素告訴Spring檢查上下文中所有的Bean並查詢使用`@Transaction`註解的Bean,而不管這個註解是用在類級方法上還是方法級別上。
對於每一個使用`@Transactional`註解的Bean,`<tx:annotation-driven>`會自動為它新增事務通知。通知的事務屬性是通過`@Transactional`註解的引數定義的。



如上無論哪種方式,當我們把`AccountServiceImp.java`中的`int a = 10 / 0;`以上都會報錯,事務並回滾。





<br/>

# 交流

如果大家有興趣,歡迎大家加入我的Java交流群:671017003 ,一起交流學習Java技術。博主目前一直在自學JAVA中,技術有限,如果可以,會盡力給大家提供一些幫助,或是一些學習方法,當然群裡的大佬都會積極給新手答疑的。所以,別猶豫,快來加入我們吧!

<br/>

# 聯絡

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

- [Blog@TyCoding's blog](http://www.tycoding.cn)
- [GitHub@TyCoding](https://github.com/TyCoding)
- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)



相關文章