您的当前位置:首页正文

SpringBoot 事务失效及其对应解决办法

2024-11-05 来源:个人技术集锦

简介

本文主要讲述Spring事务会去什么情况下失效及其解决办法

Spring 通过AOP 进行事务控制,如果操作数据库报异常,则会进行回滚;如果没有报异常则会提交事务;但是,如果Spring 事务失效,会导致数据缺失/重复等异常问题。

演示环境 

编译工具:IDEA 社区版本

数据库:MySQL 8

框架:SpringBoot 2.1.0 + MyBatis-plus3.4.3 + lombok等

库表:

-- bill.base_project definition

CREATE TABLE `base_project` (
  `id` varchar(64) NOT NULL COMMENT '主键',
  `tid` varchar(64) NOT NULL COMMENT '地区行政编码',
  `ptid` varchar(64) NOT NULL COMMENT '所属省级行政编码',
  `project_name` varchar(256) NOT NULL COMMENT '项目名称',
  `project_address` varchar(512) DEFAULT NULL COMMENT '项目地址',
  `project_no` varchar(128) NOT NULL COMMENT '项目编号',
  `developer_entity_name` varchar(218) DEFAULT NULL COMMENT '开发企业名称',
  `developer_entity_no` varchar(128) DEFAULT NULL COMMENT '开发企业编号',
  `suery_unit` varchar(128) DEFAULT NULL COMMENT '勘察单位',
  `design_unit` varchar(128) DEFAULT NULL COMMENT '设计单位',
  `build_unit` varchar(128) DEFAULT NULL COMMENT '施工单位',
  `supervisor_unit` varchar(128) DEFAULT NULL COMMENT '监理单位',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

SpringBootCase 父类项目 pom.xml 文件

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>SpringBoot-MyBatisPlus</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>SpringBootCase</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-properties-migrator</artifactId>
            <scope>runtime</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

SpringBoot-MyBatisPlus 子类项目 pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>SpringBootCase</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>SpringBoot-MyBatisPlus</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <mybatisplus-spring-boot-starter.version>3.4.3</mybatisplus-spring-boot-starter.version>
        <mysql-spring-boot-starter.version>8.0.19</mysql-spring-boot-starter.version>
    </properties>


    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus-spring-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-spring-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <executable>true</executable>
                    <!--必须为ZIP模式,不指定的话-Dloader.path不生效-->
                    <layout>ZIP</layout>
                    <!-- 打包的时候排除的jar包-->
                    <includes>
                        <include>
                            <groupId>non-exists</groupId>
                            <artifactId>non-exists</artifactId>
                        </include>
                    </includes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <!--指定的依赖路径-->
                            <outputDirectory>
                                ${project.build.directory}/lib
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

资源配置文件:application.yml

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.43.10:3306/bill?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true
    username: root
    password: 123456
    hikari:
      minimum-idle: 10
      maximum-pool-size: 20
      idle-timeout: 500000
      max-lifetime: 540000
      connection-timeout: 60000
      connection-test-query: select 1

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.zzg.mybatisplus.entity


其他相关文件,会在相关代码演示中一一讲解

JDBC 事务原理

我这里简单编写了一份jdbc 事务管理的通用伪代码:

//获取数据库连接
Connection connection = DriverManager.getConnection();
//设置自动提交为false
connection.setAutoCommit(false);
// SQL 操作
.........
//事务提交或回滚
connection.commit()/connection.rollback
//关闭数据库连接
connection.close();

 Spring 事务原理

Spring 通过事务注解@Transactional来控制事务,底层实现是基于切面编程AOP实现的,而Spring中实现AOP机制采用的是动态代理,具体分为JDK动态代理和CGLIB动态代理两种模式。

功能描述:

1.Spring 在bean的初始化过程中,检查方法是否被Transactional注解修饰,如果方法被修饰需要对相应的Bean进行代理,生成代理对象。

2. 在方法调用的时候,会执行切面的逻辑,而这里切面的逻辑中就包含了开启事务、提交事务或者回滚事务等逻辑。

温馨提示:Spring 本身不实现事务,底层还是依赖于数据库的事务。

Spring 事务失效场景

前提Mapper接口和PO对象:

package cn.zzg.mybatisplus.mapper;

import cn.zzg.mybatisplus.entity.BaseProjectPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface BaseProjectMapper extends BaseMapper<BaseProjectPO> {
}
package cn.zzg.mybatisplus.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;

@Data
@TableName("base_project")
public class BaseProjectPO extends Model {
    private String id;

    private String tid;

    private String ptid;

    private String projectName;

    private String projectAddress;

    private String projectNo;

    private String developerEntityName;

    private String developerEntityNo;

    private String sueryUnit;

    private String designUnit;

    private String buildUnit;

    private String supervisorUnit;
}
package cn.zzg.mybatisplus.service;

import cn.zzg.mybatisplus.entity.BaseProjectPO;
import com.baomidou.mybatisplus.extension.service.IService;

import java.io.IOException;

public interface IBaseProjectService extends IService<BaseProjectPO> {
}

1.抛出异常

比如事务代码控制如下:

在IBaseProjectService服务接口,新增如下方法定义:

 void insertBaseProject1(BaseProjectPO baseProject) throws IOException;

 在BaseProjectServiceImpl服务实现类中如下实现:

   @Transactional
   @Override
    public void insertBaseProject1(BaseProjectPO baseProject) throws IOException {
        boolean target = this.baseMapper.insert(baseProject) > 0 ? true: false;
        throw new IOException();
   }

在Controller调用该方法

   @GetMapping("/insert1")
    public boolean insert1() throws IOException {
        BaseProjectPO po = new BaseProjectPO();
        po.setId("4");
        po.setTid("4");
        po.setPtid("4");
        po.setProjectNo("4");
        po.setProjectName("4");
        service.insertBaseProject1(po);
        return true;
    }

最终数据库库表截图: 

说明此事务失效。 

造成原因

@Transactional 没有特别指定异常,Spring 只会在遇到运行时异常RuntimeException或者error时进行回滚,而IOException等异常不会影响回滚。

解决办法:

配置rollbackFor属性,常用配置为:@Transactional(rollbackFor = Exception.class)

服务类调整后,事务生效代码片段

   @Transactional(rollbackFor = Exception.class)
   @Override
    public void insertBaseProject1(BaseProjectPO baseProject) throws IOException {
        boolean target = this.baseMapper.insert(baseProject) > 0 ? true: false;
        throw new IOException();
   }

2.业务方法捕获异常

在IBaseProjectService服务接口,新增如下方法定义:
 void insertBaseProject2(BaseProjectPO baseProject) ;

在BaseProjectServiceImpl服务实现类中如下实现:

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void insertBaseProject2(BaseProjectPO baseProject) {
        try{
            this.baseMapper.insert(baseProject);
            int i = 1/0;
        }catch (Exception e){
            e.printStackTrace();
        }
    }

在Controller调用该方法

 @GetMapping("/insert1")
    public boolean insert1() throws IOException {
        BaseProjectPO po = new BaseProjectPO();
        po.setId("5");
        po.setTid("5");
        po.setPtid("5");
        po.setProjectNo("5");
        po.setProjectName("5");
        service.insertBaseProject2(po);
        return true;
    }

最终数据库库表截图: 

 说明此事务失效。 

造成原因:

Spring是否进行回滚是根据你的代码是否抛出异常决定的,所以如果你自己捕获了异常,自己处理并且没有抛出异常会导致Spring 事务失效。

解决办法:

1.配置rollbackFor属性,常用配置为:@Transactional(rollbackFor = Exception.class)。并在try...catch 代码片段中抛出相关异常。

2.使用编写式事务,个人推荐使用。

transactionTemplate.execute

服务类调整后,事务生效代码片段

第一种方法:

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void insertBaseProject2(BaseProjectPO baseProject) throws IOException {
        try{
            this.baseMapper.insert(baseProject);
            int i = 1/0;
        }catch (Exception e){
            e.printStackTrace();
            throw new IOException();
        }
    }

第二种方法:

    @Override
    public void insertBaseProject2(BaseProjectPO baseProject) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult(){
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                   BaseProjectServiceImpl.super.baseMapper.insert(baseProject);
                    int i = 1/0;
                } catch (Exception e) {
                    status.setRollbackOnly() ;
                }
            }
        });
    }

温馨提示:记录用户操作日志优化点......

3.同一类中的方法调用

在IBaseProjectService服务接口,新增如下方法定义:

 boolean insertBaseProject(BaseProjectPO baseProject);

在BaseProjectServiceImpl服务实现类中如下实现:

  @Override
  public boolean insertBaseProject(BaseProjectPO baseProject) {
      return this.insertTrans(baseProject);
   
  }

  @Transactional
  public boolean insertTrans(BaseProjectPO baseProject){
    // 数据库操作
   boolean target = this.baseMapper.insert(baseProject) > 0 ? true: false;
   // 模拟异常操作
    int i = 1/0;
   return target;
  }

在Controller调用该方法


    @GetMapping("/insert")
    public boolean insert() {
        BaseProjectPO po = new BaseProjectPO();
        po.setId("2");
        po.setTid("2");
        po.setPtid("3");
        po.setProjectNo("2");
        po.setProjectName("2");
        return service.insertBaseProject(po);
    }

最终数据库库表截图: 

 说明此事务失效。

造成原因:

Spring的事务管理功能是通过动态代理实现的,而Spring默认使用JDK动态代理,而JDK动态代理采用接口实现的方式,通过反射调用目标类。简单理解,就是insertBaseProject()方法中调用this.insertTrans(),这里的this是真实对象,所以会直接走insertTrans()的业务逻辑,而不会走切面逻辑,所以事务失败。

Aop 事务伪代码

@Service
class A{
    @Transactinal
    method b(){...}
    
    method a(){    //标记1
        b();
    }
}
 
//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法插入一个startTransaction()方法:
class proxy$A{
    A objectA = new A();
    method b(){    //标记2
        startTransaction();
        objectA.b();
    }
 
    method a(){    //标记3
        objectA.a();    //由于a()没有注解,所以不会启动transaction,而是直接调用A的实例的a()方法
    }
}

详细说明:

当我们调用A的bean的a()方法的时候,也是被proxy$A拦截,执行proxy$A.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行。

 解决办法:

方案一:解决方法可以是直接在调用类中添加@Transactional注解insertBaseProject()

方案二@EnableAspectJAutoProxy(exposeProxy = true)在启动类中添加,会由Cglib代理实现。

在pom.xml 添加cglib 相关依赖

   <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>

Application 程序入口 添加

@EnableAspectJAutoProxy(exposeProxy = true)
package cn.zzg.mybatisplus;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
@MapperScan({"cn.zzg.mybatisplus.mapper"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

服务类调整后,事务生效代码片段

    @Override
  public boolean insertBaseProject(BaseProjectPO baseProject) {
      //return this.insertTrans(baseProject);
      return ((BaseProjectServiceImpl) AopContext.currentProxy()).insertTrans(baseProject);

  }

方案三: 使用ApplicationContext 获取Sevice 实现类

package cn.zzg.mybatisplus.components;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContetxtHolder implements ApplicationContextAware {
    private static ApplicationContext context;

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

    public static ApplicationContext getContext(){
        return context;
    }
}

 在BaseProjectServiceImpl服务实现类中如下实现:

    @Override
  public boolean insertBaseProject(BaseProjectPO baseProject) {
        //return this.insertTrans(baseProject);
        //return ((BaseProjectServiceImpl) AopContext.currentProxy()).insertTrans(baseProject);
        BaseProjectServiceImpl service = ApplicationContetxtHolder.getContext().getBean(BaseProjectServiceImpl.class);
        return service.insertTrans(baseProject);
  }

在Controller调用该方法

    @GetMapping("/insert")
    public boolean insert() {
        BaseProjectPO po = new BaseProjectPO();
        po.setId("6");
        po.setTid("6");
        po.setPtid("6");
        po.setProjectNo("6");
        po.setProjectName("6");
        return service.insertBaseProject(po);
    }

 4. 方法使用 final 或 static关键字

如果Spring使用了Cglib代理实现(比如你的代理类没有实现接口),而你的业务方法恰好使用了final或者static关键字,那么事务也会失败。更具体地说,它应该抛出异常,因为Cglib使用字节码增强技术生成被代理类的子类并重写被代理类的方法来实现代理。如果被代理的方法的方法使用finalstatic关键字,则子类不能重写被代理的方法。

如果Spring使用JDK动态代理实现,JDK动态代理是基于接口实现的,那么finalstatic修饰的方法也就无法被代理。

总而言之,方法连代理都没有,那么肯定无法实现事务回滚了。

解决办法

方法去掉final或者static关键字

5. 方法不是public

如果方法不是publicSpring事务也会失败,因为Spring的事务管理源码AbstractFallbackTransactionAttributeSource中有判断computeTransactionAttribute()。如果目标方法不是公共的,则TransactionAttribute返回null

// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
  return null;
}

解决办法

方法访问级别修改为publi

6. 错误使用传播机制

Spring事务的传播机制是指在多个事务方法相互调用时,确定事务应该如何传播的策略。Spring提供了七种事务传播机制:REQUIREDSUPPORTSMANDATORYREQUIRES_NEWNOT_SUPPORTEDNEVERNESTED。如果不知道这些传播策略的原理,很可能会导致交易失败。

@Service
public class TransactionService {


    @Autowired
    private UserMapper userMapper;

    @Autowired
    private AddressMapper addressMapper;


    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public  void doInsert(User user,Address address) throws Exception {
        //do something
        userMapper.insert(user);
        saveAddress(address);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public  void saveAddress(Address address) {
        //do something
        addressMapper.insert(address);
    }
}

在上面的例子中,如果用户插入失败,不会导致saveAddress()回滚,因为这里使用的传播是REQUIRES_NEW,传播机制REQUIRES_NEW的原理是如果当前方法中没有事务,就会创建一个新的事务。如果一个事务已经存在,则当前事务将被挂起,并创建一个新事务。在当前事务完成之前,不会提交父事务。如果父事务发生异常,则不影响子事务的提交。

事务的传播机制说明如下:

REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
MANDATORY 如果当前上下文中存在事务,否则抛出异常。
REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

解决办法

将事务传播策略更改为默认值REQUIREDREQUIRED原理是如果当前有一个事务被添加到一个事务中,如果没有,则创建一个新的事务,父事务和被调用的事务在同一个事务中。即使被调用的异常被捕获,整个事务仍然会被回滚。

7.没有被Spring管理


//@Service(value = "baseProjectServiceImpl")
public class BaseProjectServiceImpl extends ServiceImpl<BaseProjectMapper, BaseProjectPO> implements IBaseProjectService {
    ******
}

此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

解决办法

保证每个事务注解的每个Bean被Spring管理。

Spring 编程事务

编程事务实现方式:

官方推荐使用TransactionTemplate 方式。本章节也是重点讲解TransactionTemplate 使用

TransactionTemplate

带返回值

@Service 
public class UserService { 
     
  @Resource 
  private TransactionTemplate transactionTemplate ; 
  @Resource 
  private UsersRepository usersRepository ; 
     
  public Integer saveUsers(Users users) { 
    this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 
    Integer result = transactionTemplate.execute(new TransactionCallback<Integer>() { 
      @Override 
      public Integer doInTransaction(TransactionStatus status) { 
        return usersMapper.insertUser(users) ; 
      } 
    }) ; 
    return result ; 
    } 
     
} 

无返回值

public void saveUsers(Users users) { 
  transactionTemplate.execute(new TransactionCallbackWithoutResult() { 
    @Override 
    protected void doInTransactionWithoutResult(TransactionStatus status) { 
      usersMapper.insertUser(users) ; 
    } 
  }) ; 
} 

事务回滚

public Users saveUser(Users users) { 
  return transactionTemplate.execute(new TransactionCallback<Users>() { 
    @Override 
    public Users doInTransaction(TransactionStatus status) { 
      try { 
        return usersMapper.insertUser(users) ; 
      } catch (Exception e) { 
        status.setRollbackOnly() ; 
      } 
      return null ; 
    } 
  }) ; 
} 

显示全文