所有分类
  • 所有分类
  • 未分类

Spring事务失效–原因/解决方案

简介

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

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

情景1:异常类型错误

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

不回滚的情况

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

会回滚的情况

  1. 用了try catch,在catch里面再抛出一个 RuntimeException异常。
  2. 将Spring默认的回滚时的异常修改为Exception
    1. 这样可以让非运行时异常也要能回滚
  3. 在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不应该有值)

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来调用方法,这样就能走代理。有以下三种方法:

  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方法的事务不会生效。

其他情景

失效原因说明
只读事务非只读事务才能回滚的,只读事务是不会回滚的
方法的权限修饰@Transactional 注解只能应用到 public 的方法上。 如果你在 protected、private 或 package 的方法上使用 @Transactional 注解,它不会报错,但事务会失效。
数据库引擎如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB
忘记配置beanspring忘记配置扫描包,bean不在spring容器管理下
切入点表达式书写错误如果采用声明式事务,一定要确保切入点表达式书写正确
加载配置@Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。
2

评论2

请先

  1. 有个问题请教下,事务不回滚是生效还是失效?或者说如果正常执行完毕不回滚是生效,碰到异常没能回滚是失效?
    CJCChester 2023-07-31 0
    • 1.事务不回滚:事务失效。2.正常情况(也就是生效):异常时要回滚事务。
      自学精灵 2023-07-31 1
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录