天天看點

聊聊資料庫的事務

文章目錄

    • 事務的介紹
      • 事務是什麼?
      • 事務有哪些特征(ACDI)?
      • 事務有什麼用?
    • 在JDBC中使用事務
    • Spring聲明式事務的使用
      • Spring聲明式資料庫事務約定
      • @Transactional 的配置項
      • Spring 事務管理器
      • 測試資料庫事務
      • 隔離級别
      • 傳播行為
      • @Transactional 自調用失效問題
友情提示:為了将知識點講清楚,本篇部落格文字性内容可能會多,建立分段閱讀,切勿煩躁,重要文字小編已使用不同格式标注出來了。

事務的介紹

事務是什麼?

事務(Transaction)是并發控制的基本機關。所謂的事務,它是一個 操作序列,這些操作要麼都執行,要麼都不執行,它是一個 不可分割 的工作機關。

事務有哪些特征(ACDI)?

  • 原子性(Atomicity):事務是一個不可分割的最小機關,事務中的操作要麼都執行成功,要麼都執行失敗。舉例:銀行轉賬,轉賬人扣款和收款人收款,這兩個步驟要麼同時成功,要麼同時失敗,不能隻發生其中一個步驟。
  • 一緻性(Consistency):事務執行前後,資料庫資料的完整性必須保持一緻。舉例:銀行轉賬前後雙方的金額總數保持一緻。
  • 隔離性(Isolation):多個使用者并發通路資料庫時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作資料所幹擾,多個并發事務之間要互相隔離。舉例:多個使用者操縱,防止資料幹擾,就要為每個客戶開啟一個自己的事務。
  • 持久性(Durability):一個事務一旦被送出,它對資料庫中資料的改變就是永久性的,接下來即使資料庫發生故障(如斷電重新開機)也不應該對其有任何影響。 舉例:如果使用

    commit

    将事務送出後,無論發生什麼都 都不會影響到我送出的資料。

事務有什麼用?

舉個例子:李明在超市買了100元商品,使用微信支付給超市支付100元,這裡其實有兩步操作。

  • 第一步:李明微信餘額扣款100元。
  • 第二步:超市賬戶餘額收款100元。

假如第一步剛執行完,超市就停電了,李明微信餘額被扣了100元,而超市賬戶這邊卻沒收到錢,這不就出大問題了嗎,不得打架了嗎?

為了解決資料庫資料一緻性問題,資料庫事務應運而生。

在JDBC中使用事務

不過無論如何都應該先配置資料庫的資訊,是以我們先在

application.properties

中進行如下代碼配置。

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter6
spring.datasource.username=root
spring.datasource.password=123456
#最大等待連接配接中的數量,設0為沒有限制
spring.datasource.tomcat.max-idle=10
#最大連接配接活動數
spring.datasource.tomcat.max-active=50
#最大等待毫秒數,機關為ms,超過時間會出錯誤資訊
spring.datasource.tomcat.max-wait=10000
#資料庫連接配接池初始化連接配接數
spring.datasource.tomcat.initial-size=5

#日志配置
logging.level.root=debug
logging.level.org.springframework=debug
logging.level.org.mybatis=debug
           

通過這樣的配置就己經在項目中定義好了資料庫連接配接池,這樣就可以使用資料庫了,本篇文章後面的内容就可以使用它了。在 Spring 資料庫事務中可以使用 程式設計式事務,也可以使用 聲明式事務。大部分的情況下,會使用聲明式事務。程式設計式事務這種比較底層的方式已經基本被淘汰了,SpringBoot 也不推薦我們使用,是以這裡不再讨論程式設計式事務。這裡将日志降低為 DEBUG 級别,這樣就可以看到很詳細的日志了,這樣有助于觀察 Spring 資料庫事務機制的運作。由于目前 MyBatis 已經被廣泛地使用在持久層中,是以本篇文章将以 MyBatis 架構作為持久層進行論述。

擴充:聲明式事務和程式設計式事務的差別?
  • 聲明式事務:通過AOP(面向切面)方式在方法前使用程式設計式事務的方法開啟事務,在方法後送出或復原。用配置檔案的方法或注解方法(如:@Transactional)控制事務。
  • 程式設計式事務:手動開啟、送出、復原事務。

為了讓讀者有更加直覺的認識,我們先從JDBC的代碼入手,這裡采用的就是 程式設計式事務。首先是一段熟悉的JDBC進行插入使用者的測試:

package com.springboot.chapter6.service.impl;

import com.springboot.chapter6.service.JdbcService;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@Service
public class JdbcServiceImpl implements JdbcService {

    @Autowired
    private DataSource dataSource;


    @Override
    public int insertUser(String userName, String note) {
        Connection conn = null;
        int result = 0;
        try {
            // 擷取連接配接
            conn = dataSource.getConnection();
            // 開啟事務
            conn.setAutoCommit(false);
            // 設定隔離級别
            conn.setTransactionIsolation(TransactionIsolationLevel.READ_COMMITTED.getLevel()); // 讀已送出
            // ---------- 業務代碼 ----------
            // 執行SQL
            PreparedStatement ps = conn.prepareStatement("insert into t_user(user_name,note) values(?,?)");
            ps.setString(1,userName);
            ps.setString(2,note);
            result = ps.executeUpdate();
            // -----------------------------
            // 送出事務
            conn.commit();
        }catch (Exception e){
            // 復原事務
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException el) {
                    el.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            // 關閉資料庫連接配接
            try {
                if (conn != null && conn.isClosed()) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}
           

閱讀代碼可以發現,業務代碼隻有那麼一丢丢,其他的都是有關JDBC的功能代碼,我們看到了資料庫連接配接的擷取和關閉以及事務的送出和復原、大量的

try... catch... finally...

語句。要知道,我們隻是執行一條SQL而己,如果執行多條SQL,這代碼顯然是更加難以控制的。

于是人們就開始不斷地優化,使用 Hibernate、MyBatis 都可以減少這些代碼,但是依舊不能減少開閉資料庫連接配接和事務控制的代碼,而 AOP 給這些帶來了福音。知道 AOP 的小夥伴們,可以知道 AOP 允許我們把那些公共的代碼抽取出來,單獨實作,為了更好地論述,下面畫出上面代碼執行SQL的流程圖,如【圖6-1】所示。

聊聊資料庫的事務

這個流程與我們 AOP 約定流程十分接近,而在圖中,有業務邏輯的部分也隻是執行SQL那一步驟,其他的步驟都是比較固定的,按照 AOP 的設計思想,就可以把除執行SQL這步之外的步驟抽取出來單獨實作,這便是 Spring 資料庫事務程式設計的思想。

Spring聲明式事務的使用

我們要使用Spring為我們提供的聲明式事務之前,要先掌握以下約定。

Spring聲明式資料庫事務約定

為了”擦除“令人厭煩的

try... catch... finally...

語句,減少那些資料庫連接配接開閉和事務復原送出的代碼,Spring 利用其 AOP 為我們提供了一個資料庫事務的約定流程。通過這個約定流程就可以減少大量的備援代碼和一些沒有必要的

try... catch... finally...

語句,讓開發者能夠更加集中于業務的開發,而不是資料庫連接配接資源和事務的功能開發,這樣開發的代碼可讀性就更高,也更好維護。

對于事務,需要通過标注告訴Spring在什麼地方啟用資料庫事務功能。對于聲明式事務,是使用

@Transactional

進行标注的。這個注解可以标注在 類或者方法上,當它标注在類上時,代表這個類所有 公共(public)非靜态 的方法都将啟用事務功能。在

@Transactional

中,還允許配置許多的屬性,如事務的 隔離級别 和 傳播行為,這是本篇文章的核心内容;又如異常類型,進而确定方法發生什麼異常下復原事務或者發生什麼異常下不復原事務等。這些配置内容,是在 SpringIoC 容器在加載時就會将這些配置資訊解析出來,然後把這些資訊存到 事務定義器(

TransactionDefinition

接口的實作類)裡,并且記錄哪些類或者方法需要啟動事務功能,采取什麼政策去執行事務。這個過程中,我們所需要做的隻是給需要事務的類或者方法标注

@Transactional

和配置其屬性而己,并不是很複雜。

有了

@Transactional

的配置,Spring 就會知道在哪裡啟動事務機制,其約定流程如【圖6-2】所示。

聊聊資料庫的事務

因為這個約定非常重要,是以這裡做進一步的讨論。

當 Spring 的上下文開始調用被

@Transactional

标注的類或者方法時,Spring 就會産生 AOP 的功能。請注意事務的底層需要啟用 AOP 功能,這是Spring事務的底層實作,後面我們會看到一些陷阱。那麼當它啟動事務時,就會根據 事務定義器 内的配置去設定事務,首先是根據 傳播行為 去确定事務的政策。有關傳播行為後面我們會再談,這裡暫且放下。然後是 隔離級别、逾時時間、隻讀 等内容的設定,隻是這步設定事務并不需要開發者完成,而是 Spring事務攔截器 根據

@Transactional

配置的内容來完成的。

在上述場景中,Spring通過對注解

@Transactional

屬性配置去設定資料庫事務,跟着 Spring 就會開始調用開發者編寫的業務代碼。執行開發者的業務代碼,可能發生異常,也可能不發生異常。在 Spring 資料庫事務的流程中,它會根據是否發生異常采取不同的政策。

如果都沒有發生異常,Spring 資料庫攔截器就會幫助我們送出事務,這點也并不需要我們幹預。如果發生異常,就要判斷一次事務定義器内的配置,如果事務定義器己經約定了該類型的異常不復原事務就送出事務,如果沒有任何配置或者不是配置不復原事務的異常,則會復原事務,并且将異常抛出,這步也是由事務攔截器完成的。

無論發生異常與否,Spring 都會釋放事務資源,這樣就可以保證資料庫連接配接池正常可用了,這也是由 Spring 事務攔截器完成的内容。

在上述場景中,我們還有一個重要的事務配置屬性沒有讨論,那就是 傳播行為。它是屬于事務方法之間調用的行為,後面我們會對其做更為詳細的讨論。但是無論怎麼樣,從流程中我們可以看到開發者在整個流程中隻需要完成業務邏輯即可,其他的使用Spring事務機制和其配置即可,這樣就可以把

try... catch... finally...

、資料庫連接配接管理和事務送出復原的代碼交由 Spring 攔截器完成,而隻需要完成業務代碼即可,是以你可以經常看到下面代碼所示的簡潔代碼。

package com.springboot.chapter6.service.impl;

import com.springboot.chapter6.dao.UserDao;
import com.springboot.chapter6.pojo.User;
import com.springboot.chapter6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    @Transactional // 聲明式事務注解
    public int insertUser(User user) {
        return userDao.insertUser(user);
    }
}
           

這裡僅僅是使用一個

@Transactional

注解,辨別 insertUser 方法需要啟動事務機制,那麼 Spring 就會按照【圖6-2】那樣,把 insertUser 方法織入約定的流程中,這樣對于資料庫連接配接的閉合、事務送出與復原都不再需要我們編寫任何代碼了,可見這是十分便利的。從代碼中,可以看到隻需要完成對應的業務邏輯便可以了,這樣就可以大幅減少代碼,同時代碼也具備更高的可讀性和可維護性。

@Transactional 的配置項

注解

@Transactional

的源碼分析:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
	// 通過bean name 指定事務管理器
    @AliasFor("transactionManager")
    String value() default "";

	// 同 value 屬性
    @AliasFor("value")
    String transactionManager() default "";

	// 指定傳播行為
    Propagation propagation() default Propagation.REQUIRED;

	// 指定隔離級别
    Isolation isolation() default Isolation.DEFAULT;

	// 指定逾時時間(機關秒)
    int timeout() default -1;

	// 是否隻讀事務
    boolean readOnly() default false;

	// 方法在發生指定異常時復原,預設是所有異常都復原
    Class<? extends Throwable>[] rollbackFor() default {};

	// 方法在發生指定異常名稱時復原,預設是所有異常都復原
    String[] rollbackForClassName() default {};

	// 方法在發生指定異常時不復原,預設是所有異常都復原
    Class<? extends Throwable>[] noRollbackFor() default {};

	// 方法在發生指定異常名稱時不復原,預設是所有異常都復原
    String[] noRollbackForClassName() default {};
}
           

value 和 transactionManager 屬性是配置一個 Spring的事務管理器,關于它後面會進行詳細讨論;timeout 是事務可以允許存在的時間戳,機關為秒;readOnly 屬性定義的是事務是否是隻讀事務;rollbackFor、rollbackForClassName、noRollbackFor 和 noRollbackForClassName 都是指定異常,我們從流程中可以看到在帶有事務的方法時,可能發生異常,通過這些屬性的設定可以指定在什麼異常的情況下依舊送出事務,在什麼異常的情況下復原事務,這些可以根據自己的需要進行指定。以上這些都比較好了解,真正麻煩的是

propagation

isolation

這兩個屬性。

propagation

指的是 傳播行為,

isolation

則是 隔離級别,它需要了解資料庫的特性才能使用,而這兩個麻煩的東西,就是本篇文章的核心内容,也是網際網路企業最為關心的内容之一,是以值得我們後面花較大篇幅去講解它們的内容和使用方法。由于這裡使用到了事務管理器,是以我們接下來先讨論一下Spring的事務管理器。

關于注解

@Transactional

值得注意的是它可以放在接口上,也可以放在實作類上。但是 Spring 團隊推薦放在實作類上,因為放在接口上将使得你的類基于接口的代理時它才生效。學習過 AOP 的小夥伴們,就能知道在 Spring 可以使用 JDK動态代理,也可以使用 CGLIG動态代理。如果使用接口,那麼你将不能切換為CGLIB動态代理,而隻能允許你使用JDK動态代理,并且使用對應的接口去代理你的類,這樣才能驅動這個注解,這将大大地限制你的使用,是以在實作類上使用

@Transactional

注解才是最佳的方式。

Spring 事務管理器

上述的事務流程中,事務的 打開、復原和送出 是由 事務管理器 來完成的。在Spring中,事務管理器的頂層接口為

PlatformTransactionManager

,Spring 還為此定義了一些列的接口和類,如【圖6-3】所示。

聊聊資料庫的事務

當我們引入其他架構時,還會有其他的事務管理器的類,比方說我們引入 Hibernate,那麼 Spring orm 包還會提供

HibernateTransactionManager

與之對應并給我們使用。因為本篇文章會以 MyBatis 架構去讨論 Spring 資料庫事務方面的問題,最常用到的事務管理器是

DataSourceTransactionManager

。從【圖6-3】可以看到它也是一個實作了接口

PlatfonnTransactionManager

的類,為此可以看到

PlatformTransactionManager

接口的源碼,如一下代碼所示:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager extends TransactionManager {
	// 擷取事務,它還會設定資料屬性
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

	// 送出事務
    void commit(TransactionStatus var1) throws TransactionException;

	// 復原事務
    void rollback(TransactionStatus var1) throws TransactionException;
}
           

顯然這些方法并不難了解,隻需要簡單地介紹一下它們便可以了。Spring 在事務管理時,就是将這些方法按照約定織入對應的流程中的,其中 getTransaction 方法的參數是一個 事務定義器(

TransactionDefinition

),它是依賴于我們配置的

@Transactional

的配置項生成的,于是通過它就能夠設定事務的屬性了,而送出和復原事務也就可以通過 commit 和 rollback 方法來執行。在 SpringBoot 中,當你依賴于

mybatis-spring-boot-starter

之後,它會自動建立一個

DataSource­TransactionManager

對象,作為事務管理器,如果依賴于

spring-boot-starter-data-jpa

,則它會自動建立

JpaTransactionManager

對象作為事務管理器,是以我們一般不需要自己建立事務管理器而直接使用它們即可。

測試資料庫事務

首先我們來建立一張表,SQL語句如下:

-- 建立使用者表
create table t_user (
	id int(12) auto_increment,
	user_name varchar(60) not null,
	note varchar(512),
	primary key(id)
)
           

為了與它映射起來,需要使用一個POJO,代碼如下:

package com.springboot.chapter6.pojo;

public class User {
    private Long id;
    private String userName;
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}
           

再給出一個MyBatis接口:

package com.springboot.chapter6.dao;

import com.springboot.chapter6.pojo.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserDao {
	// 擷取使用者資訊
    User getUser(Long id);
    // 新增使用者
    int insertUser(User user);
}
           

接着是與這個MyBatis接口檔案對應的一個映射檔案,代碼如下,它提供SQL與相關POJO的映射規則。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.springboot.chapter6.dao.UserDao">
    <select id="getUser" parameterType="long" resultType="com.springboot.chapter6.pojo.User">
        select id, user_name as userName, note from t_user where id= #{id}
    </select>
    
    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
        insert into t_user(user_name,note) value(#{userName},#{note})
    </insert>
</mapper>
           

<insert> 元素定義的屬性

useGeneratedKeys

keyProperty

,則表示在插入之後使用資料庫生成機制回填對象的主鍵。

接着需要建立一個 UserService 和它的實作類 UserServicelmpl,然後通過

@Transactioanal

啟用 Spring資料庫事務機制,代碼如下:

package com.springboot.chapter6.service;

import com.springboot.chapter6.pojo.User;

public interface UserService {
    // 擷取使用者資訊
    User getUser(Long id);
    // 新增使用者
    int insertUser(User user);
}
           
package com.springboot.chapter6.service.impl;

import com.springboot.chapter6.dao.UserDao;
import com.springboot.chapter6.pojo.User;
import com.springboot.chapter6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,timeout = 1)
    public User getUser(Long id) {
        return userDao.getUser(id);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,timeout = 1)
    public int insertUser(User user) {
        return userDao.insertUser(user);
    }
}
           

代碼中的方法上标注了注解

@Transactional

,意味着這兩個方法将啟用 Spring資料庫事務機制。在事務配置中,采用了 讀寫送出 的隔離級别,後面我們将讨論隔離級别的含義,這裡的代碼還會限制逾時時間為 1s。然後可以寫一個控制器,用來測試事務的啟用情況,代碼所下:

package com.springboot.chapter6.controller;

import com.springboot.chapter6.pojo.User;
import com.springboot.chapter6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    // 測試擷取使用者
    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser(Long id){
        return userService.getUser(id);
    }

    // 測試插入使用者
    @RequestMapping("/insertUser")
    @ResponseBody
    public Map<String,Object> insertUser(String userName,String note){
        User user = new User();
        user.setUserName(userName);
        user.setNote(note);
        // 結果會回填主鍵,傳回插入條數
        int update = userService.insertUser(user);
        Map<String,Object> result = new HashMap<>();
        result.put("success",update == 1);
        result.put("user",user);
        return result;
    }
}
           

有了這個控制器,我們還需要給 SpringBoot 配置 MyBatis 架構的内容,于是需要在配置檔案

application.properties

中加入以下代碼:

#MyBatis映射檔案通配
mybatis.mapper-locations=classpath:mapper/*.xml
           

這樣 MyBatis 架構就配置完了。依賴于

mybatis-spring-boot-starter

之後,SpringBoot 會自動建立事務管理器、MyBatis 的 SqlSessionFactory 和 SqlSessionTemplate等内容。下面我們需要配置 SpringBoot 的運作檔案,以達到測試的目的,并且檢視 SpringBoot 自動為我們建立的事務管理器、SqlSessionFactory 和 SqISessionTemplate 資訊。啟動檔案如下代碼:

package com.springboot.chapter6;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.PlatformTransactionManager;

import javax.annotation.PostConstruct;

@SpringBootApplication
@MapperScan(basePackages = "com.springboot.chapter6.dao")
public class Chapter6Application {

    public static void main(String[] args) {
        SpringApplication.run(Chapter6Application.class, args);
    }

    // 注入事務管理器,它由SpringBoot自動生成
    @Autowired
    PlatformTransactionManager transactionManager;

    // 使用後初始化方法,觀察自動生成的事務管理器
    @PostConstruct
    public void viewTransactionManager(){
        // 啟動前加入斷點觀測
        System.out.println(transactionManager.getClass().getName());
    }
}
           

首先這裡使用了

@MapperScan

掃描對應的包,這樣就可以把 MyBatis 對應的接口檔案掃描到 SpringIoC 容器中了。這裡通過注解

@Autowired

直接注入了事務管理器,它是通過 SpringBoot 的機制自動生成的,并不需要我們去關心;而在 viewTransactionManager 方法中,加入了注解

@PostConstruct

,是以在這個類對象被初始化後,會調用這個方法,在這個方法中,因為先前已經将 IoC 容器注入進來,是以可以通過 IoC 容器擷取對應的 Bean 以監控它們,并且在控制台列印的地方加入斷點,這樣我們以 debug 的方式啟動它就可以進入斷點了。下圖就是我在啟動時監控得到的内容。

聊聊資料庫的事務

從圖中可以看到,SpringBoot 已經生成了事務管理器,這便是 SpringBoot 的魅力,允許我們以最小的配置代價運作 Spring 的項目。那麼按照之前的約定使用注解

@Transactional

标注類和方法後,Spring 的事務攔截器就會同時使用事務管理器的方法開啟事務,然後将代碼織入 Spring 資料庫事務的流程中,如果發生異常,就會復原事務,如果不發生異常,那麼就會送出事務,這樣就我們從大量的備援代碼中解放出來了,是以我們可以看到在服務類(Service)中代碼是比較簡單明了的。下面我們打開浏覽器,在位址欄中輸入http://localhost:8080/user/insertUser?userName=zhangsan&note=zs,就能看到日志列印出來:

聊聊資料庫的事務

從日志中,我們可以看到 Spring 擷取了資料庫連接配接,并且修改了隔離級别,然後執行SQL,在最後會自動地關閉和送出資料庫事務,因為我們對方法标注了

@Transactional

,是以 Spring 會把對應方法的代碼織入約定的事務流程中。

隔離級别

上面我們隻是簡單地使用事務,這裡将讨論 Spring事務機制中最重要的兩個配置項,即 隔離級别 和 傳播行為。毫無疑問這兩部分内容是本篇文章的核心内容,也是網際網路企業最為關注的内容之一,是以它們十分重要,值得花上大篇幅去讨論。我們從這兩個配置項的大概含義談起。首先是隔離級别,因為網際網路應用時刻面對着高并發的環境,如商品庫存,時刻都是多個線程共享的資料,這樣就會在多線程的環境中扣減商品庫存。對于資料庫而言,就會出現多個事務同時通路同一記錄的情況,這樣引起資料出現不一緻的情況,便是資料庫的 丢失更新(LostUpdate)問題。

應該說,隔離級别是資料庫的概念,有些難度,是以在使用它之前應該先了解資料庫的相關知識,不太了解的小夥伴們可以回頭看看文章開頭對資料庫事務的介紹。

Isolation (隔離性):這是我們讨論的核心内容,正如上述,可能多個應用程式線程同時通路同一資料,這樣資料庫同樣的資料就會在各個不同的事務中被通路,這樣會産生丢失更新。為了壓制丢失更新的産生,資料庫定義了隔離級别的概念,通過它的選擇,可以在不同程度上壓制丢失更新的發生。因為網際網路的應用常常面對高并發的場景,是以隔離性是需要掌握的重點内容。

多個事務同時操作資料的情況下,會引發丢失更新的場景,例如,電商有一種商品,在瘋狂搶購中,會出現多個事務同時通路商品庫存的場景,這樣就會産生丢失更新。一般而言,存在兩種類型的丢失更新,讓我們了解下它們。下面假設一種商品的庫存數量還有 100,每次搶購都隻能搶購 1 件商品,那麼在搶購中就可能出現如【表6-1】所示的場景。

聊聊資料庫的事務

可以看到,T5 時刻事務 1 復原,導緻原本庫存為99 的變為了 100,顯然事務 2 的結果就丢失了,這就是一個錯誤的值。類似地,對于這樣一個事務復原另外一個事務送出而引發的資料不一緻的情況,我們稱為 第一類丢失更新。然而它卻沒有讨論的價值,因為目前大部分資料庫已經克服了第一類丢失更新的問題,也就是現今資料庫系統已經不會再出現【表6-1】的情況了。是以對于這樣的場景不再深入讨論,而是讨論第二類丢失更新,也就是多個事務都送出的場景。

如果是多個事務并發送出,會出現怎麼樣的不一緻的場景呢?例如可能發生如【表6-2】所示的場景。

聊聊資料庫的事務

注意T5 時刻送出的事務。因為在事務 1 中,無法感覺事務 2 的操作,這樣它就不知道事務 2 已經修改過了資料,是以它依舊認為隻是發生了一筆業務,是以庫存變為了 99,而這個結果又是一個錯誤的結果。這樣,T5 時刻事務 1 送出的事務,就會引發事務 2 送出結果的丢失,我們把這樣的多個事務都送出引發的丢失更新稱為 第二類丢失更新。這是我們網際網路系統需要關注的重點内容。為了克服這些問題,資料庫提出了事務之間的隔離級别的概念,這就是本篇文章的重點内容之一。

詳解隔離級别:

上面我們讨論了第二類丢失更新。為了壓制丢失更新,資料庫标準提出了 4 類隔離級别,在不同的程度上壓制丢失更新,這 4 類隔離級别是未送出讀、讀寫送出、可重複讀和串行化,它們會在不同的程度上壓制丢失更新的情景。

也許你會有一個疑問,都全部消除丢失更新不就好了嗎,為什麼隻是在不同的程度上壓制丢失更新呢?其實這個問題是從兩個角度去看的,一個是 資料的一緻性,另一個是 性能。資料庫現有的技術完全可以避免丢失更新,但是這樣做的代價,就是付出鎖的代價,在網際網路中,系統不單單要考慮資料的一緻性,還要考慮系統的性能。試想,在網際網路中使用過多的鎖,一旦出現商品搶購這樣的場景,必然會導緻大量的線程被挂起和恢複,因為使用了鎖之後,一個時刻隻能有一個線程通路資料,這樣整個系統就會十分緩慢,當系統被數千甚至數萬使用者同時通路時,過多的鎖就會引發岩機,大部分使用者線程被挂起,等待持有鎖事務的完成,這樣使用者體驗就會十分糟糕。因為使用者等待的時間會十分漫長,一般而言,網際網路系統響應超過 5 秒,就會讓使用者覺得很不友好,進而引發使用者忠誠度下降的問題。是以選擇隔離級别的時候,既需要考慮資料的一緻性避免髒資料,又要考慮系統性能的問題。是以資料庫的規範就提出了4種隔離級别來在不同的程度上壓制丢失更新。下面我們通過商品搶購的場景來講述這4種隔離級别的差別。

1、未送出讀

未送出讀(readuncommitted)是 最低 的隔離級别,其含義是允許一個事務讀取另外一個事務沒有送出的資料。未送出讀是一種 危險 的隔離級别,是以一般在我們實際的開發中應用不廣,但是它的優點在于并發能力高,适合那些對資料一緻性沒有要求而追求高并發的場景,它的最大壞處是出現 髒讀。讓我們看看可能發生的髒讀場景,如【表6-3】所示。

聊聊資料庫的事務

【表6-3】中的T3 時刻,因為采用未送出讀,是以事務 2 可以讀取事務 1 未送出的庫存資料為 1,這裡當它扣減庫存後則資料為 0,然後它送出了事務,庫存就變為了 0,而事務 1 在T5 時刻復原事務,因為第一類丢失更新已經被克服,是以它不會将庫存復原到 2,那麼最後的結果就變為了 0,這樣就出現了錯誤。髒讀一般是比較危險的隔離級别,在我們實際應用中采用得不多。為了克服髒讀的問題,資料庫隔離級别還提供了讀寫送出(readcommited)的級别,下面我們就讨論它。

2、讀寫送出

讀寫送出(readcommitted)隔離級别,是指一個事務隻能讀取另外一個事務已經送出的資料,不能讀取未送出的資料。例如,【表6-3】的場景在限制為讀寫送出後,就變為【表6-4】描述的場景了。

聊聊資料庫的事務

在T3 時刻,由于采用了讀寫送出的隔離級别,是以事務2 不能讀取到事務 1 中未送出的庫存 1,是以扣減庫存的結果依舊為 1,然後它送出事務,則庫存在T4 時刻就變為了 1。T5時刻,事務1 復原,因為第一類丢失更新己經克服,是以最後結果庫存為 1,這是一個正确的結果。但是讀寫送出也會産生下面的問題,如【表6-5】所描述的場景。

聊聊資料庫的事務

在T3 時刻事務 2 讀取庫存的時候,因為事務 1 未送出事務,是以讀出的庫存為 1,于是事務 2 認為目前可扣減庫存;在T4 時刻,事務 1 己經送出事務,是以在T5 時刻,它扣減庫存的時候就發現庫存為 0,于是就無法扣減庫存了。這裡的問題在于事務 2 之前認為可以扣減,而到扣減那一步卻發現已經不可以扣減,于是庫存對于事務 2 而言是一個可變化的值,這樣的現象我們稱為 不可重複讀,這就是讀寫送出的一個不足。為了克服這個不足,資料庫的隔離級别還提出了可重複讀的隔離級别,它能夠消除不可重讀的問題。

3、可重複讀

可重複讀的目标是克服讀寫送出中出現的不可重複讀的現象,因為在讀寫送出的時候,可能出現一些值的變化,影響目前事務的執行,如上述的庫存是個變化的值,這個時候資料庫提出了可重複讀的隔離級别。這樣就能夠克服不可重複讀的現象如【表6-6】所示。

聊聊資料庫的事務

可以看到,事務 2 在T3 時刻嘗試讀取庫存,但是此時這個庫存已經被事務 1 事先讀取,是以這個時候資料庫就阻塞它的讀取,直至事務 1 送出,事務 2 才能讀取庫存的值。此時已經是T5 時刻,而讀取到的值為 0,這時就已經無法扣減了,顯然在讀寫送出中出現的不可重複讀的場景被消除了。但是這樣也會引發新的問題的出現,這就是 幻讀。假設現在商品交易正在進行中,而背景有人也在進行查詢分析和列印的業務,讓我們看看如【表6-7】所示可能發生的場景。

聊聊資料庫的事務

4、串行化

串行化(Serializable)是資料庫 最高 的隔離級别,它會要求所有的SQL都會按照順序執行,這樣就可以克服上述隔離級别出現的各種問題,是以它能夠完全保證資料的一緻性。

使用合理的隔離級别

通過上面的講述,讀者應該對隔離級别有了更多的認識,使用它能夠在不同程度上壓制丢失更新,于是可以總結成如【表6-8】所示的一張表。

聊聊資料庫的事務

作為網際網路開發人員,在開發高并發業務時需要時刻記住隔離級别可能發生的各種概念和相關的現象,這是資料庫事務的核心内容之一,也是網際網路企業關注的重要内容之一。追求更高的隔離級别,它能更好地保證了資料的一緻性,但是也要付出鎖的代價。有了鎖,就意味着性能的丢失,而且 隔離級别越高,性能就越是直線地下降。是以我們在選擇隔離級别時,要考慮的不單單是資料一緻性的問題,還要考慮系統性能的問題。例如,一個高并發搶購的場景,如果采用串行化隔離級别,能夠有效避免資料的不一緻性,但是這樣會使得并發的各個線程挂起,因為隻有一個線程可以操作資料,這樣就會出現大量的線程挂起和恢複,導緻系統緩慢。而後續的使用者要得到系統響應就需要等待很長的時間,最終因為響應緩慢,而影響他們的忠誠度。是以在現實中一般而言,選擇隔離級别會以讀寫送出為主,它能夠防止髒讀,而不能避免不可重複讀和幻讀。為了克服資料不一緻和性能問題,程式開發者還設計了樂觀鎖,甚至不再使用資料庫而使用其他的手段。例如,使用 Redis 作為資料載體,這些内容我們會在後續章節談及。對于隔離級别,不同的資料庫的支援也是不一樣的。例如,Oracle 隻能支援讀寫送出和串行化,而 MySQ L則能夠支援4種,對于 Oracle 預設的隔離級别為 讀寫送出,MySQL則是 可重複讀,這些需要根據具體資料庫來決定。隻要掌握了隔離級别的含義,使用隔離級别就很簡單,隻需要在

@Transactional

配置對應即可,如下代碼所示。

@Transactional(isolation = Isolation.SERIALIZABLE)
public int insertUser(User user) {
    return userDao.insertUser(user);
}
           

上面的代碼中我們使用了序列化的隔離級别來保證資料的一緻性,這使它将阻塞其他的事務進行并發,是以它隻能運用在那些低井發而又需要保證資料一緻性的場景下。對于高并發下又要保證資料一緻性的場景,則需要另行處理了。

當然,有時候一個個地指定隔離級别會很不友善,是以 SpringBoot 可以通過配置檔案指定預設的隔離級别。例如,當我們需要把隔離級别設定為讀寫送出時,可以在

application.properties

檔案加入預設的配置,如下代碼所示。

#隔離級别數字配置的含義:
#-1 資料庫預設的隔離級别
#1 未送出讀
#2 讀寫送出
#4 可重複讀
#8 串行化
#tomcat資料源預設隔離級别
spring.datasource.tomcat.default-transaction-isolation=2
#dbcp2資料庫連接配接池預設隔離級别
#spring.datasource.dbcp2.default-transaction-isolation=2
           

代碼中配置了 tomcat 資料源的預設隔離級别,而注釋的代碼則是配置了 DBCP2 資料源的隔離級别,注釋中己經說明了數字所代表的隔離級别,相信讀者也有了比較清晰的認識,這裡配置為 2,說明将資料庫的隔離級别預設為讀寫送出。

傳播行為

傳播行為是 方法之間調用事務采取的政策問題。在絕大部分的情況下,我們會認為資料庫事務要麼全部成功,要麼全部失敗。但現實中也許會有特殊的情況。例如,執行一個批量程式,它會處理很多的交易,絕大部分交易是可以順利完成的,但是也有極少數的交易因為特殊原因不能完成而發生異常,這時我們不應該因為極少數的交易不能完成而復原批量任務調用的其他交易,使得那些本能完成的交易也變為不能完成了。此時,我們真實的需求是,在一個批量任務執行的過程中,調用多個交易時,如果有一些交易發生異常,隻是復原那些出現異常的交易,而不是整個批量任務,這樣就能夠使得那些沒有問題的交易可以順利完成,而有問題的交易則不做任何事情,如【圖6-5】所示。

聊聊資料庫的事務

在 Spring 中,當一個方法調用另外一個方法時,可以讓事務采取不同的政策工作,如建立事務或者挂起目前事務等,這便是事務的傳播行為。這樣講還是有點抽象,我們再回到【圖6-5】中。圖中,批量任務 我們稱之為 目前方法,那麼 批量事務 就稱為 目前事務,當它調用單個交易時,稱單個交易為 子方法,目前方法調用子方法的時候,讓每一個子方法不在目前事務中執行,而是建立一個新的事務去執行子方法,我們就說目前方法調用子方法的傳播行為為 建立事務。此外,還可能讓子方法在 無事務、獨立事務 中執行,這些完全取決于你的業務需求。

傳播行為的定義

在 Spring 事務機制中對資料庫存在 7 種傳播行為,它是通過枚舉類

Propagation

定義的。下面先來研究它的源碼,如下代碼所示。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.transaction.annotation;

public enum Propagation {
	/**
	 * (常用)
	 * 需要事務,它是預設傳播行為,如果目前存在事務,就沿用目前事務,
	 * 否則建立一個事務運作子方法
	 */
    REQUIRED(0),

	/**
	 * 支援事務,如果目前存在事務,就沿用目前事務,
	 * 如果不存在,則繼續采用無事務的方式運作子方法
 	 */
    SUPPORTS(1),

	/**
	 * 必須使用事務,如果目前沒有事務,則會抛出異常,
	 * 如果存在目前事務,就沿用目前事務
	 */
    MANDATORY(2),

	/**
	 * (常用)
	 * 無論目前事務是否存在,都會建立新事務運作方法,
	 * 這樣新事務就可以擁有新的鎖和隔離級别等特性,與目前事務互相獨立
	 */
    REQUIRES_NEW(3),

	/**
	 * 不支援事務,目前存在事務時,将挂起事務,運作方法
	 */
    NOT_SUPPORTED(4),
	
	/**
	 * 不支援事務,如果目前方法存在事務,則抛出異常,否則繼續使用無事務機制運作
	 */
    NEVER(5),

	/**
	 * (常用)
	 * 在目前方法調用子方法時,如果子方法發生異常,
	 * 隻因滾子方法執行過的SQL,而不復原目前方法的事務
	 */
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}
           

以上代碼中加入中文注釋解釋了每一種傳播行為的含義。傳播行為一共分為 7 種,但是常用的隻有上面代碼标注的 3 種,其他的使用率比較低。基于實用的原則,本篇文章隻讨論這 3 種傳播行為。下面的小節将對這 3 種傳播行為進行測試。

測試傳播行為

本節中我們繼續沿用上個章節的代碼來測試 RQUIRED、REQUIRES_NEW 和 NESTED 這 3 種最常用的傳播行為。這裡讓我們建立服務接口

UserBatchService

和它的實作類

UserBatchServiceimpl

。它是一個批量應用,用來批量新增使用者。代碼如下所示。

package com.springboot.chapter6.service;

import com.springboot.chapter6.pojo.User;

import java.util.List;

public interface UserBatchService {
	// 批量新增使用者
    int insertUsers(List<User> userList);
}
           
package com.springboot.chapter6.service.impl;

import com.springboot.chapter6.pojo.User;
import com.springboot.chapter6.service.UserBatchService;
import com.springboot.chapter6.service.UserService;
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;

import java.util.List;

@Service
public class UserBatchServiceImpl implements UserBatchService {

    @Autowired
    private UserService userService;

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public int insertUsers(List<User> userList) {
        int count = 0;
        for (User user : userList) {
            // 調用子方法,将使用@Transactional定義的傳播行為
            count += userService.insertUser(user); // 标記
        }
        return count;
    }
}
           

注意标記的代碼。這裡它将調用上節代碼的

insertUser

方法,隻是

insertUser

方法中沒有定義傳播行為。按照我們之前的論述,它會采用 RQUIRED,也就是沿用目前的事務,是以它将與

insertUsers

方法使用同一個事務。下面我們在上節的使用者控制器(UserController)的基礎上新增一個方法測試它,代碼如下所示。

@Autowired
private UserBatchService userBatchService;

@RequestMapping("/insertUsers")
@ResponseBody
public Map<String,Object> insertUsers(String userName1,String note1,
                                      String userName2,String note2){
    User user1 = new User();
    user1.setUserName(userName1);
    user1.setNote(note1);

    User user2 = new User();
    user2.setUserName(userName2);
    user2.setNote(note2);

    List<User> userList = new ArrayList<>();
    userList.add(user1);
    userList.add(user2);
    // 結果會回填主鍵,傳回插入條數
    int inserts = userBatchService.insertUsers(userList);
    Map<String,Object> result = new HashMap<>();
    result.put("success",inserts > 0);
    result.put("user",userList);
    return result;
}
           

這樣我們就可以通過請求這個方法來測試使用者批量插入了。在浏覽器位址欄中輸入請求 http://localhost:8080/user/insertUsers?userName1=usemame_1&note1=note_1&userName2=usemame_2&note2=note_2,就可以觀察背景日志。我的日志如下:

聊聊資料庫的事務

通過加粗的日志部分,我們可以看到都是在沿用已經存在的目前事務。接着我們把上面代碼中調用的

insertUser

方法的注解給修改一下,代碼如下所示。

@Override
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int insertUser(User user) {
    return userDao.insertUser(user);
}
           

再進行測試,可以得到如下日志:

聊聊資料庫的事務

在日志中,為了更好地讓讀者了解,加粗的文字是我加進去的。從日志中可以看到,它啟用了新的資料庫事務去運作每一個

insertUser

方法,并且獨立送出,這樣就完全脫離了原有事務的管控,每一個事務都可以擁有自己獨立的隔離級别和鎖。

最後,我們再測試 NESTED 隔離級别。它是一個如果子方法復原而目前事務不復原的方法,于是我們再把上面的代碼修改為如下代碼,進行測試。

@Override
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.NESTED)
public int insertUser(User user) {
    return userDao.insertUser(user);
}
           

再次運作程式,可以看到如下日志:

聊聊資料庫的事務

在大部分的資料庫中,一段SQL語句中可以設定一個标志位,然後後面的代碼執行時如果有異常,隻是復原到這個标志位的資料狀态,而不會讓這個标志位之前的代碼也復原。這個标志位,在資料庫的概念中被稱為 儲存點(savepoint) 。從加粗日志部分可以看到,Spring為我們生成了 nested 事務,而從其日志資訊中可以看到儲存點的釋放,可見Spring也是使用儲存點技術來完成讓子事務復原而不緻使目前事務復原的工作。注意,并不是所有的資料庫都支援儲存點技術,是以 Spring 内部有這樣的規則:當資料庫支援儲存點技術時,就啟用儲存點技術;如果不能支援,就建立一個事務去運作你的代碼,即等價于 REQUIRES_NEW 傳播行為。NESTED 傳播行為和 RQUIRES_NEW 還是有差別的。NESTED 傳播行為會沿用目前事務的隔離級别和鎖等特性,而 RQUIRES_NEW 則可以擁有自己獨立的隔離級别和鎖等特性,這是在應用中需要注意的地方。

@Transactional 自調用失效問題

@Transactional

在某些場景下會失效,這是要注意的問題。在上節中,我們測試傳播行為,是使用了一個 UserBactchServicelmpl 類去調用 UserServiceImpl 類的方法,那麼如果我們不建立UserBactchServicelmpl 類,而隻是使用 UserServicelmpl 類進行批量插入使用者會怎麼樣呢?下面我們改造 UserServicelmpl,代碼如下所示。

package com.springboot.chapter6.service.impl;

import com.springboot.chapter6.dao.UserDao;
import com.springboot.chapter6.pojo.User;
import com.springboot.chapter6.service.UserService;
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;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;


    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public int insertUsers(List<User> userList) {
        int count = 0;
        for (User user : userList) {
            // 調用自己類自身的方法,産生自調用方法
            count += insertUser(user);
        }
        return count;
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    public int insertUser(User user) {
        return userDao.insertUser(user);
    }
}
           

代碼中新增了方法 insertUsers,對應的接口也需要改造,這步比較簡單就不再示範了。對于 insertUser方法,我們把傳播行為修改為 REQUIRES_NEW,也就是每次調用産生新的事務,而 insertUsers 方法就調用了這個方法。這是一個類自身方法之間的調用,我們稱之為自調用。那麼它能夠成功地每次調用都産生新的事務嗎?

UserController 隻需要将

userBatchService.insertUsers(userList);

改成

userService.insertUsers(userList);

即可,浏覽器輸入http://localhost:8080/user/insertUsers?userName1=usemame_1&note1=note_1&userName2=usemame_2&note2=note_2 進行測試,下面是我的測試日志:

聊聊資料庫的事務

過日志可以看到,Spring 在運作中并沒有建立任何新的事務獨立地運作 insertUser 方法。換句話說,我們的注解

@Transactional

失效了,為什麼會這樣呢?在上節,我們談過 Spring 資料庫事務的約定,其實作原理是 AOP,而 AOP 的原理是動态代理,在自調用的過程中,是類自身的調用,而不是代理對象去調用,那麼就不會産生 AOP,這樣 Spring 就不能把你的代碼織入到約定的流程中,于是就産生了現在看到的失敗場景。為了克服這個問題,我們可以像上節那樣,用一個 Service 去調用另一個 Service,這樣就是代理對象的調用,Spring 才會将你的代碼織入事務流程。當然也可以從 SpringIoC 容器中擷取代理對象去啟用 AOP,例如,我們再次對 UserServicelmpl 進行改造,代碼如下所示。

package com.springboot.chapter6.service.impl;

import com.springboot.chapter6.dao.UserDao;
import com.springboot.chapter6.pojo.User;
import com.springboot.chapter6.service.UserService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {

    @Autowired
    private UserDao userDao;

    private ApplicationContext applicationContext;

    // 實作生命周期方法,設定IOC容器
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public int insertUsers(List<User> userList) {
        int count = 0;
        // 從IOC容器中取出代理對象
        UserService userService = applicationContext.getBean(UserService.class);
        for (User user : userList) {
            // 使用代理對象調用方法插入使用者,此時會織入Spring資料庫事務流程中
            count += userService.insertUser(user);
        }
        return count;
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    public int insertUser(User user) {
        return userDao.insertUser(user);
    }
}
           

從代碼中我們實作了 ApplicationContextAware 接口的

setApplicationContext

方法,這樣便能夠把 IoC 容器設定到這個類中來。于是在 insertUsers 方法中,我們通過 IoC 容器擷取了 UserService 的接口對象。但是請注意,這将是一個代理對象,并且使用它調用了傳播行為為 REQUIRES_NEW 的 insertUser方法,這樣才可以運作成功。我還監控了擷取的 UserService 對象,如下圖所示。

聊聊資料庫的事務

從代碼中我們可以看到,從 IoC 容器取出的對象是一個代理對象,通過它能夠克服自調用的問題。下面是運作這段代碼的日志:

聊聊資料庫的事務

從标記的日志部分可以看出,Spring 已經為我們的方法建立了新的事務,這樣自調用的問題就克服了,隻是這樣代碼需要依賴于 Spring 的 API,這樣會造成代碼的侵入。使用上節中的一個類調用另外一個類的方法則不會有依賴,隻是相對麻煩一些。