Spring声明式事务失效场景

发布于:2024-08-11 ⋅ 阅读:(131) ⋅ 点赞:(0)

背景

Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。

但是很多童鞋在使用上大多仅限于为方法标记 @Transactional,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。

本文既是记录这些坑

搭建测试环境

为了简单,这里使用 SpringBoot 整合 Mp快速搭建一下环境

spring.datasource.url=jdbc:mysql://192.168.133.128:3306/wxpay?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:mapper/*.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 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>3.3.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example.springbootV3</groupId>
	<artifactId>springbootV3</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springbootV3</name>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>3.1.3</version>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
		</dependency>
		<!-- springboot3版本整合mp-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
			<version>3.5.5</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
@SpringBootApplication
@MapperScan(basePackages = "com.example.demo.mapper")
public class DemoApplication {
	public static void main(String[] args) {
		var run = SpringApplication.run(DemoApplication.class, args);
	}
}
@RestController
@RequestMapping("/tx")
public class TxController {
    @Resource
    private UserService userService;
    @GetMapping("/createUser")
    public int createUser(@RequestParam("name") String name) {
        return userService.createUser(name);
    }
}

测试事务失效场景

@Transactional 注解标注在 private 方法上

代码如下,在 controller 层调用 userService 的 createUser 方法,但是 createUser 方法并没有标注 Transactional 注解,这样搞事务是不会生效的,虽然抛了异常,数据还是入库了

那你可能会想到,把 insertUser 方法变成 public 不就行了,然后重新测试,发现依然不行哈哈,因为Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象,而 this 指针代表对象自己,Spring 不可能注入 this,所以通过 this 访问方法必然不是代理。

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    public int createUser(String name) {
        User user = new User();
        user.setAge(18);
        user.setId(1);
        user.setName(name);
        user.setCreateTime(new Date());
        try {
            this.insertUser(user);
        } catch (Exception ex) {
            log.error("create user failed because {}", ex);
        }
        return 1;
    }
    @Transactional
    private void insertUser(User user) {
        this.save(user);
        throw new RuntimeException("invalid username!");
    }
}

像上面这种可以简化如下,也被称为Spring AOP 自调用问题,当一个方法被标记了@Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。

@Service
public class MyService {
private void method1() {
     method2();
     //......
}
@Transactional
 public void method2() {
     //......
  }
}

异常被 catch 了,事务失效

看到上面的例子,你可能马上想出整改方向,直接 controller 调用 service 层带有 Transactional 注解的 public 方法就好了,于是你立马写出下面一版代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    @Transactional
    public int createUser(String name) {
        User user = new User();
        user.setAge(18);
        user.setName(name);
        user.setCreateTime(new Date());
        try {
            this.save(user);
            throw new RuntimeException("invalid username!");
        } catch (Exception ex) {
            log.error("create user failed because {}", ex);
        }
        return 1;
    }

启动重新运行,发现事务还是失效了…

通过 AOP 实现事务处理可以理解为,使用 try…catch…来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。

这里的“一定条件”,主要包括两点。

第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。

第二,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。

那怎么整改呢?很简单,在 catch 代码块加上手动回滚代码

@Override
@Transactional
public int createUser(String name) {
    User user = new User();
    ...
    try {
        this.save(user);
        throw new RuntimeException("invalid username!");
    } catch (Exception ex) {
        log.error("create user failed because {}", ex);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();//手动回滚
    }
    return 1;
}

方法抛出的是受检异常,事务也会失效

上面也说了,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务,假如你写出下面的代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    @Transactional
    public int createUser(String name) throws IOException {
        User user = new User();
        user.setAge(18);
        user.setId(1);
        user.setName(name);
        user.setCreateTime(new Date());
        this.save(user);
        otherTask(); // 抛出了IOException ,这是个受检异常
        return 1;
    }
    private void otherTask() throws IOException {
        Files.readAllLines(Paths.get("file-that-not-exist"));
    }
}

启动重新运行,发现事务失效了,那怎么改呢,很简单,·在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):@Transactional(rollbackFor = Exception.class)

事务传播行为配置不合理导致事务失效

有这么一个场景:一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册。

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Resource
    private SubUserService subUserService;
    @Override
    @Transactional
    public int createUser(String name) {
        createMainUser(name+"主"); // 注册主账号
        subUserService.createSubUserWithExceptionWrong(name); // 注册子账号
        return 1;
    }
    private void createMainUser(String name) {
        User user = new User();
        user.setAge(18);
        user.setId(1);
        user.setName(name);
        user.setCreateTime(new Date());
        this.save(user);
        log.info("注册主账号..");
    }
}
@Service
@Slf4j
public class SubUserService {
    @Autowired
    private UserMapper userMapper;
    @Transactional
    public void createSubUserWithExceptionWrong(String name) {
        User user = new User();
        user.setAge(18);
        user.setId(22);
        user.setName(name+"子");
        user.setCreateTime(new Date());
        userMapper.insert(user); // 这里不要使用 userService的方法,不然启动报循环引用错误》
        throw new RuntimeException("注册子账号失败了...");
    }
}
@GetMapping("/createUser")
public int createUser(@RequestParam("name") String name) {
    try {
        return userService.createUser(name);
    } catch (IOException e) {
        log.error("createUserWrong failed, reason:{}", e.getMessage());
    }
    return 222;
}

启动运行会发现,事务回滚了,子账号和主账号都没有插入到数据库。

你马上就会意识到,不对呀,因为运行时异常逃出了 @Transactional 注解标记的 createUser 方法,Spring 当然会回滚事务了。如果我们希望主方法不回滚,应该把子方法抛出的异常捕获了。也就是这么改,把 subUserService.createSubUserWithExceptionWrong 包裹上 catch,这样外层主方法就不会出现异常了:

@Override
@Transactional
public int createUser(String name) {
    createMainUser(name+"主"); // 注册主账号
    try {
        subUserService.createSubUserWithExceptionWrong(name); // 注册子账号
    } catch (Exception exception) {
        // 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为 rollback了,所以最终还是会回滚。
        log.error("create sub user error:{}", exception.getMessage());
    }
    return 1;
}

你按照上面改了之后发现还是不行,还是回滚了,这是因为,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。

看到这里,修复方式就很明确了,想办法让子逻辑在独立事务中运行,也就是改一下 SubUserService 注册子用户的方法,为注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSubUserWithExceptionWrong(String name) {
    User user = new User();
    user.setAge(18);
    user.setId(22);
    user.setName(name+"子");
    user.setCreateTime(new Date());
    userMapper.insert(user); // 这里不要使用 userService的方法,不然启动报循环引用错误》
    throw new RuntimeException("注册子账号失败了...");
}

主方法没什么变化,同样需要捕获异常,防止异常漏出去导致主事务回滚

@Override
@Transactional
public int createUser(String name) {
    createMainUser(name+"主"); // 注册主账号
    try {
        subUserService.createSubUserWithExceptionWrong(name); // 注册子账号
    } catch (Exception exception) {
        // 捕获异常,防止主方法回滚
        log.error("create sub user error:{}", exception.getMessage());
    }
    return 1;
}

数据库可以看到,主账号的注册不受影响
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到