Spring事务管理:@Transactional注解失效和回滚失败的13种常见场景及解决方案

在Spring应用开发中,@Transactional注解是实现数据库事务管理的关键工具,它能够简化事务操作,确保数据一致性。然而,如果使用不当,@Transactional注解可能会失效或无法正确回滚事务,导致数据错误甚至系统崩溃。本文将深入探讨@Transactional注解失效和回滚失败的13种常见场景,并提供相应的解决方案,帮助开发者避免这些陷阱,构建更健壮的应用程序。

图片

在需要事务管理的方法上添加@Transactional注解是一个良好的编程习惯。但是,许多开发者只是机械地添加这个注解,在功能正常运行后很少会深入验证异常情况下事务是否能够正确回滚。@Transactional注解虽然使用简单,但在一些意想不到的情况下会失效,令人防不胜防!

我们将这些事务问题归纳为三类:不必要不生效不回滚,接下来将通过一些示例演示每种场景。

一、不必要使用@Transactional注解的场景

1. 没有数据库操作的业务方法

在仅包含查询操作或HTTP请求等不涉及数据库操作的业务方法上使用@Transactional注解,虽然不会造成严重影响,但从编码规范的角度来看不够严谨,建议移除。

@Transactional  
public String testQuery() {  
    standardBak2Service.getById(1L);  
    return "testB";  
}

2. 事务范围过大

一些开发者为了方便,直接将@Transactional注解添加到类级别或抽象类级别,这会导致类中所有方法或抽象类的所有实现类方法都被事务管理,增加不必要的性能开销或系统复杂性。建议根据实际需求,仅在需要事务管理的方法上添加@Transactional注解。

@Transactional  
public abstract class BaseService {  
}  
  
@Slf4j  
@Service  
public class TestMergeService extends BaseService {  
  
    private final TestAService testAService;  
  
    public String testMerge() {  
        testAService.testA();  
        return "ok";  
    }  
}

如果在类中某个方法上也添加了@Transactional注解,它将覆盖类级别的事务配置。例如,如果类级别配置了只读事务,方法级别上的@Transactional注解会覆盖该配置,启用读写事务。

@Transactional(readOnly = true)  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
  
    @Transactional  
    public String testMerge() {  
        testAService.testA();  
        testBService.testB();  
        return "ok";  
    }  
} 

二、@Transactional注解不生效的场景

3. 方法访问权限问题

避免将@Transactional注解添加到私有方法上!

@Transactional注解依赖于Spring AOP切面来增强事务行为,而AOP是通过代理机制实现的。私有方法无法被代理,因此AOP对私有方法的增强无效,@Transactional注解也无法生效。

@Transactional  
private String testMerge() {  
    testAService.testA();  
    testBService.testB();  
    return "ok";  
}

如果在testMerge()方法内部调用私有方法,事务是否会生效呢?

答案是:事务会生效。

@Transactional  
public String testMerge() throws Exception {  
    ccc();  
    return "ok";  
}  
  
private void ccc() {  
    testAService.testA();  
    testBService.testB();  
}

4. 被finalstatic修饰的方法

与上述原因类似,在被finalstatic修饰的方法上添加@Transactional注解也不会生效。

  • static静态方法属于类本身,而不是实例,因此代理机制无法对静态方法进行代理或拦截。
  • final修饰的方法不能被子类重写,事务相关的逻辑无法插入到final方法中,代理机制无法对final方法进行拦截或增强。

这些都是Java基础概念,使用时需要注意。

@Transactional  
public static void b() {  
}  
  
@Transactional  
public final void b() {  
}

5. 同类内部方法调用问题

这种情况非常常见!

同类内部方法间的调用是@Transactional注解失效的重灾区。有一种说法是,在方法内部调用另一个同类的方法时,这种调用不会经过代理,因此事务管理不会生效。但这说法不够全面,需要具体情况具体分析。

例如:testMerge()方法开启事务,调用同类的非事务方法a()b(),此时b()抛出异常,根据事务的传播性,a()b()的事务都会生效。

@Transactional  
public String testMerge() {  
    a();  
    b();  
    return "ok";  
}  
  
public void a() {  
    standardBakService.save(testAService.buildEntity());  
}  
  
public void b() {  
    standardBak2Service.save(testBService.buildEntity2());  
    throw new RuntimeException("b error");  
}

如果testMerge()方法未开启事务,并在同类中调用了非事务方法a()和事务方法b(),当b()抛出异常时,a()b()的事务都不会生效。因为这种调用直接通过this对象进行,未经过代理,因此事务管理无法生效。这经常导致问题!

public String testMerge() {  
    a();  
    b();  
    return "ok";  
}  
  
public void a() {  
    standardBakService.save(testAService.buildEntity());  
}  
  
@Transactional  
public void b() {  
    standardBak2Service.save(testBService.buildEntity2());  
    throw new RuntimeException("b error");  
}

5.1 独立的Service类

要使b()方法的事务生效,最简单的方法是将其剥离到一个独立的Service类中,并注入使用,交给Spring管理。不过,这种方式会创建很多类。

@Slf4j  
@Service  
public class TestBService {  
  
    @Transactional  
    public void b() {  
        standardBak2Service.save(testBService.buildEntity2());  
        throw new RuntimeException("b error");  
    }  
}

5.2 自注入方式

或者通过自注入的方式解决,虽然解决了问题,但逻辑看起来很奇怪,它破坏了依赖注入的原则。虽然Spring支持这种用法,但要注意循环依赖的问题。

@Slf4j  
@Service  
public class TestMergeService {  
    @Autowired  
    private TestMergeService testMergeService;  
  
    public String testMerge() {  
        a();  
        testMergeService.b();  
        return "ok";  
    }  
  
    public void a() {  
        standardBakService.save(testAService.buildEntity());  
    }  
  
    @Transactional  
    public void b() {  
        standardBak2Service.save(testBService.buildEntity2());  
        throw new RuntimeException("b error");  
    }  
}

5.3 手动获取代理对象

b()方法之所以没有被代理,我们可以手动获取代理对象来调用b()方法。通过AopContext.currentProxy()方法返回当前的代理对象实例,这样调用代理的方法时,就会经过AOP的切面,@Transactional注解就会生效了。

@Slf4j  
@Service  
public class TestMergeService {  
  
    public String testMerge() {  
        a();  
        ((TestMergeService) AopContext.currentProxy()).b();  
        return "ok";  
    }  
  
    public void a() {  
        standardBakService.save(testAService.buildEntity());  
    }  
  
    @Transactional  
    public void b() {  
        standardBak2Service.save(testBService.buildEntity2());  
        throw new RuntimeException("b error");  
    }  
}

6. Bean未被Spring管理

前面提到,@Transactional注解通过AOP来管理事务,而AOP依赖于代理机制。因此,Bean必须由Spring管理实例化! 要确保为类添加@Controller@Service@Component等注解,使其被Spring管理,这一点很容易被忽视。

@Service  
public class TestBService {  
  
    @Transactional  
    public String testB() {  
        standardBak2Service.save(entity2);  
        return "testB";  
    }  
}

7. 异步线程调用

如果我们在testMerge()方法中使用异步线程执行事务操作,通常也无法成功回滚。

testMerge()方法在事务中调用了testA()testA()方法中开启了事务。接着,在testMerge()方法中,我们通过一个新线程调用了testB()testB()中也开启了事务,并且在testB()中抛出了异常。

此时的回滚情况是怎样的呢?

@Transactional  
public String testMerge() {  
    testAService.testA();  
    new Thread(() -> {  
        try {  
            testBService.testB();  
        } catch (Exception e) {  
            throw new RuntimeException();  
        }  
    }).start();  
    return "ok";  
}  
  
@Transactional  
public String testB() {  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    dataImportJob2Service.save(entity2);  
    throw new RuntimeException("test2");  
}  
  
@Transactional  
public String testA() {  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}

答案是:testA()不回滚,testB()回滚。

testA()无法回滚是因为没有捕获到新线程中testB()抛出的异常;testB()方法可以回滚,是因为事务管理器只对当前线程中的事务有效,因此在新线程中执行的事务会回滚。

在多线程环境下,Spring的事务管理器不会跨线程传播事务,事务的状态(如事务是否已开启)存储在线程本地的ThreadLocal中,用于存储和管理事务上下文信息。这意味着每个线程都有一个独立的事务上下文,事务信息在不同线程之间不会共享。

8. 不支持事务的数据库引擎

不支持事务的数据库引擎不在本文讨论范围内,只需了解即可。我们通常使用的关系型数据库,如MySQL,默认使用支持事务的InnoDB引擎,而MyISAM引擎则较少使用。

以前启用MyISAM引擎是为了提高查询效率。但现在,非关系型数据库如RedisMongoDBElasticsearch等中间件提供了更高效的解决方案。

三、@Transactional注解事务不回滚的场景

9. 错误使用事务传播属性

@Transactional注解有一个重要的参数propagation,它控制着事务的传播行为。有时,事务传播参数配置错误也会导致事务不回滚。

propagation支持7种事务传播特性:

  • REQUIRED默认的传播行为,如果当前没有事务,则创建一个新事务;如果存在事务,则加入当前事务。
  • MANDATORY:支持当前事务,如果不存在则抛出异常。
  • NEVER:以非事务方式执行,如果存在事务,则抛出异常。
  • REQUIRES_NEW:无论当前是否存在事务,都会创建一个新事务,原有事务被挂起。
  • NESTED:嵌套事务,被调用方法在一个嵌套的事务中运行,这个事务依赖于当前的事务。
  • SUPPORTS:如果当前存在事务,则加入;如果没有,就以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,将其挂起。

为了加深理解,我们将通过案例模拟每种特性的使用场景。

REQUIRED

REQUIRED是默认的事务传播行为。如果testMerge()方法开启了事务,那么其内部调用的testA()testB()方法也将加入这个事务。如果testMerge()没有开启事务,而testA()testB()方法上使用了@Transactional注解,这些方法将各自创建新的事务,只控制自身的回滚。

@Component  
@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
  
    @Transactional  
    public String testMerge() {  
        testAService.testA();  
        testBService.testB();  
        return "ok";  
    }  
}  
  
@Transactional  
public String testA() {  
    log.info("testA");  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}  
  
@Transactional  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    throw new RuntimeException("testB");  
}

MANDATORY

MANDATORY传播特性简单来说就是只能被开启事务的上层方法调用。例如,testMerge()方法未开启事务调用testB()方法,那么将抛出异常;testMerge()开启事务调用testB()方法,则加入当前事务。

@Component  
@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
  
    public String testMerge() {  
        testAService.testA();  
        testBService.testB();  
        return "ok";  
    }  
}  
  
@Transactional  
public String testA() {  
    log.info("testA");  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}  
  
@Transactional(propagation = Propagation.MANDATORY)  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    throw new RuntimeException("testB");  
}

抛出的异常信息:

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

NEVER

NEVER传播特性是强制你的方法只能以非事务方式运行,如果方法存在事务操作会抛出异常,我实在想不到有什么使用场景。

@Transactional(propagation = Propagation.NEVER)  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    return "ok";  
}

抛出的异常信息:

org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

REQUIRES_NEW

我们在使用Propagation.REQUIRES_NEW传播特性时,不论当前事务的状态如何,调用该方法都会创建一个新的事务。

例如,testMerge()方法开始一个事务,调用testB()方法时,它会暂停testMerge()的事务,并启动一个新的事务。如果testB()方法内部发生异常,新事务会回滚,但原先挂起的事务不会受影响。这意味着,挂起的事务不会因为新事务的回滚而受到影响,也不会因为新事务的失败而回滚。

@Transactional  
public String testMerge() {  
    testAService.testA();  
    testBService.testB();  
    return "ok";  
}  
  
@Transactional  
public String testA() {  
    log.info("testA");  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}  
  
@Transactional(propagation = Propagation.REQUIRES_NEW)  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    throw new RuntimeException("testB");  
}

NESTED

方法的传播行为设置为NESTED时,其内部方法会开启一个新的嵌套事务(子事务)。在没有外部事务的情况下,NESTEDREQUIRED效果相同;存在外部事务的情况下,一旦外部事务回滚,它会创建一个嵌套事务(子事务)。

也就是说,外部事务回滚时,子事务会跟着回滚;但子事务的回滚不会对外部事务和其他同级事务造成影响。

@Component  
@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
  
    @Transactional  
    public String testMerge() {  
        testAService.testA();  
        testBService.testB();  
        throw new RuntimeException("testMerge");  
        return "ok";  
    }  
}  
  
@Transactional  
public String testA() {  
    log.info("testA");  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}  
  
@Transactional(propagation = Propagation.NESTED)  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    throw new RuntimeException("testB");  
}

NOT_SUPPORTED

NOT_SUPPORTED事务传播特性表示该方法必须以非事务方式运行。当方法testMerge()开启事务并调用事务方法testA()testB()时,如果testA()testB()的事务传播特性为NOT_SUPPORTED,那么testB()将以非事务方式运行,并挂起当前的事务。

默认传播特性的情况下,testB()异常事务加入会导致testA()回滚,而挂起的意思是说,testB()其内部一旦抛出异常,不会影响testMerge()中其他testA()方法的回滚。

@Component  
@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
  
    @Transactional  
    public String testMerge() {  
        testAService.testA();  
        testBService.testB();  
        return "ok";  
    }  
}  
  
@Transactional  
public String testA() {  
    log.info("testA");  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}  
  
@Transactional(propagation = Propagation.NOT_SUPPORTED)  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    throw new RuntimeException("testB");  
}

SUPPORTS

如果当前方法的事务传播特性是SUPPORTS,那么只有在调用该方法的上层方法开启了事务的情况下,该方法的事务才会有效。如果上层方法没有开启事务,那么该方法的事务特性将无效。

例如,如果入口方法testMerge()没有开启事务,而testMerge()调用的方法testA()testB()的事务传播特性为SUPPORTS,那么由于testMerge()没有事务,testA()testB()将以非事务方式执行。即使在这些方法上加上@Transactional注解,也不会回滚异常。

@Component  
@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
  
    public String testMerge() {  
        testAService.testA();  
        testBService.testB();  
        return "ok";  
    }  
}  
  
@Transactional(propagation = Propagation.SUPPORTS)  
public String testA() {  
    log.info("testA");  
    DeepzeroStandardBak entity = buildEntity();  
    standardBakService.save(entity);  
    return "ok";  
}  
  
@Transactional(propagation = Propagation.SUPPORTS)  
public String testB() {  
    log.info("testB");  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    standardBak2Service.save(entity2);  
    throw new RuntimeException("testB");  
}

10. 自身吞掉异常

在代码审查和网络观点收集过程中,我发现导致事务不回滚的最常见原因是开发者在业务代码中手动使用try...catch捕获了异常,然后没有重新抛出异常....

例如:testMerge()方法开启了事务,并调用了非事务方法testA()testB(),同时在testMerge()中捕获了异常。如果testB()中发生了异常并抛出,但testMerge()捕获了这个异常而没有继续抛出,Spring事务将无法捕获到异常,从而无法进行回滚。

@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
    
    @Transactional  
    public String testMerge() {  
        try {  
            testAService.testA();  
            testBService.testB();  
        } catch (Exception e) {  
            log.error("testMerge error:{}", e);  
        }  
        return "ok";  
    }  
}  
  
@Service  
public class TestAService {  
    public String testA() {  
        standardBakService.save(entity);  
        return "ok";  
    }  
}  
  
@Service  
public class TestBService {  
    public String testB() {  
        standardBakService.save(entity2);  
        throw new RuntimeException("test2");  
    }  
}

为了确保Spring事务能够正常回滚,我们需要在catch块中主动重新抛出RuntimeExceptionError类型的异常,以便Spring事务能够捕获并处理。

@Transactional  
public String testMerge() {  
    try {  
        testAService.testA();  
        testBService.testB();  
    } catch (Exception e) {  
        log.error("testMerge error:{}", e);  
        throw new RuntimeException(e);  
    }  
    return "ok";  
}

捕获异常并不意味着一定不会回滚,这取决于具体情况。

例如,当testB()方法上也加上了@Transactional注解时,如果在该方法中发生异常,事务会捕获到这个异常。由于事务传播的特性,testB()的事务会合并到上层方法的事务中。因此,即使在testMerge()中捕获了异常而未抛出,事务仍然可以成功回滚。

@Transactional  
public String testB() {  
    DeepzeroStandardBak2 entity2 = buildEntity2();  
    dataImportJob2Service.save(entity2);  
    throw new RuntimeException("test2");  
}

但这有一个前提,必须在testMerge()方法上添加@Transactional注解以启用事务。如果testMerge()方法没有开启事务,不论其内部是否使用try块,都只能部分回滚testB(),而testA()将无法回滚。

11. 事务无法捕获的异常

Spring的事务默认会回滚RuntimeException及其子类,以及Error类型的异常。

如果抛出的是其他类型的异常,例如checked exceptions(检查型异常),即继承自Exception但不继承自RuntimeException的异常,比如SQLExceptionDuplicateKeyException,事务将不会回滚。

因此,我们在主动抛出异常时,要确保该异常是事务能够捕获的类型。

@Transactional  
public String testMerge() throws Exception {  
    try {  
        testAService.testA();  
        testBService.testB();  
    } catch (Exception e) {  
        log.error("testMerge error:{}", e);  
        throw new Exception(e);  
    }  
    return "ok";  
}

如果你必须抛出默认情况下不会导致事务回滚的异常,需要在@Transactional注解的rollbackFor参数中明确指定该异常,这样才能进行回滚。

@Transactional(rollbackFor = Exception.class)  
public String testMerge() throws Exception {  
    try {  
        testAService.testA();  
        testBService.testB();  
    } catch (Exception e) {  
        log.error("testMerge error:{}", e);  
        throw new Exception(e);  
    }  
    return "ok";  
}

问问你身边的同事,哪些异常属于运行时异常,哪些属于检查型异常,十有八九他们可能无法给出准确的回答!

为了降低出现bug的风险,建议在使用@Transactional注解时,将rollbackFor参数设置为ExceptionThrowable,这样可以扩大事务回滚的范围。

12. 自定义异常范围问题

针对不同业务定制异常类型是很常见的做法,@Transactional注解的rollbackFor参数支持自定义的异常,但我们往往习惯于将这些自定义异常继承自RuntimeException

那么这就出现和上面同样的问题,事务的范围不足,许多异常类型仍然无法触发事务回滚。

@Transactional(rollbackFor = CustomException.class)  
public String testMerge() throws Exception {  
    try {  
        testAService.testA();  
        testBService.testB();  
    } catch (Exception e) {  
        log.error("testMerge error:{}", e);  
        throw new Exception(e);  
    }  
    return "ok";  
}

要解决这个问题,可以在catch块中主动抛出我们自定义的异常。

@Transactional(rollbackFor = CustomException.class)  
public String testMerge() throws Exception {  
    try {  
        testAService.testA();  
        testBService.testB();  
    } catch (Exception e) {  
        log.error("testMerge error:{}", e);  
        throw new CustomException(e);  
    }  
    return "ok";  
}

13. 嵌套事务问题

还有一种场景是嵌套事务问题,例如,我们在testMerge()方法中调用了事务方法testA()和事务方法testB(),此时不希望testB()抛出异常导致整个testMerge()都回滚;这就需要单独try...catch处理testB()的异常,阻止异常向上抛出。

@RequiredArgsConstructor  
@Slf4j  
@Service  
public class TestMergeService {  
  
    private final TestBService testBService;  
  
    private final TestAService testAService;  
    
    @Transactional  
    public String testMerge() {  
        testAService.testA();  
        try {  
            testBService.testB();  
        } catch (Exception e) {  
            log.error("testMerge error:{}", e);  
        }  
        return "ok";  
    }  
}  
  
@Service  
public class TestAService {  
  
    @Transactional  
    public String testA() {  
        standardBakService.save(entity);  
        return "ok";  
    }  
}  
  
@Service  
public class TestBService {  
  
    @Transactional  
    public String testB() {  
        standardBakService.save(entity2);  
        throw new RuntimeException("test2");  
    }  
}

总结

以上关于@Transactional注解的使用注意事项是在代码审查和参考网络观点后整理总结的。