简介

        本文介绍Spring什么时候事务会失效以及如何解决。

        Spring通过AOP进行事务的控制,如果操作数据库报异常,则会进行回滚;如果没有报异常则会提交事务。但是,有时候Spring事务会失效,本文将介绍Spring的事务何时会失效,以及如何避免事务失效。

情景1:异常类型错误

        声明式事务和注解事务回滚的原理:当被切面切中或者是加了注解的方法中抛出了unchecked exception异常(默认情况)时,Spring会进行事务回滚。unchecked exception异常也就是:RuntimeException及其子类。

不回滚的情况


  1. 把异常给try catch了,没有手动抛出RuntimeException异常
  2. 抛出的异常不属于运行时异常(如IO异常),因为Spring默认情况下是捕获到运行时异常就回滚

会回滚的情况


  1. 用了try catch,在catch里面再抛出一个 RuntimeException异常。
  2. 将Spring默认的回滚时的异常修改为Exception
  1. 这样可以让非运行时异常也要能回滚
  1. 在catch后写回滚代码来实现回滚。

  1. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  2. 这样就可以在抛异常后也能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不应该有值)

Spring(SpringBoot)--事务失效--原因/场景/解决方案_事务

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 ...
......

事务调用事务

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,然后使用这个bean来调用方法。有以下三种方法:


  1. @Autowired注入自己
  2. 通过ApplicationContext 获得自己
  3. 通过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方法的事务不会生效。

Spring(SpringBoot)--事务失效--原因/场景/解决方案_java_02

其他情景


失效原因



说明



只读事务



非只读事务才能回滚的,只读事务是不会回滚的



方法的权限修饰



@Transactional 注解只能应用到 public 的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。



数据库引擎



如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB



忘记配置bean



spring忘记配置扫描包,bean不在spring容器管理下



切入点表达式书写错误



如果采用声明式事务,一定要确保切入点表达式书写正确



加载配置



@Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。


其他网址

《深入浅出SpringBoot2.x》=> 6.5 @Transactional自调用失效问题