spring事务失效和事务不回滚场景

spring事务失效的12种场景

引言

原文:
聊聊spring事务失效的12种场景,太坑了
读书笔记:担心大佬文章搬家,故整理此学习笔记


spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。


一 . 事务不生效

1. 访问权限问题

自定义的事务方法),它的访问权限不是public,而是privatedefaultprotected的话,spring则不会提供事务功能

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

2. 方法用final,static修饰

如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能;

@Service
public class UserService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

如果某个方法用static修饰了,同样无法通过动态代理,变成事务方法;(参考)

@Service
public class UserService {

    @Transactional
    public static void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

3. 方法内部调用

3.1 在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解是不会生效的

spring扫描bean的时候会扫描方法上是否包含@Transactional注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动transaction。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,所以就不会启动transaction,我们看到的现象就是@Transactional注解无效。(参考:Spring事务管理嵌套事务详解

3.1.1 事务方法之间的嵌套调用
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSomeThing();
    }
}

spring容器获取到的UserService对象实际是包装好的proxy对象,因此调用add方法的对象是动态代理对象。而在类内部add调用updateStatus的过程中,实质执行的代码是this.updateStatus,此处this对象是实际的serviceImpl对象而不是本该生成的代理对象,因此直接调用了updateStatus方法。所以updateStatus方法的@Transactional不生效。(参考:关于加@Transactional注解的方法之间调用,事务是否生效的问题

3.1.2 普通方法和事务方法之间的嵌套调用
@Service
class A{
    method a(){    //标记1
        b();
    }
    
    @Transactinal
    method b(){...}
}

伪代码表示代理类

//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法插入一个startTransaction()方法:
class proxy$A{
    A objectA = new A();
 
    method a(){    //标记3
        objectA.a();    //由于a()没有注解,所以不会启动transaction,而是直接调用A的实例的a()方法
    }
    
    method b(){    //标记2
        startTransaction();
        objectA.b();
    }
}

当我们调用A的bean的a()方法的时候,也是被proxy A ‘ 拦截 ‘ ,执行 ‘ p r o x y A`拦截`,执行`proxy A拦截,执行proxyA.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean`来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行。(参考:Spring事务管理嵌套事务详解

3.2 如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

3.2.1 把这两个方法分开到不同的类中
@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }
 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }
 }
3.2.2 在该Service类中注入自己
@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

答案:不会。(参考:spring:我是如何解决循环依赖的?

3.2.3 通过AopContent类
@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

4.未被spring管理

即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@Controller@Service@Repository@Component等注解可以自动实现bean实例化和依赖注入的功能。

//@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

亲测:此时编译不会报错,但是启动springBoot项目时会报错

5.多线程调用

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源value是数据库连接

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实是指同一个数据库连接只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。如上Demo的多线程导致add和doOtherThing两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常add方法也回滚不可能的

6.表不支持事务

mysql 数据库引擎myisam 不支持事务,某张表的事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

7.未开启事务

7.1 springBoot 项目

因为springBoot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的开启了事务。我们所要做的事情很简单,只需要配置spring.datasource相关参数即可。

7.2 传统的spring项目

需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。如果在pointcut标签中的切入点匹配规则配错了的话,有些类的事务也不会生效。

<!-- 配置事务管理器 --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把事务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

二. 事务不回滚

1.错误的传播特性

@Service
public class UserService {

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

2.自己吞了异常

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

3.手动抛了别的异常

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

因为spring事务,默认情况下会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

4.自定义了回滚异常

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

报错的异常不是BusinessException,事务不会回滚。

即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。
这是为什么呢?
因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:ExceptionThrowable

5.嵌套事务回滚多了

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。

why?

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

怎么样才能只回滚保存点呢?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

三 其他

1 大事务问题

如何避免大事务问题


2.编程式事务

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

相较于@Transactional注解声明式事务,建议使用基于TransactionTemplate的编程式事务。主要原因如下:

  • 避免由于spring aop问题,导致事务失效的问题。
  • 能够更小粒度的控制事务的范围,更直观。

如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis-Plus是一个基于MyBatis的增强工具,它简化了MyBatis的开发流程。在使用MyBatis-Plus进行事务管理时,可以使用Spring提供的@Transactional注解来声明事务,从而实现事务回滚。 要在MyBatis-Plus中实现事务回滚,你可以按照以下步骤进行操作: 1. 确保你的项目中已经集成了Spring框架和MyBatis-Plus。 2. 在需要进行事务管理的方法上添加@Transactional注解。该注解可以添加在类或方法上,用于声明事务边界。 3. 在方法执行过程中,如果发生异常或满足某些条件需要回滚事务,可以通过抛出RuntimeException或使用Spring提供的TransactionAspectSupport类手动回滚事务。 以下是一个示例代码片段,展示了如何在MyBatis-Plus中实现事务回滚: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void createUser(User user) { try { // 执行业务逻辑,可能会抛出异常 userMapper.insert(user); // 其他操作... } catch (Exception e) { // 发生异常时手动回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); throw e; } } } ``` 在上述示例中,通过@Transactional注解声明了一个事务边界。如果在createUser方法执行过程中发生异常事务将被回滚。 需要注意的是,事务回滚是根据异常类型来判断的。如果抛出的异常是RuntimeException或其子类,事务将会回滚。如果抛出的异常是非RuntimeException类型,事务将不会回滚,除非在@Transactional注解中指定了rollbackFor属性,将其设置为想要回滚异常类型。 希望以上信息对你有所帮助!如有更多疑问,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值