宁波网站建设公司哪有,做聊天室cpa用什么类型的网站好,济南建站方案,网站开发前端培训Spring声明式事务失效场景 背景搭建测试环境测试事务失效场景Transactional 注解标注在 private 方法上异常被 catch 了#xff0c;事务失效方法抛出的是受检异常#xff0c;事务也会失效事务传播行为配置不合理导致事务失效 背景
Spring 针对 Java Transaction API (JTA)、… Spring声明式事务失效场景 背景搭建测试环境测试事务失效场景Transactional 注解标注在 private 方法上异常被 catch 了事务失效方法抛出的是受检异常事务也会失效事务传播行为配置不合理导致事务失效 背景
Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API实现了一致的编程模型而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式配合 Spring Boot 的自动配置大多数 Spring Boot 项目只需要在方法上标记 Transactional 注解即可一键开启方法的事务性配置。
但是很多童鞋在使用上大多仅限于为方法标记 Transactional不会去关注事务是否有效、出错后事务是否正确回滚也不会考虑复杂的业务代码中涉及多个子业务逻辑时怎么正确处理事务。
本文既是记录这些坑
搭建测试环境
为了简单这里使用 SpringBoot 整合 Mp快速搭建一下环境
spring.datasource.urljdbc:mysql://192.168.133.128:3306/wxpay?useUnicodetruecharacterEncodingutf-8rewriteBatchedStatementstrueuseSSLfalseserverTimezoneAsia/Shanghai
spring.datasource.usernameroot
spring.datasource.passwordroot
spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver#mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locationsclasspath*:mapper/*.xml?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion3.3.0/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.example.springbootV3/groupIdartifactIdspringbootV3/artifactIdversion0.0.1-SNAPSHOT/versionnamespringbootV3/namepropertiesjava.version17/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactIdversion3.1.3/version/dependencydependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactId/dependency!-- springboot3版本整合mp--dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot3-starter/artifactIdversion3.5.5/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build
/projectSpringBootApplication
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 {Resourceprivate 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 ServiceImplUserMapper, User implements UserService {Overridepublic 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;}Transactionalprivate 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();//......
}
Transactionalpublic void method2() {//......}
}
异常被 catch 了事务失效
看到上面的例子你可能马上想出整改方向直接 controller 调用 service 层带有 Transactional 注解的 public 方法就好了于是你立马写出下面一版代码
Service
public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService {OverrideTransactionalpublic 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 ServiceImplUserMapper, User implements UserService {OverrideTransactionalpublic 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 ServiceImplUserMapper, User implements UserService {Resourceprivate SubUserService subUserService;OverrideTransactionalpublic 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 {Autowiredprivate UserMapper userMapper;Transactionalpublic 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;
}数据库可以看到主账号的注册不受影响