背景
环境
相关环境配置:
-
SpringBoot+PostGreSQL
-
Spring Data JPA
问题
两个使用 Transaction 注解的 ServiceA 和 ServiceB,在 A 中引入了 B 的方法用于更新数据 ,当 A 中捕捉到 B 中有异常时,回滚动作正常执行,但是当 return 时则出现org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
异常。
代码示例:
ServiceA
@Transactional
public class ServiceA {
@Autowired
private ServiceB serviceB;
public Object methodA() {
try{
serviceB.methodB();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
复制代码
ServiceB
@Transactional
public class ServiceB {
public void methodB() {
throw new RuntimeException();
}
}
复制代码
知识回顾
@Transactional
Spring Boot 默认集成事务,所以无须手动开启使用 @EnableTransactionManagement 注解,就可以用 @Transactional 注解进行事务管理。
@Transactional
的作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口 :不推荐在接口上使用。
@Transactional
的常用配置参数
关于事务传播机制的详细介绍,可以参考这篇文章。
@Transactional
事务注解原理
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
Spring AOP 自调用问题
若同一类中的其他没有 @Transactional
注解的方法内部调用有 @Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。
这是由于Spring AOP
代理的原因造成的,因为只有当 @Transactional
注解的方法在类以外被调用的时候,Spring 事务管理才生效。
关于 AOP 自调用的问题,文章结尾会介绍相关解决方法。
@Transactional
的使用注意事项总结
@Transactional
注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;- 避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效; - 正确的设置
@Transactional
的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败。
Spring 的 @Transactional
注解控制事务有哪些不生效的场景?
- 数据库引擎是否支持事务(MySQL的MyISAM引擎不支持事务);
- 注解所在的类是否被加载成Bean类;
- 注解所在的方法是否为 public 方法;
- 是否发生了同类自调用问题;
- 所用数据源是否加载了事务管理器;
- @Transactional 的扩展配置 propagation(事务传播机制)是否正确。
- 方法未抛出异常
- 异常类型错误(最好配置rollback参数,指定接收运行时异常和非运行时异常)
案例分析
构建项目
1、创建 Maven 项目,选择相应的依赖。一般不直接用 MySQL 驱动,而选择连接池。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<mysql.version>8.0.19</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.18</version>
</dependency>
</dependencies>
复制代码
2、配置 application.yml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mysql_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
username: root
password: root
jpa:
hibernate:
ddl-auto: none
open-in-view: false
properties:
hibernate:
order_by:
default_null_ordering: last
order_inserts: true
order_updates: true
generate_statistics: false
jdbc:
batch_size: 5000
show-sql: true
logging:
level:
root: info # 是否需要开启 sql 参数日志
org.springframework.orm.jpa: DEBUG
org.springframework.transaction: DEBUG
org.hibernate.engine.QueryParameters: debug
org.hibernate.engine.query.HQLQueryPlan: debug
org.hibernate.type.descriptor.sql.BasicBinder: trace
复制代码
hibernate.ddl-auto: update
实体类中的修改会同步到数据库表结构中,慎用。show_sql
可开启 hibernate 生成的 SQL,方便调试。open-in-view
指延时加载的一些属性数据,可以在页面展现的时候,保持 session 不关闭,从而保证能在页面进行延时加载。默认为 true。logging
下的几个参数用于显示 sql 的参数。
3、MySQL 数据库中创建两个表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`age` int DEFAULT NULL,
`address` varchar(100) DEFAULT NULL,
`created_date` timestamp NULL,
`last_modified_date` timestamp NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
CREATE TABLE `job` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`user_id&#