You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring @Transactional配合@Scheduled、Stream使用时事务不回滚如何解决?

Spring项目@Transactional注解配合@Scheduled/Stream操作不生效问题排查与解决

问题背景

在Spring项目中使用@Transactional注解时,发现该注解配合@Scheduled定时任务、Stream流操作使用时无法触发事务回滚,业务方法抛出异常后,已执行的持久化操作数据仍然被写入数据库。


相关代码

定时任务调度类

@Service
public class DocumentServiceScheduler {

    private final Path storageArchitasPath;

    @Autowired
    private DocumentService documentService;

    public DocumentServiceScheduler(final DocumentConfiguration documentConfiguration) {
        this.storageArchitasPath = documentConfiguration.storageArchitasPath();
    }

    @Scheduled(cron = "${document.scan.timer}")
    public void scanDocumentsTask() {
        this.scanDocuments();
    }

    public void scanDocuments() {
        final List<DocumentProcessDetail> report = new ArrayList<>();

        FileCoreUtils.loadPathFilesInDirectory(this.storageArchitasPath).forEach(fileName -> report.add(this.documentService.processDocument(fileName)));

        if (!report.isEmpty()) {
            final DataSource excelReport = FileCoreUtils.writeReportAsExcel(report);
            this.documentService.sendReportByEmail(excelReport);
        }
    }
}

业务实现类

@Service
public class DocumentServiceImpl implements DocumentService {

    private static Logger logger = LoggerFactory.getLogger(DocumentServiceImpl.class);

    private static String DOCUMENT_SEPARATOR = "#";

    private final Path storageArchitasPath;

    private final Path storageSystemPath;

    private final DocumentRepository documentRepository;

    private final DocumentTypeRepository documentTypeRepository;

    public DocumentServiceImpl(final DocumentRepository documentRepository, final DocumentTypeRepository documentTypeRepository, final DocumentConfiguration documentConfiguration) {
        this.documentRepository = documentRepository;
        this.documentTypeRepository = documentTypeRepository;
        this.storageArchitasPath = documentConfiguration.storageArchitasPath();
        this.storageSystemPath = documentConfiguration.storageSystemPath();
    }

    @Override
    @Transactional
    public DocumentProcessDetail processDocument(final Path fileName) {
        final Date uploadDate = new Date();

        try {
            final String[] splitFileName = fileName.getFileName().toString().split(DOCUMENT_SEPARATOR);
            String brokerId = splitFileName[0].trim();

            final StringBuilder error = this.validateDocumentFormat(splitFileName);

            if (error.length() != 0) {
                FileUtils.forceDelete(this.storageArchitasPath.resolve(fileName).toFile());
                return new DocumentProcessDetail(fileName.toString(), uploadDate, "Failure", error.toString());
            }

            if (splitFileName.length == 2) {
                if (this.deleteDocument(splitFileName)) {
                    return new DocumentProcessDetail(fileName.toString(), uploadDate, "Success", null);
                }

                FileUtils.forceDelete(this.storageArchitasPath.resolve(fileName).toFile());
                error.append("Couldn't delete file, not found in database");
                return new DocumentProcessDetail(fileName.toString(), uploadDate, "Failure", error.toString());

            }

            final String type = splitFileName[1].toUpperCase().trim();
            final String language = splitFileName[2].toUpperCase().trim();
            final String filename = splitFileName[3].trim();
            final String uuid = UUID.randomUUID().toString();
            final String newFileName = uuid + "." + FilenameUtils.getExtension(fileName.toString());

            final Path destination = this.generateStorageSystemPath(brokerId, newFileName);
            brokerId = brokerId.equalsIgnoreCase("all") ? null : brokerId;

            final Optional<DocumentTypeDB> documentTypeDB = this.documentTypeRepository.findById(DocumentType.valueOf(type.toUpperCase()));

            final DocumentDB document = new DocumentDB(filename, brokerId, documentTypeDB.get(), uploadDate, DocumentLanguage.valueOf(language), uuid);

            this.documentRepository.save(document);
            FileCoreUtils.moveFile(this.storageArchitasPath.resolve(fileName), destination);
            throw new Exception();
            //return new DocumentProcessDetail(uuid, fileName.toString(), uploadDate, "Success", null);
        } catch (final Exception e) {
            try {
                FileUtils.forceDelete(this.storageArchitasPath.resolve(fileName).toFile());
            } catch (final IOException e1) {
                logger.error("An unexpected error while deleting file: " + e.getStackTrace());
            }
            //TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            throw new RuntimeException();
            //return new DocumentProcessDetail(fileName.toString(), uploadDate, "failure", "An unexpected error occurred: " + e.getStackTrace());
        }
    }
}

Repository接口

@Repository
public interface DocumentRepository extends JpaRepository<DocumentDB, Long>, 
DocumentRepositoryCustom {

    DocumentDB findByDocumentReference(String reference);

}

环境说明

  • 数据库:MySQL 5.7
  • 持久层框架:Spring Data JPA

问题表现

  • processDocument方法抛出RuntimeException后,documentRepository.save操作的数据依然被持久化到数据库
  • 手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()强制回滚被忽略
  • 新增@EnableTransactionManagement注解无效果

启动自动配置评估日志如下:

==========================
CONDITION EVALUATION DELTA
==========================


Positive matches:
-----------------

    None


Negative matches:
-----------------

   TransactionAutoConfiguration.EnableTransactionManagementConfiguration:
      Did not match:
         - @ConditionalOnMissingBean (types: org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; SearchStrategy: all) found beans of type 'org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration' org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration (OnBeanCondition)

   TransactionAutoConfiguration.EnableTransactionManagementConfiguration.CglibAutoProxyConfiguration:
      Did not match:
         - Ancestor org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration did not match (ConditionEvaluationReport.AncestorsMatchedCondition)
      Matched:
         - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)



Exclusions:
-----------

    None


Unconditional classes:
----------------------

    None

补充测试验证

测试方法

@Transactional
public void testDoc() {
    final DocumentDB document = new DocumentDB("test", this.brokerId, new DocumentTypeDB(DocumentType.valueOf("TAX_FORMS")), new Date(), DocumentLanguage.valueOf("FRENCH"), "000");
    System.out.println("documentRepository.save called... start");
    this.documentRepository.save(document);
    System.out.println("documentRepository.save called... end");
}

事务执行日志

2021-08-18 15:32:26.486  INFO 7976 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@7be58f16 testClass = DocumentServiceImplTest, testInstance = be.architas.broker.portal.core.service.DocumentServiceImplTest@553f1d75, testMethod = testDoc@DocumentServiceImplTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@242aa8d9 testClass = DocumentServiceImplTest, locations = '{}', classes = '{class be.architas.broker.portal.core.BrokerPortalImplTestConfig}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1e4d3ce5, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@379614be, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@14fa86ae, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@398dada8, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4066c471, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@65466a6a], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4ebd0b52]; rollback [true]
documentRepository.save called... start
2021-08-18 15:32:26.651 TRACE 7976 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: 
    select
        documentty_.ID,
        documentty_.IS_OFFICIAL as is_offic2_1_,
        documentty_.RETENTION as retentio3_1_ 
    from
        DOCUMENT_TYPE documentty_ 
    where
        documentty_.ID=?
Hibernate: 
    insert 
    into
        DOCUMENT
        (BROKER_ID, DOCUMENT_REFERENCE, LANGUAGE, NAME, TYPE_ID, UPLOAD_DATE) 
    values
        (?, ?, ?, ?, ?, ?)
2021-08-18 15:32:26.757 TRACE 7976 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
documentRepository.save called... end
2021-08-18 15:32:26.771  INFO 7976 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@7be58f16 testClass = DocumentServiceImplTest, testInstance = be.architas.broker.portal.core.service.DocumentServiceImplTest@553f1d75, testMethod = testDoc@DocumentServiceImplTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@242aa8d9 testClass = DocumentServiceImplTest, locations = '{}', classes = '{class be.architas.broker.portal.core.BrokerPortalImplTestConfig}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@1e4d3ce5, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@379614be, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@14fa86ae, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@398dada8, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4066c471, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@65466a6a], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]

测试日志显示外层事务已经标记回滚,但Repository的save方法似乎拥有独立事务,执行完成后直接提交。


问题根源与解决方案

核心问题排查

  1. MySQL表引擎不支持事务
    MySQL 5.7默认创建表使用MyISAM引擎,该引擎不支持事务操作,即使上层代码配置了事务也无法回滚。
  • 验证方法:执行SQL SHOW CREATE TABLE DOCUMENT; 查看表引擎
  • 解决方法:执行SQL ALTER TABLE DOCUMENT ENGINE = InnoDB; 将表引擎修改为支持事务的InnoDB,所有涉及事务操作的表都需要同步修改。
  1. Repository方法配置了独立事务传播级别
    如果DocumentRepository或父接口JpaRepository上配置了@Transactional(propagation = Propagation.REQUIRES_NEW),会导致每次调用save方法时开启独立的新事务,外层事务回滚无法影响到已经提交的独立事务。
  • 解决方法:删除Repository层的独立事务配置,保持默认的Propagation.REQUIRED传播级别,让save方法加入外层业务方法的同一个事务。

其他辅助检查

  • 确认@Transactional注解的包是org.springframework.transaction.annotation.Transactional,不是javax.persistence包下的同名注解。
  • 多数据源场景下,需要给@Transactional显式指定对应数据源的事务管理器:@Transactional(transactionManager = "jpaTransactionManager")。
  • 如果需要回滚所有异常类型,可给注解添加属性rollbackFor = Exception.class。

内容的提问来源于stack exchange,提问作者bindCake

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 09:27:02