找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4130

积分

0

好友

545

主题
发表于 3 天前 | 查看: 17| 回复: 0

对于从事Java开发工作的同学而言,Spring 事务无疑是日常开发中最为熟悉的特性之一。

在许多业务场景中,一个请求可能需要同时写入多张表。为了保证这些操作的原子性——即要么全部成功,要么全部失败,从而避免数据不一致的问题——我们通常会启用 Spring 事务。

确实,Spring 事务的使用非常便捷,仅需一个简单的 @Transactional 注解就能轻松管理事务。相信这也是大多数开发者的首选方式,并且一直用得很顺畅。

然而,如果使用不当,这个看似简单的注解也会在无形中埋下隐患。今天,我们就来深入探讨事务失效的一些典型场景,或许其中就有你已经踩过的坑。

Spring事务失效的12种场景思维导图

一 事务不生效

1.访问权限问题

众所周知,Java 的访问权限主要有四种:private、default、protected、public,它们的访问级别从左到右依次增大。

在开发过程中,如果错误地定义了事务方法的访问权限,就会导致事务功能失效,例如:

@Service
public class UserService {

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

上述代码中,add 方法的访问权限被定义成了 private,这将直接导致事务失效。Spring 明确要求被代理的方法必须是 public 的。

究其根源,在 AbstractFallbackTransactionAttributeSource 类的 computeTransactionAttribute 方法中有一个关键判断:如果目标方法不是 public,则 TransactionAttribute 返回 null,意味着不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don‘t allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  }

简单来说,如果我们自定义的事务方法(即目标方法)的访问权限不是 public,而是 private、default 或 protected,Spring 将不会为其提供事务功能。

2. 方法用 final 修饰

有时,为了防止某个方法被子类重写,我们会将其定义为 final。对于普通方法而言,这样做没有问题,但如果一个事务方法被定义成 final,例如:

@Service
public class UserService {

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

如上所示,add 方法被 final 修饰,这同样会导致事务失效。

为什么?如果你了解 Spring 事务的底层原理,就会知道它依赖于 AOP,即通过 JDK 动态代理或 CGLib 生成代理类,并在代理类中实现事务增强。

但如果一个方法被 final 修饰,那么在它的代理类中就无法重写该方法,自然也就无法为其添加事务功能。

注意:如果一个方法是 static 的,同样无法通过动态代理将其转变为事务方法。

3.方法内部调用

有时候,我们需要在某个 Service 类的一个方法中,调用同一个类内的另一个事务方法,例如:

@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) {
        doSameThing();
    }
}

在事务方法 add 中,直接调用了另一个事务方法 updateStatus。根据前面的介绍,updateStatus 方法之所以拥有事务能力,是因为 Spring AOP 为其生成了代理对象。但这里的调用方式(this.updateStatus())直接调用了原始对象的方法,绕过了代理,因此 updateStatus 方法中的事务不会生效。

由此可见,在同一个类中,方法之间的直接内部调用会导致被调用方法的事务失效。

那么问题来了,如果某些场景确实需要在同一个类的某个方法中调用自身的另一个方法,并且希望被调方法的事务生效,该怎么办呢?

3.1 新加一个 Service 方法

这是一个非常直接的方法:新创建一个 Service 类(或使用现有的另一个 Service 类),将 @Transactional 注解加在新 Service 的方法上,然后将需要事务执行的代码移到这个新方法中。具体代码如下:

@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 在该 Service 类中注入自己

如果不想新增 Service 类,也可以选择在当前 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 IOC 内部的三级缓存机制保证了不会出现循环依赖问题。

3.3 通过 AopContext 类

还可以通过 AopContext.currentProxy() 获取当前类的代理对象,然后通过代理对象进行调用。具体代码如下:

@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();
    }
 }

注意:使用此方法需要在启动类或配置类上添加 @EnableAspectJAutoProxy(exposeProxy = true) 注解。

4.未被 spring 管理

在日常开发中,有一个细节极易被忽略:使用 Spring 事务的前提是,对象必须被 Spring 容器管理,即需要创建 Bean 实例。

通常,我们通过 @Controller@ServiceComponent@Repository 等注解,可以自动实现 Bean 的实例化和依赖注入。

当然,创建 Bean 实例的方法还有很多。但如果匆忙开发了一个 Service 类,却忘了添加 @Service 注解,例如:

//@Service
public class UserService {

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

从上面的例子可以看到,UserService 类没有加 @Service 注解,因此该类不会被 Spring 容器管理,它的 add 方法自然也不会生成事务。

5.多线程调用

在实际项目开发中,多线程的使用场景相当常见。那么,Spring 事务用在多线程环境中,会出现问题吗?

@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表数据”);
    }
}

上例中,事务方法 add 调用了事务方法 doOtherThing,但 doOtherThing 是在另一个新线程中执行的。

这会导致两个方法不在同一个线程中,它们获取到的数据库连接也不同,因此属于两个独立的事务。此时,即使 doOtherThing 方法中抛出异常,add 方法的事务也不会回滚。

如果你看过 Spring 事务的源码,就会知道 Spring 事务是通过数据库连接来实现的。当前线程中通过 ThreadLocal 保存了一个 Map,key 是数据源,value 是数据库连接。

private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>(“Transactional resources”);

我们常说的“同一个事务”,其实是指同一个数据库连接,只有持有同一个数据库连接,才能实现同时提交和回滚。在不同的线程中,获取到的数据库连接肯定不同,因此它们属于不同的事务。

6.表不支持事务

众所周知,在 MySQL 5 之前,默认的数据库引擎是 MyISAM。它的优点显而易见:索引文件和数据文件分开存储,对于读多写少的单表操作,性能比 InnoDB 更优。一些老项目中可能还在使用它。

在创建表时,只需要将 ENGINE 参数设置为 MyISAM 即可:

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

MyISAM 虽然有其优点,但有一个致命的缺陷:不支持事务

如果只是单表操作,问题可能不大。但如果涉及跨多张表的操作,由于不支持事务,数据极有可能出现不一致的情况。此外,MyISAM 还不支持行锁和外键。

因此,在实际业务场景中,MyISAM 已经很少使用。自 MySQL 5 以后,InnoDB 逐渐成为主流引擎。

提醒:有时在开发中发现某张表的事务始终不生效,那不一定是 Spring 事务的配置问题,最好先确认一下你使用的数据库表,其存储引擎是否支持事务。

7.未开启事务

有时,事务没有生效的根本原因竟然是:没有开启事务

你可能会觉得好笑,开启事务不是一个项目最基本的功能吗?为什么还会没有开启?

没错,对于一个已经搭建好的成熟项目,事务功能肯定是具备的。但如果你是正在搭建一个项目 Demo,只涉及一张表,而这张表的事务没有生效。那么,可能的原因有很多,而“未开启事务”这一点极其容易被忽略。

如果你使用的是 Spring Boot 项目,那么很幸运。因为 Spring Boot 通过 DataSourceTransactionManagerAutoConfiguration 类,已经默默地帮你开启了事务。你只需要配置好 spring.datasource 相关参数即可。

但如果你使用的是传统的 Spring 项目,则需要在 applicationContext.xml 文件中,手动配置事务相关参数。一旦忘记配置,事务肯定不会生效。具体配置如下:

<!-- 配置事务管理器 -->
<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>

另外需要注意,如果 pointcut 标签中的切入点表达式匹配规则配置错误,也会导致某些类的事务不生效。

二 事务不回滚

1.错误的传播特性

在使用 @Transactional 注解时,我们可以指定 propagation 参数。该参数用于定义事务的传播行为,Spring 目前支持 7 种传播特性:

  • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。这是默认值。
  • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • REQUIRES_NEW:创建一个新事务,如果当前存在事务,则把当前事务挂起。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。
  • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  • NESTED:如果当前存在事务,则创建一个嵌套事务作为当前事务的子事务来运行;如果当前没有事务,则创建一个新事务。

如果手动设置 propagation 参数时设置错误,例如:

@Service
public class UserService {

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

我们将 add 方法的事务传播特性设置成了 Propagation.NEVER。这种传播特性明确表示“不支持事务”,如果存在事务则会抛出异常。实际上,只有 REQUIREDREQUIRES_NEWNESTED 这三种传播特性才会创建新事务。

2.自己吞了异常

事务不回滚,一个最常见的原因是开发者在代码中手动进行了 try...catch 处理,却没有重新抛出异常。例如:

@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.手动抛了别的异常

即使开发者没有手动捕获异常,但如果抛出的异常类型不正确,Spring 事务同样可能不会回滚。

@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);
        }
    }
}

在上面的情况中,开发人员捕获了异常,却又手动抛出了一个 Exception(非运行时异常)。默认情况下,Spring 事务只会回滚 RuntimeException(运行时异常)和 Error(错误)。对于普通的受检异常(Exception),它不会自动触发回滚。

4.自定义了回滚异常

使用 @Transactional 注解时,我们可以通过 rollbackFor 参数来自定义触发回滚的异常类型。这是一个很有用的功能,但如果该参数的值设置不当,就会导致一些莫名其妙的问题。

@Slf4j
@Service
public class UserService {

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

如果执行上述代码时,保存或更新数据过程中程序报错,抛出了 SQLExceptionDuplicateKeyException 等异常。由于我们定义的 rollbackFor = BusinessException.class,而抛出的异常并不属于 BusinessException,因此事务不会回滚。

虽然 rollbackFor 有默认值,但阿里巴巴的 Java 开发手册中,仍建议开发者显式指定该参数。这是为什么呢?

因为如果使用默认值(即只回滚 RuntimeException 和 Error),一旦程序抛出了受检异常(Exception),事务将不会回滚,这可能造成严重的业务逻辑错误。因此,通常建议将该参数设置为 Exception.classThrowable.class,以确保所有异常都能触发回滚。

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表数据”);
    }
}

这里使用了嵌套事务(Propagation.NESTED)。原本的期望是:当调用 roleService.doOtherThing 方法时,如果出现异常,只回滚 doOtherThing 方法内部的操作(即回滚到保存点),而不回滚外层 add 方法中 userMapper.insertUser 的操作。

但事实是,insertUser 的操作也被回滚了。

原因在于:doOtherThing 方法抛出的异常没有被内部捕获,它会继续向上传播,直到被外层 add 方法的代理方法捕获。此时,Spring 会回滚整个事务,而不仅仅是嵌套事务内部的保存点。

那么,如何才能实现只回滚嵌套事务(保存点)呢?可以将内部嵌套事务的调用放在 try/catch 块中,并且不在 catch 块中继续向上抛出异常。

@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);
        }
    }
}

这样就能保证,即使内部嵌套事务出现异常,也只会回滚该嵌套事务,而不会影响外部事务的提交。

三 其他

1 大事务问题

在使用 Spring 事务时,还有一个非常令人头疼的问题:大事务问题

通常,我们会在方法上添加 @Transactional 注解来为整个方法添加事务功能,例如:

@Service
public class UserService {

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}

@Service
public class RoleService {

    @Autowired
    private RoleService roleService;

    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

@Transactional 注解加在方法级别的一个缺点是:整个方法中的所有代码都包含在同一个事务中

在上面的例子中,UserService.add() 方法里,实际上只有这两行才需要事务:

roleService.save(userModel);
update(userModel);

RoleService.save() 方法中,可能只有这一行需要事务:

saveData(userModel);

而当前的写法,会导致所有的 query 查询方法也被包裹在同一个长事务中。如果查询方法众多、调用层级深,且部分查询比较耗时,就会导致整个事务的执行时间过长,从而引发大事务问题。

大事务可能引发的六大问题思维导图

2.编程式事务

以上讨论的内容都是基于 @Transactional 注解的,我们称之为 声明式事务

实际上,Spring 还提供了另一种创建事务的方式,即通过手动编写代码来实现,我们称之为 编程式事务。例如:

   @Autowired
   private TransactionTemplate transactionTemplate;

   ...

   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) -> {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

在 Spring 中,为了支持编程式事务,专门提供了 TransactionTemplate 类。在其 execute 方法中,实现了事务的开启、提交/回滚逻辑。

相较于 @Transactional 注解的声明式事务,我更推荐大家使用基于 TransactionTemplate 的编程式事务,主要原因如下:

  1. 避免因 Spring AOP 机制导致的事务失效问题:编程式事务直接调用,不依赖代理。
  2. 控制粒度更细,意图更清晰:可以精确控制哪一段代码需要运行在事务中,代码逻辑一目了然。

建议:在项目中尽量减少使用 @Transactional 注解开启事务。但这并非绝对禁止,如果业务逻辑简单且稳定,使用 @Transactional 注解可以提高开发效率。但务必警惕本文提到的各种事务失效场景,更多实战经验与解决方案欢迎到云栈社区交流探讨。




上一篇:Spring @Transactional 注解失效的6种场景深度剖析与避坑指南
下一篇:线程池异常静默消失?探究控制台不打日志的背后原因
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 10:06 , Processed in 0.580777 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表