当前位置: 首页 > article >正文

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使用不当的话,可能会导致事务失效,所以今天我们一起来总结梳理一下常见的一些失效场景,我这里梳理了下面这些场景:

image-20230419182746992

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{

    @Resource
    private UserMapper userMapper;
    @Resource
    private SysOperLogMapper logMapper;

    @Override
    @Transactional
    public 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{

    @Resource
    private UserMapper userMapper;
    @Resource
    private SysOperLogMapper logMapper;

    @Override
    @Transactional
    public 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{

    @Resource
    private UserMapper userMapper;
    @Resource
    private SysOperLogMapper logMapper;

    @Override
    public JSONResult addUser(User user, SysOperLog log) {
        doSomething(user, log);
        return JSONResult.success("操作成功");
    }

    @Transactional
    public 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{

    @Resource
    private UserMapper userMapper;
    @Resource
    private SysOperLogMapper logMapper;

    @Override
    @Transactional
    private 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的源码:

image-20230419173640624

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属性指示这是一个只读事务,因此在保存数据时会抛出如下异常:

image-20230419174314860

我们使用@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注解源码:

image-20230419175336972

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 {

//省略其他代码,只留了下面核心代码
    
@Nullable
 protected 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方法中看一下,有一个解释如下:

image-20230419181636899

简单翻译:

image-20230419181711752

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

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

3、总结

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


http://www.kler.cn/a/13691.html

相关文章:

  • 第三十四章 配置镜像 - 在镜像中激活日志加密
  • 系统环境配置
  • leetcode每日一题——美团笔试题【1】
  • 【1】从零开始学习目标检测:YOLO算法详解
  • 【数据结构】数据结构小试牛刀之单链表
  • 关于git这一篇就够了
  • Scala之面向对象(2)
  • Java阶段二Day05
  • react知识库
  • 【状态估计】电力系统状态估计的虚假数据注入攻击建模与对策(Matlab代码实现)
  • Java7
  • 机器学习——SVM的易错题型
  • ES使用小结
  • 时序预测 | MATLAB实现SSA-LSTM、LSTM麻雀算法优化长短期记忆神经网络时间序列预测(含优化前后对比)
  • 在VMmare上安装Windows 2003
  • D3.js(3) path/折线图
  • apple pencil值不值得购买?ipad平替电容笔安利
  • JavaSE-part2
  • Java 线程
  • flutter protobuf插件的安装和使用