Spring(SpringBoot)--事务失效--原因/场景/解决方案
简介
本文介绍 Spring 什么时候事务会失效以及如何解决。
Spring 通过 AOP 进行事务的控制,如果操作数据库报异常,则会进行回滚;如果没有报异常则会提交事务。但是,有时候 Spring 事务会失效,本文将介绍 Spring 的事务何时会失效,以及如何避免事务失效。
情景 1:异常类型错误
声明式事务和注解事务回滚的原理:当被切面切中或者是加了注解的方法中抛出了 unchecked exception 异常(默认情况)时,Spring 会进行事务回滚。unchecked exception 异常也就是:RuntimeException 及其子类。
不回滚的情况
- 把异常给 try catch 了,没有手动抛出 RuntimeException 异常
- 抛出的异常不属于运行时异常(如 IO 异常),因为 Spring 默认情况下是捕获到 RuntimeException 就回滚
会回滚的情况
- 用了 try catch,在 catch 里面再抛出一个 RuntimeException 异常。
- 将 Spring 默认的回滚时的异常修改为 Exception
- 这样可以让非运行时异常也要能回滚
- 在 catch 后写回滚代码来实现回滚。
- TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
- 这样就可以在抛异常后也能 return 返回值;比较适合需要拿到返回值的场景(),
情况 2 示例:
@Transactional(rollbackFor = { Exception.class })
public boolean test() {
doDbSomeThing();
//其他操作
return true;
}
情况 3 示例
/** TransactionAspectSupport手动回滚事务:*/
public boolean test() {
try {
doDbSomeThing();
} catch (Exception e) {
e.printStackTrace();
//加上之后抛了异常就能回滚(有这句代码就不需要再手动抛出运行时异常了)
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return false;
}
return true;
}
情景 2:自调用
简介
如果在一个类里边,调用同类里边的方法,会导致被调用的方法事务失效,有如下几种情景:
情景 1:无事务调用事务,被调用的方法事务失效,此时抛出了异常也不会回滚。
情景 2:在 REQUIRED 级别调用 REQUIRES_NEW 级别时,进入 REQUIRES_NEW 级别的方法时没有新创建事务。但若 REQUIRES_NEW 级别的方法里抛了异常,则 REQUIRED 级别与 REQUIRES_NEW 级别的操作都会回滚。
复现
情景 1:无事务调用事务
package com.example.demo.user.controller;
import com.example.demo.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
UserService userService;
@PostMapping("/test")
public void test() {
userService.insertAndUpdate();
}
}
package com.example.demo.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.user.entity.User;
public interface UserService extends IService<User> {
void insertAndUpdate();
}
package com.example.demo.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.user.entity.User;
import com.example.demo.user.mapper.UserMapper;
import com.example.demo.user.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public void insertAndUpdate() {
User user = new User();
user.setName("Tony");
this.save(user);
updateUser(user);
}
@Transactional
public void updateUser(User user) {
user.setAge(20);
this.updateById(user);
int i = 1 / 0;
}
}
测试结果:(事务失效。我们想要的是:id 和 name 有值正常,age 不应该有值)
JDBC Connection [HikariProxyConnection@769992042 wrapping com.mysql.cj.jdbc.ConnectionImpl@3eac1b48] will not be managed by Spring
==> Preparing: INSERT INTO t_user ( name ) VALUES ( ? )
==> Parameters: Tony(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@113376db]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a445157] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@80636421 wrapping com.mysql.cj.jdbc.ConnectionImpl@3eac1b48] will not be managed by Spring
==> Preparing: UPDATE t_user SET name=?, age=? WHERE id=?
==> Parameters: Tony(String), 20(Integer), 1(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a445157]
2021-05-19 21:58:13.902 ERROR 5948 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at ...
......
情景 2:事务调用事务
package com.example.demo.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.user.entity.User;
import com.example.demo.user.mapper.UserMapper;
import com.example.demo.user.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insertAndUpdate() {
User user = new User();
user.setName("Tony");
this.save(user);
updateUser(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUser(User user) {
user.setAge(20);
this.updateById(user);
}
}
访问:http://127.0.0.1:8080/test/test
结果: (进入 REQUIRES_NEW 级别的方法时没有新创建事务)
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
JDBC Connection [HikariProxyConnection@367846290 wrapping com.mysql.cj.jdbc.ConnectionImpl@7f014cae] will be managed by Spring
==> Preparing: INSERT INTO t_user ( name ) VALUES ( ? )
==> Parameters: Tony(String)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] from current transaction
==> Preparing: UPDATE t_user SET name=?, age=? WHERE id=?
==> Parameters: Tony(String), 20(Integer), 2(Long)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
解决方案
简介
bean 的方法自调用时不会走动态代理,就无法执行到事务的 AOP。解决方案就是:从容器中获取此类的 bean,然后使用这个 bean 来调用方法,这样就能走代理。有以下三种方法:
- @Autowired 注入自己(推荐)
- 通过 ApplicationContext 获得自己
- 通过 AopContext 获取当前代理对象
法 1:@Autowired 注入自己(推荐)
package com.example.demo.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.user.entity.User;
import com.example.demo.user.mapper.UserMapper;
import com.example.demo.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserServiceImpl userServiceImpl;
@Override
public void insertAndUpdate() {
User user = new User();
user.setName("Tony");
this.save(user);
userServiceImpl.updateUser(user);
}
@Transactional
public void updateUser(User user) {
user.setAge(20);
this.updateById(user);
int i = 1 / 0;
}
}
法 2:ApplicationContext 获得自己
package com.example.demo.common;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
public void setApplicationContext(ApplicationContext context) throws BeansException {
ApplicationContextHolder.context = context;
}
public static ApplicationContext getContext() {
return context;
}
}
package com.example.demo.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.common.ApplicationContextHolder;
import com.example.demo.user.entity.User;
import com.example.demo.user.mapper.UserMapper;
import com.example.demo.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public void insertAndUpdate() {
User user = new User();
user.setName("Tony");
this.save(user);
UserServiceImpl userServiceImpl = ApplicationContextHolder.getContext()
.getBean(UserServiceImpl.class);
userServiceImpl.updateUser(user);
}
@Transactional
public void updateUser(User user) {
user.setAge(20);
this.updateById(user);
int i = 1 / 0;
}
}
法 3:AopContext 获取代理对象
1. 开启 AspectJ 动态代理
启动类加上:@EnableAspectJAutoProxy(exposeProxy = true)
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@MapperScan("com.example.demo.**.mapper")
@EnableAspectJAutoProxy(exposeProxy = true)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2. 导入 aspect 包
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
3. 使用 AopContext 获取当前代理对象
package com.example.demo.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.user.entity.User;
import com.example.demo.user.mapper.UserMapper;
import com.example.demo.user.service.UserService;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public void insertAndUpdate() {
User user = new User();
user.setName("Tony");
this.save(user);
UserServiceImpl userServiceImpl = (UserServiceImpl) AopContext.currentProxy();
userServiceImpl.updateUser(user);
}
@Transactional
public void updateUser(User user) {
user.setAge(20);
this.updateById(user);
int i = 1 / 0;
}
}
原理
Spring 的 AopProxy.java 通过调用 getProxy,获取代理,然后通过反射执行方法时传的是代理类。
目前 Spring 实现动态代理的方式有两种,一种是 cglib,一种是 jdk 的,两个的实现方式不一样,但是事务失效原因是一样的。
cglib 要实现代理,就要实现 MethodInterceptor 接口,例如 DynamicAdvisedInterceptor.java,最后通过反射执行方法时传的是目标类,不是代理类,也就是说我们通过 aop 执行 A 方法的时候,我们的通过反射调用的实例换成了目标类,这个就不会触发 Spring 的 aop 了。所以 B 方法的事务不会生效。
其他情景
失效原因 | 说明 |
只读事务 | 非只读事务才能回滚的,只读事务是不会回滚的 |
方法的权限修饰 | @Transactional 注解只能应用到 public 的方法上。 如果你在 protected、private 或 package 的方法上使用 @Transactional 注解,它不会报错,但事务会失效。 |
数据库引擎 | 如使用 mysql 且引擎是 MyISAM,则事务会不起作用,原因是 MyISAM 不支持事务,可以改成 InnoDB |
忘记配置 bean | spring 忘记配置扫描包, bean 不在 spring 容器管理下 |
切入点表达式书写错误 | 如果采用声明式事务,一定要确保切入点表达式书写正确 |
加载配置 | @Transactional 注解开启配置,必须放到 listener 里加载,如果放到 DispatcherServlet 的配置里,事务也是不起作用的。 |