Spring 事務傳播行為

roran發表於2020-04-04

簡介

  • Spring在TransactionDefinition介面中規定了7種型別的事務傳播行為。 事務傳播行為是Spring框架獨有的事務增強特性,不屬於事務實際提供方即資料庫的行為。
事務傳播行為型別 說明
REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。
SUPPORTS 支援當前事務,如果當前沒有事務,就以非事務方式執行。
MANDATORY 使用當前的事務,如果當前沒有事務,就丟擲異常。
REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
NEVER 以非事務方式執行,如果當前存在事務,則丟擲異常。
NESTED 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則建立一個事務。

準備

POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>fun.roran</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- lombok start -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- lombok end -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>

複製程式碼

application.yml

server:
  port: 80
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
    username: root
    password: 123456
複製程式碼

sql

CREATE TABLE `tx` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `value` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
複製程式碼

DAO

TxDAO

package fun.roran.demo.dao;

public interface TxDAO {
    void a();
    void b();
}

複製程式碼

TxDAOImpl

package fun.roran.demo.dao.impl;

import fun.roran.demo.dao.TxDAO;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
@Repository
public class TxDAOImpl implements TxDAO {
    @Resource
    JdbcTemplate jdbcTemplate;
    @Override
    public void a() {
        jdbcTemplate.execute("INSERT INTO tx (value) VALUES ('a')");
    }

    @Override
    public void b() {
        jdbcTemplate.execute("INSERT INTO tx (value) VALUES ('b')");

    }
}

複製程式碼

Service

TxServiceA

package fun.roran.demo.service;

public interface TxServiceA {
    void a();
}

複製程式碼

TxServiceAImpl

package fun.roran.demo.service.impl;
import fun.roran.demo.dao.TxDAO;
import fun.roran.demo.service.TxServiceA;
import fun.roran.demo.service.TxServiceB;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class TxServiceAImpl implements TxServiceA {
    @Resource
    private TxDAO txDAO;

    @Resource
    private TxServiceB txServiceB;

    @Override
    public void a() {
        txDAO.a();
        txServiceB.b();
    }
}
複製程式碼

TxServiceB

package fun.roran.demo.service;

public interface TxServiceB {
    void b();
}

複製程式碼

TxServiceBImpl

package fun.roran.demo.service.impl;

import fun.roran.demo.dao.TxDAO;
import fun.roran.demo.service.TxServiceB;

import javax.annotation.Resource;

public class TxServiceBImpl implements TxServiceB {
    @Resource
    private TxDAO txDAO;

    @Override
    public void b() {
        txDAO.b();
    }
}

複製程式碼

REQUIRED

  • 如果當前沒有事務,就新建一個事務。 如果已經存在一個事務中,加入到這個事務中。
  • 預設為該級別。

情況1:a()開啟事務

  • 以上文程式碼為例,在a()開啟事務的情況下,a()b()相當於處於同一個事務。它們要麼全都提交,要麼全都不提交。
  • 只要b()丟擲異常,a()b()會一起回滾。 注意,即使b()的異常被a()捕獲,仍是要一起回滾的。
  • a()呼叫完b()後丟擲異常回滾,b()也會回滾。

Spring 事務傳播行為

情況2:a()不開啟事務

  • a()不開啟事務,b()開啟事務。
  • a()呼叫完b()後丟擲異常,b()自然不會回滾。 同樣若b()丟擲異常回滾,也不會影響a()已經執行過的程式碼。

Spring 事務傳播行為

SUPPORTS

  • 支援當前事務,如果當前沒有事務,就以非事務方式執行。

情況1:a()開啟事務

  • a()開啟事務的情況下,b()會加入a()的事務。二者屬於同一事務。
  • 只要b()丟擲異常,a()b()會一起回滾。 注意,即使b()的異常被a()捕獲,仍是要一起回滾的。
  • a()呼叫b()後丟擲異常,則b()也會回滾。

Spring 事務傳播行為

情況2:a()不開啟事務

  • a()不開啟事務,則a()b()都處在非事務環境下。

Spring 事務傳播行為

MANDATORY

  • 使用當前的事務,如果當前沒有事務,就丟擲異常。

情況1:a()開啟事務

  • a()開啟事務的情況下,b()會加入a()的事務。二者屬於同一事務。
  • 只要b()丟擲異常,a()b()會一起回滾。 注意,即使b()的異常被a()捕獲,仍是要一起回滾的。
  • a()呼叫b()後丟擲異常,則b()也會回滾。

Spring 事務傳播行為

情況2:a()不開啟事務

  • 呼叫b()時會丟擲異常。

異常

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
複製程式碼

REQUIRES_NEW

  • 新建事務,如果當前存在事務,把當前事務掛起。

情況1:a()開啟事務

  • a()開啟事務,執行b()時,會將a()的事務掛起,b()建立一個自己的事務。
  • b()丟擲異常回滾,若a()沒有對異常進行補獲,則也要回滾。 若a()捕獲了異常,則a()不用回滾。
  • a()呼叫完b()後丟擲異常,a()回滾,b()不用回滾(因為b()的事務已經提交了)。

Spring 事務傳播行為

情況2:a()不開啟事務

  • b()會開啟一個自身的事務。 b()若發生異常回滾不會影響到a()已執行操作,a()呼叫完b()後丟擲異常自然不會影響到b()

Spring 事務傳播行為

NOT_SUPPORTED

  • 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

情況1:a()開啟事務

  • a()開啟事務,但在執行b()的過程中,a()的事務被掛起且b()是無事務方式執行的。
  • a()呼叫b()後丟擲異常,則b()不會被回滾(除了其他b()的其它操作可以回滾)。
  • b()執行中丟擲異常,則b()已經執行的操作也不會回滾。 同時若a()沒有捕獲b()丟擲的異常,a()也會被回滾。若a()捕獲了,則不會被回滾。
  • 總而言之,不管a()b()哪個操作丟擲異常且未捕獲,a()一定會被回滾,b()一定不會回滾。

Spring 事務傳播行為

情況2:a()不開啟事務

  • a()b()都處於無事務狀態。

NEVER

  • 以非事務方式執行,如果當前存在事務,則丟擲異常。

情況1:a()開啟事務

  • 呼叫b()丟擲異常。

異常

org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
複製程式碼

情況2:a()不開啟事務

  • a()b()都處於無事務狀態。

NESTED

  • 如果當前存在事務,則在巢狀事務內執行。 如果當前沒有事務,則建立一個事務。

情況1:a()開啟事務

  • 呼叫b()時,開啟一個事務,這個事務是a()的子事務。
  • b()丟擲異常並回滾時。該異常若被a()捕獲,則a()不會回滾,否則a()回滾。
  • a()呼叫b()後丟擲異常,則a()b()一起回滾。

情況2:a()不開啟事務

  • 相當於只有b()開啟了事務。

總結

Spring 事務傳播行為

事務傳播失效

同一個類中事務傳播失效

  • 如果將上文的a()b()方法寫在同一個類中,那麼事務傳播行為將失效。
  • 因為事務傳播行為的實現是通過代理物件實現的。而原來的物件是沒有事務傳播行為功能的。

程式碼示例

package fun.roran.demo.service.impl;
import fun.roran.demo.dao.TxDAO;
import fun.roran.demo.service.TxServiceA;
import fun.roran.demo.service.TxServiceB;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class TxServiceAImpl implements TxServiceA {
    @Resource
    private TxDAO txDAO;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void a() {
        txDAO.a();
        b();
    }

    @Transactional(propagation = Propagation.NEVER, rollbackFor = Exception.class)
    @Override
    public void b() {
        txDAO.b();
    }
}
複製程式碼
  • 呼叫a()方法,發現能夠正常執行,不丟擲異常。
  • 原因:根據TxServiceAImpl物件生產代理物件後,代理物件底層還是呼叫TxServiceAImpl物件的b()方法,而這個方法是不支援事務傳播功能的。

解決方法

  • 不通過this呼叫方法,而是通過注入代理物件類呼叫。
程式碼示例
package fun.roran.demo.service.impl;
import fun.roran.demo.dao.TxDAO;
import fun.roran.demo.service.TxServiceA;
import fun.roran.demo.service.TxServiceB;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class TxServiceAImpl implements TxServiceA {
    @Resource
    private TxDAO txDAO;

    @Resource
    private TxServiceA txServiceA;


    @Transactional(rollbackFor = Exception.class)
    @Override
    public void a() {
        txDAO.a();
        txServiceA.b();
    }

    @Transactional(propagation = Propagation.NEVER, rollbackFor = Exception.class)
    @Override
    public void b() {
        txDAO.b();
    }
}
複製程式碼

public方法

  • @Transactional只能用於 public 的方法上,否則事務失效。 如果要用在非public方法上,可以開啟 AspectJ 代理模式。

相關文章