关于使用多线程批量插入,获取异步执行结果时一直阻塞的问题记录!

现象:

        做个数据迁移功能,先清空表,然后批量插入,由于插入的数据量太大,同步执行太慢,虽然使用了mybatis plus 的savebatch然后也优化了些但是还有点差距,所以需要使用多线程进行批量分批插入操作!具体做法如下

@Transaction(roollback=Exception.class)
public void saveBatchRoot(){
    List<RootDto> rootList = rootRepo.list();
    if(CollectionUtils.isEmpty(rootList){return;}
    List<CompletableFuture<Void>> listFuture = new ArrayList<>();
    rootRepo.truncate();
    Integer size = rootList/3;
    for(int i=0;i<5;i++){
        //五个线程处理,先不用自定义线程池了直接用默认的
        Integer num = i;
        CompletableFuture<Void> voidFuture = CompletableFuture.runAsync(()->{
            rootRepo.saveBatch(rootList.subList(num*size,(num+1)*size>rootList.size?rootList.size:(num+1)*size;
            log.info("执行完成,当前线程{}",Thread.currentThread().getName());
        });
       listFutrue.add(voidFuture);         
    }
    CompleatbleFuture.allof(listFuture.toArray(new CompletableFuture[0])).join();
    log.info("执行完");
}

执行时候,在执行savebatch的时候就卡死了,查看数据库表死锁了;

后查资料分析原因大致是

主线程开启了事务,并且对单表进行了truncate,然后使用future开启多个子线程进行insert,最后使用future.get()方法阻塞,导致死锁。在这种情况下,可能由于在主线程中进行truncate操作后直接使用阻塞的`future.get()`方法导致死锁。主线程开启了事务并对单表进行了truncate会导致表被锁定,而子线程需要插入数据时也会尝试获取同一表的锁,由于主线程持有该表的锁并且在等待子线程执行完成时被阻塞,子线程无法获取到所需的锁从而导致死锁的发生。

解决这个问题的方法可以是调整主线程的执行顺序,避免在truncate操作后直接调用`future.get()`方法阻塞主线程。可以考虑在主线程truncate操作后提交事务,然后再启动子线程进行insert操作,最后在获取子线程的结果之前再阻塞主线程。这样可以避免死锁的发生,确保表在不同线程间正常地共享。

另外,在使用多线程编程时,需要注意避免多个线程之间出现资源竞争的情况,合理设计线程之间的协作方式,以及合理地使用事务来保证数据的完整性和一致性。

后来一想在主线程truncate操作后提交事务,启动子线程进行insert操作,那如果insert出现异常 如何保证整个事务进行回滚,truncate已经提交了啊,又查资料:

在这种情况下,可以考虑使用数据库事务的保存点(Savepoint)来解决这个问题。在主线程进行truncate操作之前设置一个保存点,然后在truncate操作之后提交事务。接着在子线程开始insert操作时,如果其中任何一个insert操作发生异常,可以通过回滚到之前设置的保存点来回滚整个事务,包括truncate操作。

具体步骤如下:
1. 在主线程开始事务之前设置一个保存点。
2. 在主线程执行truncate操作后提交事务。
3. 启动子线程进行insert操作,如果其中任何一个insert操作发生异常,则回滚事务至之前设置的保存点。

这样可以保证在insert操作发生异常时整个事务可以回滚,包括truncate操作。使用保存点可以更加细粒度地控制事务的回滚范围,确保数据的一致性和完整性。需要注意的是,在使用保存点回滚时需要确保数据库支持保存点的功能。

那如何使用保存点呢:

当涉及复杂的事务处理时,确保数据的一致性和完整性通常是至关重要的。下面是一个简单的示例,演示了如何在主线程开启事务、truncate表并使用保存点保存事务状态,然后在子线程中插入数据时出现异常时回滚整个事务的过程。

```java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Savepoint;

public class Main {
    private static final String URL = "jdbc:mysql://localhost:3306/database";
    private static final String USER = "username";
    private static final String PASSWORD = "password";

    public static void main(String[] args) {
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
            conn.setAutoCommit(false); // 设置手动提交事务

            Savepoint savepoint = conn.setSavepoint(); // 设置保存点

            // 在主线程中进行truncate操作
            truncateTable(conn);

            // 提交truncate操作后 savepoint仍然可用
            conn.commit();

            // 启动子线程插入数据
            Thread insertThread = new Thread(() -> {
                try {
                    insertData(conn);
                    conn.commit(); // 提交子线程的插入操作
                } catch (SQLException e) {
                    try {
                        conn.rollback(savepoint); // 回滚至保存点
                    } catch (SQLException rollbackException) {
                        rollbackException.printStackTrace();
                    }
                }
            });
            insertThread.start();
            insertThread.join(); // 等待子线程执行完成
        } catch (SQLException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void truncateTable(Connection conn) throws SQLException {
        String sql = "TRUNCATE TABLE table_name";
        try (PreparedStatement statement = conn.prepareStatement(sql)) {
            statement.executeUpdate();
        }
    }

    private static void insertData(Connection conn) throws SQLException {
        // 在这里模拟插入数据时发生异常
        String sql = "INSERT INTO table_name (column1, column2) VALUES (?, ?)";
        try (PreparedStatement statement = conn.prepareStatement(sql)) {
            statement.setString(1, "value1");
            statement.setString(2, "value2");
            statement.executeUpdate();

            // 模拟插入数据时发生异常
            throw new SQLException("Simulated insert exception");
        }
    }
}
```

在这个示例中,主线程先设置一个保存点,然后进行truncate操作并提交事务。在子线程中插入数据时模拟了一个异常情况,如果插入异常发生,子线程会捕获异常并通过回滚到保存点来回滚整个事务。这样可以保证在插入数据出现异常时整个事务得到回滚,包括truncate操作。

除了这个方法我们还有哪些思路:

除了使用保存点来保证事务回滚之外,还有其他一些解决方案来处理主线程先truncate表,然后多线程大批量进行insert操作的情况。以下是其中一些可能的解决方法:

1. **使用数据库连接池**:确保多线程中每个线程都使用独立的数据库连接,避免多线程共享同一个连接导致的竞争和死锁问题。通过数据库连接池管理连接,可以更好地控制连接的分配和释放,避免资源争夺。

2. **分阶段提交事务**:在多线程insert操作时,可以将数据分批提交到数据库,每个批次插入完成后提交事务,而不是等到整个插入过程完成再提交事务。这样可以减少事务持有锁的时间,降低死锁的可能性。

3. **使用队列**:主线程truncate表后,将需要插入的数据放入一个队列中,然后子线程从队列中取出数据进行插入。这样可以避免子线程直接操作数据库时出现的竞争问题,并且可以控制插入的速率以避免资源争夺。

4. **监控和处理异常**:在子线程中进行insert操作时,实时监控异常情况,并及时处理异常。可以针对不同的异常情况采取不同的处理方式,比如记录异常信息、重试操作或者回滚事务。这样可以最大程度地保证数据的一致性。

5. **优化数据库设计**:考虑优化表结构、索引设计和SQL查询语句,以减少数据库操作的时间和锁定的资源,提高并发能力。

通过综合采取上述方法,可以更好地处理主线程truncate表后多线程insert操作可能导致的死锁和竞争问题,确保数据的完整性和一致性。当涉及复杂的多线程数据库操作时,细致的设计和合理的处理异常是确保系统稳定性和数据完整性的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值