Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

一、Spring 事务的使用

1.1、编程式事务(了解即可)

Spring 编程式事务的使用主要有 3 个步骤:

  • 开启事务(获取事务):通过 Spring Boot 中内置的 DataSourceTransactionManager 的 getTransaction 方法,并搭配内置的 TransactionDefinition 实例作为方法的参数,来获取事务(此操作同时也会开启事务)。
  • 提交事务:DataSourceTransactionManager 创建出实例后,使用它的 commit 方法(参数是 getTransaction 方法的返回值,也就是 TransactionStatus,它的本质就是一个事务),就可以完成提交事务。
  • 回滚事务:通过 DataSourceTransactionManager 的 rollback 方法(参数就是 事务)进行事务的回滚。

具体的,如果我的业务逻辑是向数据库插入一条数据,如下代码示例:

import com.example.demo.entity.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserService userService;
 
    //事务管理
    @Autowired
    private DataSourceTransactionManager transactionManager;
 
    //事务属性设置(不设置有默认属性)
    @Autowired
    private TransactionDefinition transactionDefinition;
 
    @RequestMapping("/add")
    public int insertUserInfo(UserInfo userInfo) {
        //检验非空
        if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) ||
        !StringUtils.hasLength(userInfo.getPassword())) {
            return 0;
        }
 
        // 1.开启事务
        TransactionStatus transactionStatus =
                transactionManager.getTransaction(transactionDefinition);
 
        // 2.执行业务逻辑
        int result = userService.insertUserInfo(userInfo);
        System.out.println("添加:" + result + "个用户~");
 
//        // 3.提交事务
//        transactionManager.commit(transactionStatus);
 
        // 4.回滚事务(根据情况而定)
        transactionManager.rollback(transactionStatus);
 
        return result;
    }
}

Ps:一个事务若已经提交,则不可以进行回滚,否则会报错:“不可多次提交或回滚事务”。

1.2、注解实现声明式事务

1.2.1、@Transactional 注解的使用

在 Spring 提供了 @Transactional 注解实现事务,特点如下:

  1. 可以添加在类上或方法上。
  2. 添加在方法上(方法必须是 public,否则不生效):在方法执行前自动开启事务,方法执行完(没有任何异常)自动提交事务,但如果方法执行期间出现异常,将会自动回滚事务;添加在类上:对所有的 public 方法生效;

具体的如下代码:

    @Transactional //声明式事务
    @RequestMapping("/add")
    public Integer add(UserInfo userInfo) {
        //非空检验
        if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) ||
                !StringUtils.hasLength(userInfo.getPassword())) {
            return 0;
        }
        int result = userService.insertUserInfo(userInfo);
        System.out.println("添加:" + result + "个用户~");
        return result;
    }

1.2.2、参数说明

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

值得注意的是 isolation 的隔离级别:

读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化 (Serializable)。

1.2.3、声明式事务对异常的处理

@Transactional 在异常被捕获的情况下,不会进行事务的自动回滚,那么如果需求改变,需要在异常被捕获的情况下也进行回滚,该如何实现呢?

方法1:重新抛出异常(有点脱了裤子放屁的感觉),如下代码:

    @Transactional //声明式事务
    @RequestMapping("/add")
    public Integer add(UserInfo userInfo) {
        //插入一条信息到数据库
        int result = userService.insertUserInfo(userInfo);
 
        try {
            int i = 1 / 0;
        } catch(Exception e) {
            System.out.println(e.getMessage());
            throw e;
        }
        System.out.println("添加:" + result + "个用户~");
        return result;
    }

方法2:手动回滚事务,如下代码:

    @Transactional //声明式事务
    @RequestMapping("/add")
    public Integer add(UserInfo userInfo) {
        //插入一条信息到数据库
        int result = userService.insertUserInfo(userInfo);
 
        try {
            int i = 1 / 0;
        } catch(Exception e) {
            System.out.println(e.getMessage());
            //手动回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        System.out.println("添加:" + result + "个用户~");
        return result;
    }

Ps:TransactionAspectSupport表示事务的切面建议、currentTransactionStatus表示获取当前事务、setRollbackOnly表示进行回滚。

1.2.3、@Transational 的工作原理

@Transaction 是基于 AOP 实现的,而 AOP 又是使用动态代理实现的。若目标对象实现了接口,默认使用 JDK 的动态代理,若目标对象没有实现接口,则会使用 CGLIB 动态代理。

@Transational 在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务。若中途遇到异常,则进行回滚事务。

二、Spring 事务的传播机制

2.1、事务传播机制是什么?

Spring 事务传播机制定义了多个包含了事务的方法,相互调用时,事务是如何在这些方法之间进行传递的。

例如这样一种情况:现在有方法一和方法二,方法一有事务,方法二没有事务,方法一会调用到方法二,那么当方法二中出现了异常,方法二需要回滚么?事务的传播机制就是用来解决这种类似的问题~

2.2、事务的传播机制有什么作用

事务的传播机制是保证一个事务在多个调用方法间的可控性。

例如这样一种情况:现在有方法一和方法二,方法一有事务,方法二没有事务,方法一会调用到方法二,那么当方法二中出现了异常,方法二需要回滚么?如果方法二回滚了,那么方法一是否也要跟着回滚?通过事务的传播机制,就会告诉你该如何进行处理~

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

2.3、事务的传播机制中有哪些?

从大的方向上看,主要分为以下三种~

2.3.1、支持当前调用链上的事务

  • Propagation.REQUIRED(需要有):默认的事务隔离级别,表示如果当前调用链存在事务,则加入该事务;如果没有事务,则为当前方法创建一个事务。(方法一的事务上添加此属性,意味如果方法二有事务,就加入当前主事务中,如果方法二没有事务,也会为这个方法创建一个事务,并加入到主事务中)
  • Propagation.SUPPORTS(可以有):如果当前调用链存在事务,则加入该事务,如果当前调用链没有事务,就以非事务方式执行。
  • Propagation.MANDATORY(强制有):如果当前调用链存在事务,则加入该事务;如果当前调用链没有事务,则抛出异常。

2.3.2、不支持当前调用链上的事务

  • Propagation.REQUIRES_NEW:如果当前调用链存在事务,则把当前调用链这个事务挂起,为当前方法创建一个新的事务(与调用链事务互不干扰,若新事务提交或回滚,则开启调用链事务),如果当前调用链没有事务,则为当前方法创建一个事务。
  • Propagation.NOT_SUPPORTED:如果当前调用链存在事务,则把当前调用链的事务挂起,也就是说 Propagation.NOT_SUPPORTED 修饰的方法,以非事务的方式运行。
  • Propagation.NEVER:以非事务的方式运行,并且若当前调用链存在事务,则抛出异常。

2.3.3、嵌套事务

Propagation.NESTED:表示当前调用链存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前调用链没有事务,则等价于 PROPAGATION_REQUIRED(为当前方法创建事务)。

Ps:Propagation.REQUIRES_NEW 和 Propagation.NESTED 区别主要是 NESTED 和主事务是一个事务,而 REQUIRES_NEW 是不同事务。

2.4、代码示例

这里我将用两个示例来说明(这两个如果你能拿捏住,其他的就没问题~),这里的调用链如下图:

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

2.4.1、支持当前事务(REQUIRED)示例

首先开始事务,使用 UserService 插入一条用户数据,然后再执行(故意制造错误)报错,最后观察执行结果,如下代码:

UserController如下:

import com.example.demo.entity.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserService userService;
 
    @Transactional(propagation = Propagation.REQUIRED)
    @RequestMapping("/insert")
    public Integer insertUserInfo(UserInfo userInfo) {
        //插入用户数据
        int result = userService.insertUserInfo(userInfo);
        System.out.println("添加:" + result + "个用户~");
        return result;
    }
 
}

LogService如下:

import com.example.demo.entity.UserInfo;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
@Service
public class UserService {
 
    @Autowired
    UserMapper userMapper;
 
    public int insertUserInfo(UserInfo userInfo) {
        int result = userMapper.insertUserInfo(userInfo);
        int e = 1 / 0;
        return result;
    }
 
}

输入 url,插入用户 “zhangsan” 运行结果如下,如下图:

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)
Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

可以观察到成功插入数据后引发的算数异常,按照我们设置的事务,是会进行回滚的,接下来观察数据库验证我们的结果:

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

结果分析:由于我们设置的事务是REQUIRED,它表示调用链上若存在事务,则加入到事务中,若没有就会为这当前方法创建一个事务,因此在 UserService 方法上即使没有事务,也会加入到当前调用链创建好的事务当中,即使引发了异常,也会进行数据的回滚~

2.4.2、嵌套事务示例 

首先开始事务,使用 UserService 的 insertUserInfo 插入一条用户数据,然后再执行 insert 方法再插入一条用户数据(这里故意引发异常),插入完后报错,最后观察执行结果,如下代码:

UserController如下:

import com.example.demo.entity.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserService userService;
 
    @Transactional(propagation = Propagation.REQUIRED)
    @RequestMapping("/insert")
    public void insertUserInfo(UserInfo userInfo) {
        //插入用户数据
        int result1 = userService.insertUserInfo(userInfo);
        int result2 = userService.insert();
    }
}

UserService如下:

import com.example.demo.entity.UserInfo;
import com.example.demo.mapper.UserMapper;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class UserService {
 
    @Autowired
    UserMapper userMapper;
 
    public int insertUserInfo(UserInfo userInfo) {
        int result = userMapper.insertUserInfo(userInfo);
 
        return result;
    }
 
    @Transactional(propagation = Propagation.NESTED)
    public int insert() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("lisi");
        userInfo.setPassword("123");
        int result = userMapper.insertUserInfo(userInfo);
        //制造异常
        int a = 1 / 0;
        return result;
    }
 
}

运行结果如下:

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)
Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)
Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

数据库结果如下:

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

结果分析:在 UserController 中开启事务,UserService 中调用 insertUserInfo 插入一条数据,接着调用 insert 方法(使用 NESTED 嵌套上一个调用类的事务)插入一条数据,插入完后引发异常,于是进行回滚当前事务,因此第二条数据添加失败,又因为是嵌套事务,所以第二条数据添加失败进行回滚之后,会继续向上找调用他的方法和事务,再次进行回滚,因此第一条用户信息也添加失败,最终没用向用户表中添加任何数据~

三、Spring 事务失效场景

3.1、访问权限

Java 中一个方法的权限为 private,会导致事务失败.  因为 Spring 要求被代理的方法必须是 public.

下图中,在 Kotlin 中就直接指出错误了.

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

3.2、方法被 final 修饰

在 Java 中,如果一个方法被 final 修饰,事务也会失效.

下图中,Kotlin 就直接指出错误了

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

原因: 因为 Spring 事务底层是 AOP,他会为目标对象中的方法生成代理逻辑. 这个代理逻辑会在方法调用前后添加事务管理的相关代码(例如,开始事务、提交事务、回滚事务). 然而,如果目标方法被声明为 final,那么动态代理就无法对该对象进行增强,因为 final 方法不能被重写.

3.3、未被 Spring 管理

如果当前方法所在类,并没有被 Spring 注解标注(交给 Spring 管理),事务会失效.

这点就用多说了…

3.4、多线程(不在同一个线程下)

如下代码,add 方法是存在事务的,但是方法中通过多线程调用了 表的删除逻辑,这样就导致两个方法不在同一个线程中,获取到的数据库连接就不一样,因此是不同的事务. 

此时 add 引发异常,remove 逻辑不会回滚.

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

3.5、表不支持事务

如果表的引擎是 myisam,那么他就是不支持事务的.  要想支持事务,需要改成 innodb 引擎,如下

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

3.6、事务没有开启

如果是 SpringBoot 项目,那么事务默认是开启的.  但如果是 Spring 项目,需要配置 xml.

3.7、事务的传播机制

如果事务的创博机制设置错了,事务也不会生效

目前只有这几种传播机制会创建新事务:REQUIRED、REQUIRES_NEW、NESTED

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

3.8、try catch

事务没有回滚,最常见的问题就是:开发者在代码中手动 try catch,如下:

Spring “事务的使用”和“事务的传播机制”(代码示例 + 详解)

这种情况下,想要让事务生效,需要在 catch 中手动抛出(有种脱了裤子放屁的感觉…)

3.9、非 RuntimmeException 及其子类异常

Spring 事务默认情况下只会回滚 RuntimmeException(运行时异常),和她的子类.  其他的异常类型不会回滚,例如 IOException

@Service
class UserService: ServiceImpl<UserMapper, User>() {
 
    @Transactional
    fun remove(id: Long) {
        this.ktUpdate().eq(User::id, id)
            .remove()
        //1.引发异常(会回滚,因为是 RuntimeException 异常的子类)
        val a = 1 / 0
        //2.引发异常(不会回滚,因为 IOException 不是 RuntimeException 的子类)
        throw IOException()
    }
 
}

3.10、自定义回滚异常

Spring 事务中,可以通过 @Transactional 自定义回滚异常。但如果程序执行过程中,出现了不是我们自定义回滚异常的类型(包括 sql 异常),事务就不会回滚.

    @Transactional(rollbackFor = [IOException::class]) //表示只有 IOException 异常及其子类才会回滚
    fun remove(id: Long) {
        this.ktUpdate().eq(User::id, id)
            .remove()
        //这里不会回滚,因为不是我们指定的异常或其子类
        throw SQLException("sql 异常!")
    }

注意,在  @Transactional 中即使指定了回滚异常类型(例如 IOException),但如果引发异常的类型是 RuntimeException 类型(或者子类),事务也会回滚!

RuntimeException (及其子类)是一个特例!

如下,事务也会回滚:

    @Transactional(rollbackFor = [IOException::class]) //表示只有 IOException 异常及其子类才会回滚
    fun remove(id: Long) {
        this.ktUpdate().eq(User::id, id)
            .remove()
        //即使指定了异常类型为 IOException,但是遇到 RuntimeException 及其子类也会回滚
        throw RuntimeException("run 异常!")
    }

四、嵌套事务导致多回滚,如何解决?

@Service
class UserService: ServiceImpl<UserMapper, User>() {
 
    @Transactional
    fun handler(id: Long) {
        add(id)
        updateById(id)
    }
 
    @Transactional(propagation = Propagation.NESTED)
    fun updateById(id: Long) {
        println("处理了一些事情...")
    }
 
}

上述代码中,如果 handler 中的 add 方法执行成功了,而 updateById 方法执行失败了,那么由于 updateById 是嵌套事务,因此 add 方法也会回滚.

如果我只想让引发异常的 updateById 回滚呢?只需要将引发异常的方法 try catch 一下就 ok,如下:

@Service
class UserService: ServiceImpl<UserMapper, User>() {
 
    @Transactional
    fun handler(id: Long) {
        add(id)
        try {
            updateById(id)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
 
    @Transactional(propagation = Propagation.NESTED)
    fun updateById(id: Long) {
        println("处理了一些事情...")
    }
 
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/129780.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!