> 文章列表 > Transactional事务失效场景汇总

Transactional事务失效场景汇总

Transactional事务失效场景汇总

文章目录

  • 1、前言
  • 2、失效场景
    • 2.1、Service没有被Spring管理
    • 2.2、事务方法被final、static关键字修饰
    • 2.3、同一个类中,方法内部调用
    • 2.4、方法的访问权限不是public
    • 2.5、数据库的存储引擎不支持事务
    • 2.6、@Transactional 注解配置错误
    • 2.7、使用了错误的事务传播机制
    • 2.8、rollbackFor属性配置错误
    • 2.9、异常被捕获并处理了,没有抛出
    • 2.10、手动抛了别的异常
    • 2.11、多线程调用场景
  • 3、总结

1、前言

作为后端程序员,在日常开发中,经常会遇到事务处理的场景,在Spring中,为了更好的支撑我们进行数据库操作,它提供了两种事务管理的方式:

  • 编程式事务
  • 声明式事务

那众所周知,我们平时用的最多的就是声明式事务,也就是使用**@Transactional**注解的方式了

但是在日常开发中,如果对注解@Transactional使用不当的话,可能会导致事务失效,所以今天我们一起来总结梳理一下常见的一些失效场景,我这里梳理了下面这些场景:

Transactional事务失效场景汇总

2、失效场景

2.1、Service没有被Spring管理

看如下代码

package org.wujiangbo.service.impl.user;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;import javax.annotation.Resource;
import java.util.List;/*** <p>* 用户表 服务实现类* </p>** @author bobo(weixin:javabobo0513)*/
//@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{@Resourceprivate UserMapper userMapper;@Resourceprivate SysOperLogMapper logMapper;@Override@Transactionalpublic JSONResult addUser(User user, SysOperLog log) {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;//制造异常:发生算数异常return JSONResult.success("操作成功");}}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

上面例子中, @Service注解注释之后,spring事务(@Transactional)没有生效,因为Spring事务是由AOP机制实现的,也就是说从Spring IOC容器获取bean时,Spring会为目标类创建代理来支持事务。但是@Service被注释后,你的service类都不是spring管理的,那怎么创建代理类来支持事务呢,所以此种场景事务注解会失效,大家在开发过程中要仔细了,不要忘记,将@Transactional所在的类,交给Spring管理

2.2、事务方法被final、static关键字修饰

看如下代码:

package org.wujiangbo.service.impl.user;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;import javax.annotation.Resource;
import java.util.List;/*** <p>* 用户表 服务实现类* </p>** @author bobo(weixin:javabobo0513)*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{@Resourceprivate UserMapper userMapper;@Resourceprivate SysOperLogMapper logMapper;@Override@Transactionalpublic final JSONResult addUser(User user, SysOperLog log) {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;return JSONResult.success("操作成功");}}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

如果一个方法被声明为final或者static,则该方法不能被子类重写,也就是说无法在该方法上进行动态代理,这会导致Spring无法生成事务代理对象来管理事务

2.3、同一个类中,方法内部调用

看下面代码:

package org.wujiangbo.service.impl.user;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;import javax.annotation.Resource;
import java.util.List;/*** <p>* 用户表 服务实现类* </p>** @author bobo(weixin:javabobo0513)*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{@Resourceprivate UserMapper userMapper;@Resourceprivate SysOperLogMapper logMapper;@Overridepublic JSONResult addUser(User user, SysOperLog log) {doSomething(user, log);return JSONResult.success("操作成功");}@Transactionalpublic void doSomething(User user, SysOperLog log){//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;}}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

事务是通过Spring AOP代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用。即以上代码,调用目标doSomething方法不是通过代理类进行的,因此事务不生效

2.4、方法的访问权限不是public

看下面代码:

package org.wujiangbo.service.impl.user;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;import javax.annotation.Resource;
import java.util.List;/*** <p>* 用户表 服务实现类* </p>** @author bobo(weixin:javabobo0513)*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{@Resourceprivate UserMapper userMapper;@Resourceprivate SysOperLogMapper logMapper;@Override@Transactionalprivate JSONResult addUser(User user, SysOperLog log) {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;return JSONResult.success("操作成功");}}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

spring事务方法addUser的访问权限不是public,所以事务就不生效了,因为Spring事务是由AOP机制实现的,AOP机制的本质就是动态代理,而代理的事务方法不是public的话,computeTransactionAttribute()就会返回null,也就是这时事务属性不存在了

大家可以看下AbstractFallbackTransactionAttributeSource的源码:

Transactional事务失效场景汇总

2.5、数据库的存储引擎不支持事务

Spring事务的底层,还是依赖于数据库本身的事务支持。在MySQL中,MyISAM存储引擎是不支持事务的,InnoDB引擎才支持事务。因此开发阶段设计表的时候,必须要确认你的选择的存储引擎是支持事务的

比如下面的SQL创建用户表时,就采用的是InnoDB存储引擎:

DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',`age` int(11) NULL DEFAULT NULL COMMENT '年龄',`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

2.6、@Transactional 注解配置错误

看如下代码:

@Transactional(readOnly = true)
public JSONResult addUser(User user, SysOperLog log) {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;return JSONResult.success("操作成功");
}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

虽然使用了@Transactional注解,但是注解中的readOnly=true属性指示这是一个只读事务,因此在保存数据时会抛出如下异常:

Transactional事务失效场景汇总

我们使用@Transactional注解时,一般不需要跟后面的readOnly属性

2.7、使用了错误的事务传播机制

看如下代码:

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public JSONResult addUser(User user, SysOperLog log) {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;return JSONResult.success("操作成功");
}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

这里事务失效的原因是:Propagation.NOT_SUPPORTED表示传播特性不支持事务

我们一起来回顾下Spring提供了七种事务传播机制。它们分别是:

  • REQUIRED(默认):如果当前存在一个事务,则加入该事务;否则,创建一个新事务。该传播级别表示方法必须在事务中执行。
  • SUPPORTS:如果当前存在一个事务,则加入该事务;否则,以非事务的方式继续执行。
  • MANDATORY:如果当前存在一个事务,则加入该事务;否则,抛出异常。
  • REQUIRES_NEW:创建一个新的事务,并且如果存在一个事务,则将该事务挂起。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在一个事务,则将该事务挂起。
  • NEVER:以非事务方式执行操作,如果当前存在一个事务,则抛出异常。
  • NESTED:如果当前存在一个事务,则在嵌套事务内执行。如果没有事务,则按REQUIRED传播级别执行。嵌套事务是外部事务的一部分,可以在外部事务提交或回滚时部分提交或回滚。

2.8、rollbackFor属性配置错误

看如下代码:

@Transactional(rollbackFor = Error.class)
public JSONResult addUser(User user, SysOperLog log) throws Exception {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);if(1 == 1){//模拟抛出异常throw new Exception();}return JSONResult.success("操作成功");
}

分析:

rollbackFor属性指定的异常必须是Throwable或者其子类。默认情况下,RuntimeExceptionError两种异常都是会自动回滚的。但是因为以上的代码例子,指定了rollbackFor = Error.class,但是抛出的异常又是Exception,而Exception和Error没有任何什么继承关系,因此事务就不生效

大家可以看一下Transactional注解源码:

Transactional事务失效场景汇总

2.9、异常被捕获并处理了,没有抛出

看如下代码:

@Transactional
public JSONResult addUser(User user, SysOperLog log){try {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;}catch (Exception e){e.printStackTrace();}return JSONResult.success("操作成功");
}

代码执行结果:

虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中

分析:

事务中的异常已经被业务代码捕获并处理,而没有被正确地传播回事务管理器,事务将无法回滚

我们可以从spring源码(TransactionAspectSupport这个类)中找到答案:

public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {//省略其他代码,只留了下面核心代码@Nullableprotected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);Object retVal;try {//Spring AOP中MethodInterceptor接口的一个方法,它允许拦截器在执行被代理方法之前和之后执行额外的逻辑。retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {//用于在发生异常时完成事务(如果Spring catch不到对应的异常的话,就不会进入回滚事务的逻辑)completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {cleanupTransactionInfo(txInfo);}//用于在方法正常返回后提交事务。commitTransactionAfterReturning(txInfo);return retVal;}
}

invokeWithinTransaction方法中,当Spring catch到Throwable异常的时候,就会调用completeTransactionAfterThrowing()方法进行事务回滚的逻辑。但是在我们测试代码中,直接把异常catch住了,并没有重新throw出来,因此 Spring自然就catch不到异常啦,因此事务回滚的逻辑就不会执行,事务就失效了

解决方案

spring事务方法中,当我们使用了try-catch,如果catch住异常,记录完异常日志,一定要重新把异常抛出来,正例如下:

@Transactional
public JSONResult addUser(User user, SysOperLog log){try {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);int i = 1/0;}catch (Exception e){e.printStackTrace();throw e;}return JSONResult.success("操作成功");
}

在catch中添加:throw e;

2.10、手动抛了别的异常

看下面代码:

@Transactional
public JSONResult addUser(User user, SysOperLog log) throws Exception {//新增用户信息userMapper.insert(user);//新增日志记录logMapper.insert(log);if(1 == 1){//模拟抛出异常throw new Exception();}return JSONResult.success("操作成功");
}

分析:

Spring默认只处理RuntimeException和Error或其子类,对于普通的Exception是不会回滚的,但是上面的代码例子中,手动抛了Exception异常,所以是不会回滚,除非用rollbackFor属性指定,如下:

@Transactional(rollbackFor = Exception.class)

2.11、多线程调用场景

看下面代码:

@Transactional
public JSONResult addUser(User user, SysOperLog log){try {//新增用户信息userMapper.insert(user);//多线程调用new Thread(() -> {//新增日志记录logMapper.insert(log);}).start();//模拟异常int i = 1/0;}catch (Exception e){e.printStackTrace();throw e;}return JSONResult.success("操作成功");
}

代码执行结果:

虽然发生了算数异常,但是日志数据还是会存到数据库之中,只有用户数据会回滚

分析:

这是因为Spring事务是基于线程绑定的,每个线程都有自己的事务上下文,而多线程环境下可能会存在多个线程共享同一个事务上下文的情况,导致事务不生效

我们可以进入到TransactionAspectSupport类的prepareTransactionInfo方法中看一下,有一个解释如下:

Transactional事务失效场景汇总

简单翻译:

Transactional事务失效场景汇总

从这里我们得知,事务信息是跟线程绑定的。

因此在多线程环境下,事务的信息都是独立的,将会导致Spring在接管事务上出现差异

3、总结

经过这样的总结梳理,相信你应该已经对@Transactional 注解使用的一些坑有所了解了,以后在开发过程中就要格外注意了