Spring數據庫事務處理

目錄

1.Spring聲明式事務

1.2 聲明式事務約定

1.3 @Transactional源碼分析

1.4 Spring事務管理器

2.隔離級別

2.1 數據庫事務特性

2.1.1 第一類丟失更新

2.1.2 第二類丟失更新

2.2 詳解隔離級別

2.2.1 未提交讀

2.2.2 讀寫提交

2.2.3 可重複讀

2.2.4 串行化

2.2.5 使用合理的隔離級別

3. 傳播行爲

3.1 傳播行爲的定義

4.@Transactional自調用失效問題


對於一些業務網站而言,產品庫存的扣減、交易記錄以及賬戶都必須是要麼同時成功,要麼同時失敗,這就是一種事務機制,Spring對這樣的機制給予了支持。而在一些特殊的場景下,如一個批處理,它將處理多個交易,但是在一些交易中發生了異常,這個時候則不能將所有的交易都回滾。如果所有的交易都回滾了,那麼那些本能夠正常處理的業務也無端地被回滾了,這顯然不是我們所期待的結果。通過Spring的數據庫事務傳播行爲,可以很方便地處理這樣的場景。

Spring中的數據庫事務可以使用編程式事務,也可以使用聲明式事務。大部分情況下,會使用聲明式事務,編程式事務基本不再使用了。

1.Spring聲明式事務

執行SQL事務的流程如下圖所示:

在上圖中,有業務邏輯的部分只是執行SQL那一步驟,其他步驟都是比較固定的,按照AOP的設計思想,就可以把除執行SQL這步以外的步驟抽取出來單獨實現,這就是Spring數據庫事務編程的思想。

1.2 聲明式事務約定

對於聲明式事務,使用@Transactional註解進行標註,該註解可以標註在類或者方法上,當它標註在類上時,代表這個類所有公共(public)非靜態的方法都將啓用事務。在@Transaction中,還允許配置許多的屬性,如事務的隔離級別和傳播行爲;又如異常類型,從而確定方法發生什麼異常下回滾事務或者發生什麼異常下不回滾事務等。

有了@Transaction的配置,Spring就會知道在哪裏啓動事務機制,其約定流程如下圖所示:

上圖中,除了執行方法邏輯由開發者提供以外,其他的邏輯都由Spring數據庫事務攔截器根據@Transactional配置的內容來實現。下面讓我們來討論一下@Transactional的配置項。

1.3 @Transactional源碼分析

源碼如下:

ackage 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.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    //指定事務管理器bean的名稱
    @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 {};
}

通過上面的註釋,我們能夠非常清晰的瞭解到每個配置項的含義。關於註解@Transaction值得注意的是它可以放在接口上,也可以放在實現類上,但是Spring團隊建議放在實現類上,因爲放在接口上將將使得你的類基於接口的代理時它才能生效。如果使用接口,那麼你將不能使用CGLIB動態代理,而只能使用JDK動態代理,這將大大地限制你的應用,因此在實現類上使用@Transactional註解纔是最佳的方式。由於註解@Transactional使用到了事務管理器,那麼下面我們首先討論事務管理器。

1.4 Spring事務管理器

在Spring中,事務管理器的頂層接口爲PlatformTransactionManager,Spring基於它定義了一系列的接口和類,類圖如下:

這裏面最常用的事務管理器是DataSourceTransactionManager,從圖中可以看到它是一個實現了接口PlatformTransactionManager的類,該接口類的實現代碼如下:

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
    //獲取事務,它還會設置數據屬性
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //提交事務
    void commit(TransactionStatus var1) throws TransactionException;
    //回滾事務
    void rollback(TransactionStatus var1) throws TransactionException;
}

Spring在事務管理時,就是將這些方法按照約定織入對應的流程中的,其中getTransaction方法的參數是一個事務定義器,它是依賴於我們配置的@Transactional的配置項生成的,通過它就能夠設置事務的屬性了。而提交和回滾可以通過commit和rollback方法來執行。

在Spring Boot中,當你依賴於mybatis-spring-boot-starter之後,它會自動創建一個DataSourceTransactionManager對象,作爲事務管理器;如果依賴於spring-boot-starter-data-jpa,則它會自動創建JtaTransactionManager對象作爲事務管理器,所以我們一般不需要自己創建事務管理器而直接使用它們即可。

2.隔離級別

上面我們只是簡單的使用了事務,下面我們將討論Spring事務機制中最重要的兩個配置項:隔離級別和傳播行爲。

2.1 數據庫事務特性

數據庫的事務具有以下4個基本特徵,也就是著名的ACID特性:

  • Atomic(原子性):事務中包含的操作被看作一個整體的業務單元,這個業務單元中的操作,要麼全部成功,要麼全部失敗,不會出現部分成功和部分失敗的場景。
  • Consistency(一致性):事務在完成時,必須使所有的數據都保持一致的狀態,在數據庫中所有的修改都是基於事務,保證了數據的完整性。
  • Isolation(隔離性):這是我們討論的核心內容,我們的同一數據可能同時被多個線程訪問,這樣數據庫同樣的數據就會在各個不同的事務中被訪問,這樣就會產生丟失更新。爲了避免丟失更新的產生,數據庫定義了隔離級別的概念,通過它的選擇,可以在不同程度上避免丟失更新的發生。
  • Durability(持久性):事務結束後,所有的數據會固話到一個地方,如保存在磁盤上,即使斷電重啓後也可以提供給應用程序訪問。

這四個特性,除了隔離性都比較好理解,爲了便於理解,這裏我們深入地討論在多個事務同時操作數據的情況下,會引發丟失更新的場景。例如,有一種秒殺商品,庫存100件,當秒殺時存在多個事務同時訪問庫存的場景。

2.1.1 第一類丟失更新

可以看到,在T5時刻事務1回滾了,導致原本庫存爲99的變爲了100,顯然事務2的結果就丟失了。對於這樣,一個事務回滾另外一個事務已提交的值而引發的數據不一致的情況,我們稱爲是第一類丟失更新。然而,它並沒有多大的討論價值,因爲目前大部分數據庫已經克服了第一類丟失更新的問題,也就是當今的數據庫系統已經不會再出現表中的情況了。

2.1.2 第二類丟失更新

因爲在事務1中,無法感知事務2的操作,這樣它就不知道事務2已經修改過了數據,因此它依舊認爲只發生了一筆交易,所以庫存變爲了99,而這個結果又是一個錯誤的結果。這樣,T5時刻事務1提交的事務,就會引發事務2提交結果的丟失,我們把這樣的多個事務都提交引發的丟失更新稱爲第二類丟失更新。

爲了克服這個問題,數據庫提出了事務之間的隔離級別。

2.2 詳解隔離級別

爲了在不同程度上克服丟失更新,數據庫標準提出了4類隔離級別,這四類隔離級別分別是未提交讀、讀寫提交、可重複讀和串行化。

也許這裏有個疑問,爲什麼只是在不同程度上去壓制丟失更新,而不能完全避免呢?其實,數據庫現有的技術完全可以避免丟失更新,但是這樣的代價就是付出鎖的代價,降低數據庫的性能。因此我們必須在性能和數據一致性中找到平衡。

2.2.1 未提交讀

未提交讀是最低的隔離級別,其含義就是允許一個事務讀取另外一個事務沒有提交的數據。未提交讀是一種危險的隔離級別,所有一般不會使用,但它的優點是併發能力強,適合那些對數據一致性沒有要求而追求高併發的場景,它最大的缺點就是出現髒讀。出現髒讀的實例如下:

出現髒讀
出現髒讀

在T3時刻,因爲採用了未提交讀,所以事務2可以讀取事務1未提交的庫存數據爲1,這裏當它扣減庫存後數據爲0,然後它提交了事務,庫存就變爲了0,然後它提交了事務,庫存就變爲了0,而事務1在T5時刻回滾事務,因爲第一類丟失更新已經克服,所以它不會回滾到2,那麼結果就變爲了0,這樣就出現了錯誤。

爲了克服髒讀的問題,數據庫隔離級別定義了讀寫提交的級別。

2.2.2 讀寫提交

讀寫提交隔離級別,是指一個事務只能讀取另外一個事務已經提交的數據,不能讀取未提交的數據。上面的場景在限制爲讀寫提交以後,就變爲以下的場景了。

克服髒讀

採取了讀寫提交的隔離級別以後,就克服了髒讀現象,但是讀寫提交也會產生下面的不可重複讀的問題:

不可重讀場景

這裏的問題在於事務2之前認爲可以扣減,而到了扣減的時候發現已經不可以扣減了,於是庫存對於事務2而言是一個可變化的值,這樣的現象我們稱爲不可重複讀,這就是讀寫提交的不足。爲了克服這個不足,數據庫的隔離級別提出了可重複讀的隔離級別,它能夠消除不可重複讀的問題。

2.2.3 可重複讀

可重複讀的目標是克服讀寫提交中出現的不可重複讀的現象,因爲在讀寫提交的時候可能會出現一些值得變化,影響當前事務的執行。克服不可重複讀的現象如下所示:

克服不可重讀

事務2在T3時刻嘗試讀取庫存,但是此時這個庫存已經被事務1事先讀取,所以這個時候數據庫就阻塞它的讀取,直至事務1提交,事務2才能讀取庫存的值。此時已經是T5時刻,而讀取到的值是0,這時就無法扣減庫存了,顯然在讀寫提交中出現的不可重複讀的場景被消除了。但是這樣也會引發新的問題出現,這就是幻讀。假設現在正在進行商品交易,而後臺有人也在進行查詢分析和打印的業務。如下場景:

這裏有一點需要注意的是,這裏的筆數不是數據庫存儲的值,而是一個統計值,商品庫存則是數據庫存儲的值。也就是說幻讀不是針對一條數據庫記錄而言的,而是多條記錄,例如這51筆交易就是多條數據庫記錄統計出來的。而可重複讀是針對數據庫的單一條記錄,例如商品的庫存是以數據庫裏面的一條記錄存儲的,它可以產生可重複讀,而不能產生幻讀。

2.2.4 串行化

串行化是數據庫最高的隔離級別,它會要求所有的SQL都會按照順序執行,這樣就可以克服上述隔離級別出現的全部問題,所以它能夠保證數據的一致性。

2.2.5 使用合理的隔離級別

下面我們總結一些數據庫的隔離級別和可能發生的現象:

隔離級別和可能發生的現象

在實際開發中,追求更高的隔離級別,它能很好地保證數據的一致性,但是也要付出鎖的代價。有了鎖,就意味着性能的丟失,而且隔離級別越高,性能就越是直線的下降。

對於隔離級別,不同的數據庫的支持也是不一樣的,例如,Oracle只能支持讀寫提交和串行化,而MySQL則能夠支持4種,對於Oracle默認的隔離級別爲讀寫提交,MySQL則是可重複讀。

在Spring中使用隔離級別非常簡單,只需要在@Transactional配置即可:

@Transactional(isolation = Isolation.SERIALIZABLE)

上面的代碼指定使用序列化的隔離級別來保證數據一致性,這使它將阻塞其他事務進行併發,所以它只能運用在那些低併發而又不需要保證數據一致性的場景下。

Spring Boot也可以通過配置文件指定默認的隔離級別,配置如下:

#指定Tomcat數據源默認隔離級別
spring.datasource.tomcat.default-transaction-isolation=2

隔離級別數字配置的含義:

  • -1:使用數據庫默認隔離級別
  • 1:未提交讀
  • 2:讀寫提交
  • 4:可重複讀
  • 8:串行化

3. 傳播行爲

傳播行爲是方法之間調用事務採取的策略問題,在絕大部分情況下,我們會認爲數據庫事務要麼全部成功,要麼全部失敗。但現實中也許存在特殊的情況,比如執行一個批量任務,它會處理很多筆交易,絕大部分交易是可以順利完成的,只有少部分可能發生異常,這時我們不應該因爲極少數的交易不成功而回滾批量任務調用的整個事務,使得那些本能完成的交易也變爲不能完成了。此時,我們真實的需求是,在一個批量任務執行的過程中,調用多個交易時,如果有一些交易發生異常,只是回滾那些出現異常的交易,而不是整個批量任務。示意圖如下所示:

事務的傳播行爲

在Spring中,當一個方法調用另外一個方法時,可以讓事務採取不同的策略工作,如新建事務或者掛起當前事務等,這就是事務的傳播行爲。

3.1 傳播行爲的定義

在Spring事務機制中對數據庫存在7種傳播行爲,它是通過枚舉類Propagation定義的。源代碼如下:

package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;

public enum Propagation {

	/**
	 * 如果當前存在事務,就沿用當前事務,否則新建一個事務運行子方法
     * 它是默認的傳播行爲
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

	/**
	 * 如果當前存在事務,就沿用當前事務,
     * 如果不存在就繼續採用無事務的方式運行子方法
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

	/**
	 * 必須使用事務,如果當前沒有事務就會拋出異常
     * 如果存在事務,就沿用當前事務
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

	/**
	 * 無論當前事務是否存在,都會創建新事務運行方法
     * 這樣新事務就會擁有新的鎖和隔離級別等特性,與當前事務相互獨立
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

	/**
	 * 不支持事務,如果當前存在事務,則掛起事務,運行方法
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

	/**
	 * 不支持事務,如果當前方法存在事務,則拋出異常,否則繼續使用無事務機制運行
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),

	/**
	 * 在當前方法調用子方法時,如果子方法發生異常
     * 只回滾子方法執行過的SQL,而不回滾當前方法的事務
	 */
	NESTED(TransactionDefinition.PROPAGATION_NESTED);


	private final int value;


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

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

}

傳播行爲一共分爲7種,但是常用的只有REQUIRED、REQUIRES_NEW和NESTED,其他的使用率非常低,不需要做過多的理解。REQUIRES_NEW會完全脫離原有事務的管控,每一個事務都擁有自己獨立的隔離級別和鎖。NESTED是一個如果子方法回滾而當前事務不回滾的方法。

在大部分的數據庫中,一段SQL語句中可以設置一個標誌位,然後後面的代碼執行時如果有異常,只是回滾到這個標誌位的數據狀態,而不會讓這個標誌位之前的代碼也回滾。這個標誌位,在數據庫的概念中被稱爲保存點。Spring就是使用保存點技術來完成讓子事務回滾而不致使當前事務回滾的工作。需要注意的是,並不是所有的數據庫都支持保存點技術,因此當數據庫支持保存點技術時,就啓用保存點;如果不支持,就新建一個事務去運行你的代碼,即等價於REQUIRES_NEW傳播行爲。

NESTED傳播行爲和REQUIRES_NEW還有一個重要區別是,NESTED傳播行爲會沿用當前事務的隔離級別和鎖等特性,而REQUIRES_NEW則可以擁有自己獨立的隔離級別和鎖等特性。

4.@Transactional自調用失效問題

@Transactional在自調用的場景下會失效,這是一個需要注意的問題。看如下的實例代碼:

package com.martin.config.service.impl;

import com.martin.config.chapter5.dao.UserMapper;
import com.martin.config.chapter5.pojo.User;
import com.martin.config.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;

/**
 * @author: martin
 * @date: 2019/11/9 22:14
 * @description:
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUser(Long id) {
        return userMapper.getUser(id);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public boolean batchAddUser(List<User> users) {
        users.forEach(value -> {
            try {
                //調用自身類自身的方法,產生自調用問題
                insertUser(value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return true;
    }

    //傳播行爲設置爲REQUIRES_NEW,每次調用產生新的事務
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
    public boolean insertUser(User user) {
        return userMapper.insertUser(user) > 0 ? true : false;
    }
}

UserServiceImpl 類中批量插入用戶信息的方法調用了自己的方法insertUser一條一條的去插入,這是一個類自身方法之間的調用,我們稱之爲自調用。執行日誌如下:

2019-11-15 22:18:57.799 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] for JDBC transaction
2019-11-15 22:18:57.800 DEBUG 20924 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] to 2
2019-11-15 22:18:57.817 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] to manual commit
2019-11-15 22:19:13.525  WARN 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=45s729ms237µs900ns).
2019-11-15 22:19:13.526 DEBUG 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
2019-11-15 22:19:20.980 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:19:20.996 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
Fri Nov 15 22:19:20 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:19:21.000 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:21.007 DEBUG 20924 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] will be managed by Spring
2019-11-15 22:19:21.010 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:19:21.033 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user0(String), 0(Integer), note0(String)
2019-11-15 22:19:21.052 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:19:21.057 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:21.057 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:19:50.621 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec] from current transaction
2019-11-15 22:19:50.621 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:19:50.622 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user2(String), 0(Integer), note2(String)
2019-11-15 22:19:50.640 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:19:50.640 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:50.640 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:19:59.668 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:19:59.677 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec] from current transaction
2019-11-15 22:19:59.677 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:19:59.677 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user3(String), 1(Integer), note3(String)
Fri Nov 15 22:19:59 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:19:59.703 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:19:59.704 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:59.704 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:20:41.188 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:20:41.188  WARN 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=50s576ms467µs300ns).
2019-11-15 22:20:41.189 DEBUG 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
2019-11-15 22:20:41.208 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec] from current transaction
2019-11-15 22:20:41.208 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:20:41.209 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user4(String), 0(Integer), note4(String)
2019-11-15 22:20:41.230 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:20:41.230 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:20:41.231 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint

從上述日誌的執行中,我們發現Spring在運行中並沒有創建任何新的事物獨立的運行insertUser,換句話說,我們的註解@Transactional失效了。

Spring數據庫事務的實現原理是AOP,而AOP的原理是動態代理,在自調用的過程中,是類自身的調用,而不是代理對象去調用,那麼就不會產生AOP,這樣就不能把你的代碼織入到約定的流程中,於是就產生了現在看到的失敗的場景。爲了克服這個問題,我們可以使用一個Service去調用另外一個Service,這樣很好理解。也可以從Spring AOP容器中獲取Bean對象去啓用AOP,實例代碼如下:

package com.martin.config.service.impl;

import com.martin.config.chapter5.dao.UserMapper;
import com.martin.config.chapter5.pojo.User;
import com.martin.config.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;

/**
 * @author: martin
 * @date: 2019/11/9 22:14
 * @description:
 */
@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {
    @Autowired
    private UserMapper userMapper;

    private ApplicationContext applicationContext;

    @Override
    public User getUser(Long id) {
        return userMapper.getUser(id);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public boolean batchAddUser(List<User> users) {
        UserService userService = applicationContext.getBean(UserService.class);
        users.forEach(value -> {
            try {
                //調用自身類自身的方法,產生自調用問題
                userService.insertUser(value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return true;
    }

    //傳播行爲設置爲REQUIRES_NEW,每次調用產生新的事務
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
    public boolean insertUser(User user) {
        return userMapper.insertUser(user) > 0 ? true : false;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

執行的日誌如下:

2019-11-15 22:35:10.193 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@504092881 wrapping com.mysql.jdbc.JDBC4Connection@24d8a954] for JDBC transaction
2019-11-15 22:35:10.204 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@504092881 wrapping com.mysql.jdbc.JDBC4Connection@24d8a954] to 2
2019-11-15 22:35:10.228 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@504092881 wrapping com.mysql.jdbc.JDBC4Connection@24d8a954] to manual commit
2019-11-15 22:35:10.257 DEBUG 21444 --- [nio-8080-exec-1] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'userServiceImpl'
2019-11-15 22:35:16.754 DEBUG 21444 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
2019-11-15 22:35:16.763 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.martin.config.service.impl.UserServiceImpl.insertUser]
Fri Nov 15 22:35:16 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:16.949 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@905d07d
2019-11-15 22:35:16.950 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] for JDBC transaction
2019-11-15 22:35:16.950 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 2
Fri Nov 15 22:35:16 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:16.975 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to manual commit
2019-11-15 22:35:16.992 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:35:17.025 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2019-11-15 22:35:17.039 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.065 DEBUG 21444 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] will be managed by Spring
2019-11-15 22:35:17.109 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:35:17.113 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@6d6432b9
Fri Nov 15 22:35:17 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:17.273 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@14190c5a
Fri Nov 15 22:35:17 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:17.349 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user0(String), 0(Integer), note0(String)
2019-11-15 22:35:17.374 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:35:17.387 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.388 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:35:17.390 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.391 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.392 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.393 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-15 22:35:17.393 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d]
2019-11-15 22:35:17.422 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 4
2019-11-15 22:35:17.444 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] after transaction
2019-11-15 22:35:17.445 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@1f680ddb
2019-11-15 22:35:17.445 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-15 22:35:17.450 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-15 22:35:17.451 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.martin.config.service.impl.UserServiceImpl.insertUser]
2019-11-15 22:35:17.452 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] for JDBC transaction
2019-11-15 22:35:17.452 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 2
Fri Nov 15 22:35:17 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:17.473 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to manual commit
2019-11-15 22:35:17.490 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:35:17.502 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2019-11-15 22:35:17.502 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.503 DEBUG 21444 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] will be managed by Spring
2019-11-15 22:35:17.504 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:35:17.507 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user1(String), 1(Integer), note1(String)
2019-11-15 22:35:17.528 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:35:17.529 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.530 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:35:17.531 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.531 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.533 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.534 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-15 22:35:17.535 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d]
2019-11-15 22:35:17.585 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 4
2019-11-15 22:35:17.608 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] after transaction
2019-11-15 22:35:17.609 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-15 22:35:17.609 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-15 22:35:17.609 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.martin.config.service.impl.UserServiceImpl.insertUser]

通過Acquired Connection 日誌,Spring爲我們的方法創建了新的事務,這樣自調用的問題就克服了。只是這樣的代碼需要依賴Spring的API,對代碼造成侵入。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章