How to Ensure Transaction Rollback Across Asynchronous Threads in Spring

This article explains how to guarantee that when any asynchronous thread fails during a large Excel import, the main thread can roll back all related transactions, covering the limitations of @Transactional and @Async, thread‑local propagation, and practical solutions using Future, CompletableFuture, custom ForkJoinPool, and manual transaction management.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
How to Ensure Transaction Rollback Across Asynchronous Threads in Spring

1. Background

In the previous article we imported 100,000 rows of Excel using a double‑asynchronous method. A reader asked how to ensure transaction safety and batch processing.

Original requirement: read a 100k‑row Excel

Serial reading took 191 seconds per file.

Optimization 1

Read Excel with POI and EasyExcel and insert into the database.

Discuss core thread‑pool size settings.

Through many tests, using a thread pool to parallelize insertion yields the best efficiency.

Optimization 2

Obtain asynchronous return values via Future, compare them with Excel rows to verify data accuracy.

Analyzed FutureTask source and drew its execution flow diagram.

Analyzed get() source and drew its execution flow diagram.

Discovered that Future.get() blocks the main thread.

Optimization 3

Java 8 introduced CompletableFuture, which upgrades Future and allows callbacks to obtain asynchronous results. CompletableFuture runs on ForkJoinPool, using daemon threads. ForkJoinPool splits a task into many subtasks, runs them on multiple CPUs, and merges the results.

Use CompletableFuture to replace Future for async return values.

Compare efficiency of CompletableFuture vs Future.

Custom ForkJoinPool thread pool.

With the same core thread count, CompletableFuture inserts about 4 seconds faster for 100k rows.

Use CompletableFuture.allOf to avoid main‑thread blocking.

Summarize the syntactic sugar of CompletableFuture.

2. Rollback all async threads when one fails

To guarantee transaction, use @Transactional.

Scenario: import several large Excel files, each with its own table, so we only need a transaction per Excel.

Current approach uses async batch read and insert.

Thus we have one main‑thread transaction plus many sub‑thread transactions; if any sub‑thread throws an exception, we must roll back the whole Excel transaction.

Adding @Transactional on both main and sub threads does not work because sub‑thread exceptions only roll back their own transaction.

Example code that simulates an exception on the last batch:

if(end == sheet.getLastRowNum()){
    logger.info("Insert last batch, simulate exception");
    int a = 1/0;
}

3. @Transactional annotation

Declarative transaction management is built on AOP; it intercepts method entry/exit to start or join a transaction and commit or roll back based on outcome.

In short, @Transactional rolls back the transaction when code throws an exception.

Add @EnableTransactionManagement on the startup class.

When placed on a class, all public methods inherit the transaction attributes; method‑level annotation can override.

Using @Transactional(rollbackFor=Exception.class) makes the transaction roll back for checked exceptions as well.

If rollbackFor is not set, only RuntimeException triggers rollback; setting rollbackFor=Exception.class expands it.

1. @Transactional

When a RuntimeException occurs without try‑catch, the program aborts and the database rolls back.

If the exception is caught and re‑thrown, the program aborts and rolls back.

If caught without re‑throw, the program continues; database inserts inside the try‑catch may fail, but surrounding inserts succeed, so no rollback occurs.

2. @Transactional(rollbackFor = Exception.class)

Setting rollbackFor=Exception.class ensures rollback for non‑runtime exceptions as well.

4. Annotation failure cases

1. Applying @Transactional to non‑public methods

Transaction interceptor only reads annotations on public methods; non‑public methods are ignored.

2. Incorrect rollbackFor configuration

rollbackFor

specifies which exception types trigger rollback.

3. Self‑invocation within the same class

When a public method A calls another method B in the same class, B's @Transactional is ignored because the call bypasses the Spring proxy.

4. Catching exceptions manually

If you catch an exception and do not re‑throw, the transaction manager assumes a normal commit, so the annotation appears ineffective.

5. Adding Future return value and transaction

1. Add transaction

@Transactional(rollbackFor = Exception.class)
public void readXls(String filePath, String filename) throws Exception {
    try {
        // ... complex operations ...
        List<Future<Integer>> futureList = new ArrayList<>();
        for (int i = 0; i < times; i++) {
            Future<Integer> sumFuture = readExcelDataAsyncFutureService.readXlsCacheAsyncMybatis();
            futureList.add(sumFuture);
        }
        boolean futureFlag = getFutureResult(futureList, excelRow);
        if (futureFlag) {
            logger.info("readXlsCacheAsync---insert success, commit transaction");
        } else {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            logger.info("readXlsCacheAsync---insert failed, rollback transaction");
        }
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        logger.error("readXlsCacheAsync---insert exception, rollback transaction:", e);
    }
}
@Async("async-executor")
@Override
public Integer readXlsCacheAsyncMybatis() {
    try {
        // ... complex operations ...
    } catch (Exception e) {
        throw new RuntimeException("Insert DB exception", e);
    }
}

(1) Transaction + no async

If insertion fails, transaction rolls back successfully.

(2) Transaction + async

Rollback fails.

2. Manual transaction

public void readXls(String filePath, String filename) throws Exception {
    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
    try {
        // ... complex operations ...
        List<Future<Integer>> futureList = new ArrayList<>();
        for (int i = 0; i < times; i++) {
            Future<Integer> sumFuture = readExcelDataAsyncFutureService.readXlsCacheAsyncMybatis();
            futureList.add(sumFuture);
        }
        boolean futureFlag = getFutureResult(futureList, excelRow);
        if (futureFlag) {
            dataSourceTransactionManager.commit(transactionStatus);
            logger.info("readXlsCacheAsync---insert success, commit transaction");
        } else {
            dataSourceTransactionManager.rollback(transactionStatus);
            logger.info("readXlsCacheAsync---insert failed, rollback transaction");
        }
    } catch (Exception e) {
        dataSourceTransactionManager.rollback(transactionStatus);
        logger.error("readXlsCacheAsync---insert exception, rollback transaction:", e);
    }
}
@Async("async-executor")
@Override
public Integer readXlsCacheAsyncMybatis() {
    try {
        // ... complex operations ...
    } catch (Exception e) {
        throw new RuntimeException("Insert DB exception", e);
    }
}

(1) Transaction + no async

Rollback works.

(2) Future + manual transaction, rollback fails

6. @Async + @Transactional transaction failure

Requirement: when any async thread fails, the main thread rolls back all async transactions.

Both @Async and @Transactional are implemented via Spring AOP interceptors; @Async adds AnnotationAsyncExecutionInterceptor, @Transactional adds TransactionInterceptor.

Spring’s transaction propagation uses ThreadLocal, which is thread‑private, so it cannot cross thread boundaries.

7. Transaction propagation cannot cross threads

1. One async thread per transaction, then commit/rollback together?

2. Core code

private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
public void setUserService(DataSourceTransactionManager dataSourceTransactionManager) {
    this.dataSourceTransactionManager = dataSourceTransactionManager;
}
@Override
public void readXls(String filePath, String filename) {
    List<TransactionStatus> transactionStatusList = Collections.synchronizedList(new ArrayList<>());
    List<TransactionResource> transactionResourceList = Collections.synchronizedList(new ArrayList<>());
    try {
        List<Future<Integer>> futureList = new ArrayList<>();
        for (int i = 0; i < times; i++) {
            Future<Integer> sumFuture = readAsyncFutureTransactionDBService.readXlsCacheAsyncMybatis(..., transactionStatusList, transactionResourceList);
            futureList.add(sumFuture);
        }
        boolean futureFlag = getFutureResult(futureList, excelRow);
        if (futureFlag) {
            for (TransactionStatus ts : transactionStatusList) {
                dataSourceTransactionManager.commit(ts);
            }
            logger.info("readXlsCacheAsync---insert success, commit transaction");
        } else {
            for (TransactionStatus ts : transactionStatusList) {
                dataSourceTransactionManager.rollback(ts);
            }
            logger.info("readXlsCacheAsync---insert failed, rollback transaction");
            throw new RuntimeException("readXlsCacheAsync---insert exception, rollback");
        }
    } catch (Exception e) {
        logger.error("readXlsCacheAsync---insert exception, rollback:", e);
        for (TransactionStatus ts : transactionStatusList) {
            dataSourceTransactionManager.rollback(ts);
        }
        throw new RuntimeException("readXlsCacheAsync---insert exception, rollback", e);
    }
}

3. Async thread class

@Async("async-executor")
@Override
public Future<Integer> readXlsCacheAsyncMybatis(XSSFSheet sheet, XSSFRow row, int start, int end,
        StringBuilder insertBuilder, List<TransactionStatus> transactionStatusList,
        List<ReadAsyncFutureTransactionServiceImpl.TransactionResource> transactionResourceList) throws Exception {
    DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);
    transactionStatusList.add(transactionStatus);
    transactionResourceList.add(ReadAsyncFutureTransactionServiceImpl.TransactionResource.copyTransactionResource());
    try {
        // insert logic
    } catch (Exception e) {
        throw new RuntimeException("readXlsCacheAsyncMybatis async read Excel and insert DB exception", e);
    }
}

4. Transaction copy class

static class TransactionResource {
    private Map<Object, Object> resources;
    private Set<TransactionSynchronization> synchronizations;
    private String currentTransactionName;
    private Boolean currentTransactionReadOnly;
    private Integer currentTransactionIsolationLevel;
    private Boolean actualTransactionActive;

    public static TransactionResource copyTransactionResource() {
        return TransactionResource.builder()
                .resources(TransactionSynchronizationManager.getResourceMap())
                .synchronizations(new LinkedHashSet<>())
                .currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
                .currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
                .currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
                .actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive())
                .build();
    }

    public void autoWiredTransactionResource() {
        resources.forEach(TransactionSynchronizationManager::bindResource);
        TransactionSynchronizationManager.initSynchronization();
        TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
        TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
    }

    public void removeTransactionResource() {
        resources.keySet().forEach(key -> {
            if (!(key instanceof DataSource)) {
                TransactionSynchronizationManager.unbindResource(key);
            }
        });
    }
}

5. Why use transaction copy class?

Without copying, transaction resources are not transferred to async threads, leading to exceptions during commit or rollback.

8. Summary

After extensive effort, the problem “when an async thread fails, the main thread rolls back all async thread transactions” is solved, concluding the double‑async import series.

Adding transactions effectively controls the accuracy of asynchronous Excel data insertion.

Best solution for reading a 100k‑row Excel

Read with EasyExcel asynchronously.

Use Future to obtain async results and compare row counts for consistency.

Execute with CompletableFuture + custom ForkJoinPool to avoid main‑thread blocking.

Set per‑thread row count based on core thread number for optimal efficiency.

Apply manual transaction, one thread per transaction, and copy transaction resources to achieve effective async transaction control.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaspringExcel Importasynchronous programmingtransaction-management
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.