【Java 开发常见的坑】——@Transactional 事务
Spring中通过@Transactional注解动态代理对目标方法的增强,可以很方便的回滚事务。但是,如果不熟悉使用@Transactional注解的话,却会有很多隐藏的坑不容易被发现,往往是在线上环境才出现问题,通过一番排查才找到问题所在,以下是本人实际工作中或是浏览其他相关博客模拟实现的场景,以此加深记忆和记录。
1.@Transactional注解标记的方法是private
2.@Transactional注解标记的方法不是Spring注入的bean调用
3.@Transactional注解没有显示声明rollbackFor属性
4.@Transactional注解标记的方法内,使用try...catch捕获异常
5.@Transactional注解使用默认的传播机制
打开@Transactional注解的内容
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
@Transactional属性详解
废话不多说,直接上案例!
以下的案例都是模拟新增用户的流程,为了简便,使用Spring Data JPA操作数据库。
User实体类
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
// 省略getter、setter
}
UserDao类
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
}
Controller类
@RestController
public class TransactionController {
@Autowired
private UserService userService;
@GetMapping("test")
public void test(){
userService.createUser();
}
}
1.@Transactional注解标记的方法是private
接下来看下Service实现类
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 创建用户
*/
public void createUser() {
insertUser();
}
@Transactional
private void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new RuntimeException("错误");
}
}
访问:http://localhost:8080/test后,可以发现控制台报错,证明有抛出异常,那么事务是否有回滚呢?查看一下数据库的User表,却发现有新增用户信息,就证明事务并没有回滚,事务回滚失效了!
这是为什么呢?这就需要知道@Transactional的原理,实际上就是Spring中的AOP,使用@Transactional注解,Spring就会通过动态代理的方式增强目标方法。所以private的方法是无法被代理,所以动态代理失效,无法回滚事务!
既然知道原因,那是不是将private方法改为public就行啦?
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 创建用户
*/
public void createUser() {
insertUser();
}
@Transactional
public void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new RuntimeException("错误");
}
}
再次访问http://localhost:8080/test,虽然控制台有输出报错信息,但还是没有回滚数据库的操作,这就纳闷了,不是使用@Transactional注解就可以了吗?
这就引申到下一个"坑"了
2.@Transactional注解标记的方法不是Spring注入的bean调用
有点拗口,其实简单理解为@Transactional注解标记的方法应该是Bean的调用,而不是方法内调用。例子中@Transactional注解标记的方法是由Bean内部方法的调用,所以将@Transactional注解放到例子中的createUser方法就可以了。
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 创建用户
*/
@Transactional
public void createUser() {
insertUser();
}
public void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new RuntimeException("错误");
}
}
访问http://localhost:8080/test,这次数据表就没有新增用户信息了,就证明事务回滚。
小结:使用@Transactional注解的方法,访问级别应该是public,而且应该是被Bean调用的方法
3.@Transactional注解没有显示声明rollbackFor属性
那我再对Service改一下,抛出的异常由原来的RuntimeException
改为Exception
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 创建用户
*/
@Transactional
public void createUser() throws Exception {
insertUser();
}
public void insertUser() throws Exception {
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new Exception("错误");
}
}
访问http://localhost:8080/test,再次发现由新增用户信息。My God!这又是什么坑呀?
其实,这是由于不熟悉@Transactional注解的原因。
这是因为Spring框架的事务管理默认地只在发生不受控异常(RuntimeException和Error)时才进行事务回滚。也就是说,当事务方法抛出受控异常(Exception中除了RuntimeException及其子类以外的)时不会进行事务回滚。
而rollbackFor
属性的默认值是 RuntimeException ,但是如果抛出的异常是 Exception 类型,@Transactional注解无法捕获异常,所以也就无法回滚事务。阿里巴巴规范建议使用@Transactional注解的时候显式地声明rollbackFor属性的值
// @Transactional注解 rollbackFor 属性默认值
@Transactional(rollbackFor = RuntimeException.class)
错误使用:
@Transactional
public void test(){}
正确使用:
@Transactional(rollbackFor = Exception.class)
public void test(){}
ps.强烈建议大家在Idea上安装阿里巴巴规范插件,插件扫描代码,发现有不规范的地方就回有提示,使咱们的代码更加规范、更加优雅!
blog-插件提示.jpg将原本使用 @Transactional 改为 @Transactional(rollbackFor = Exception.class)后,重新启动访问http://localhost:8080/test后可以发现,用户信息没有新增,就证明事务回滚了!
小结:使用 @Transactional 注解的时候,为了避免隐藏的bug,一定要显式声明rollbackFor属性的值!
4.@Transactional注解标记的方法内,使用try...catch捕获异常
接下来,模拟另外一个坑,这也是一个十分常见的事务失效问题
改动使用 @Transactional 注解的方法,将原本throw异常改为try...catch捕获异常
/**
* 创建用户
*/
@Transactional(rollbackFor = Exception.class)
public void createUser(){
try {
insertUser();
} catch (Exception e) {
e.printStackTrace();
}
}
访问http://localhost:8080/test后可以发现,用户信息新增了,就证明事务并没有回滚!
这是因为异常信息在被@Transactional捕获之前被try...catch...捕获了,相对于try...catch..."吃"掉了异常,@Transactional就无法捕获异常,所以就无法回滚事务!
那我想通过使用try...catch...捕获异常并做出一些补偿机制,怎么办?其实也是可以的,加上一行:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
/**
* 创建用户
*/
@Transactional(rollbackFor = Exception.class)
public void createUser(){
try {
insertUser();
} catch (Exception e) {
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 可以自定义出异常后的操作
}
}
小结:使用@Transactional注解的时候,要注意异常信息会不会被try...catch...捕获。
5.@Transactional注解使用默认的传播机制
@Transactional注解中,有个属性propagation,默认的传播级别为Propagation.REQUIRED
propagation属性的值有以下几种选择
- Propagation.REQUIRED(默认):如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务
- Propagation.SUPPORTS:如果当前存在事务,则加入事务,没有则以非事务方式运行
- Propagation.MANDATORY:当前存在事务,则加入事务,不存在事务则抛出异常
- Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED
但要根据实际的业务场景选择事务传播级别,不一定默认的传播级别适用!
假设现在的业务场景是,先创建用户信息,然后根据用户信息创建学生信息(Student表),但如果由于某些原因,创建学生信息失败,但不能影响用户信息的创建。所以创建用户信息和学生信息应该在不同的事务内,这样才不会相互影响,这样的话,使用@Transactional默认的传播级别就实现不了,但我们可以改变propagation
属性值,改为Propagation.REQUIRES_NEW
Student实体类
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String classroom;
// 省略getter、setter
}
StudentDao类
@Repository
public interface StudentDao extends JpaRepository<Student,Integer> {
}
StudentService实现类
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
/**
* 创建学生基本信息
*/
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void createStudentInfo() throws Exception {
Student student = new Student();
student.setName("MuggleLee");
student.setClassroom("高一一班");
studentDao.save(student);
throw new Exception("错误");
}
}
UserService实现类
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private StudentService studentService;
/**
* 创建用户
*/
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void createUser() {
insertUser();
try {
studentService.createStudentInfo();
} catch (Exception e) {
e.printStackTrace();
}
}
private void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
}
}
重启后访问http://localhost:8080/test,可以发现用户信息可以正常新增,但学生信息却没有新增,就证明学生新增信息被事务回滚,但不影响用户信息新增。
以上都是常见的事务失效的场景,希望能够诸位在开发的时候,多加注意!
如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~
参考资料:
极客时间——专栏:Java业务开发常见错误100例