Java多线程数据库事务提交控制

项目代码基于:MySql 数据,开发框架为:SpringBoot、Mybatis

开发语言为:Java8

项目代码:https://gitee.com/john273766764/springboot-mybatis-threads

前言

        公司业务中遇到一个需求,需要同时修改最多约5万条数据,而且还不支持批量或异步修改操作。于是只能写个for循环操作,但操作耗时太长,只能一步一步寻找其他解决方案。

        具体操作如下:

一、循环操作的代码

        先写一个最简单的for循环代码,看看耗时情况怎么样。

 /***
     * 一条一条依次对50000条数据进行更新操作
     * 耗时:2m27s,1m54s
     */
    @Test
    void updateStudent() {
        List<Student> allStudents = studentMapper.getAll();
        allStudents.forEach(s -> {
            //更新教师信息
            String teacher = s.getTeacher();
            String newTeacher = "TNO_" + new Random().nextInt(100);
            s.setTeacher(newTeacher);
            studentMapper.update(s);
        });
    }

        循环修改整体耗时约 1分54秒,且代码中没有手动事务控制应该是自动事务提交,所以每次操作事务都会提交所以操作比较慢,我们先对代码中添加手动事务控制,看查询效率怎样。

二、使用手动事务的操作代码

        修改后的代码如下:

 @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;
 
    @Autowired
    private TransactionDefinition transactionDefinition;
 
    /**
     * 由于希望更新操作 一次性完成,需要手动控制添加事务
     * 耗时:24s
     * 从测试结果可以看出,添加事务后插入数据的效率有明显的提升
     */
    @Test
    void updateStudentWithTrans() {
        List<Student> allStudents = studentMapper.getAll();
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        try {
            allStudents.forEach(s -> {
                //更新教师信息
                String teacher = s.getTeacher();
                String newTeacher = "TNO_" + new Random().nextInt(100);
                s.setTeacher(newTeacher);
                studentMapper.update(s);
            });
            dataSourceTransactionManager.commit(transactionStatus);
        } catch (Throwable e) {
            dataSourceTransactionManager.rollback(transactionStatus);
            throw e;
        }
    }

        添加手动事务操控制后,整体耗时约 24秒,这相对于自动事务提交的代码,快了约5倍,对于大量循环数据库提交操作,添加手动事务可以有效提高操作效率。

三、尝试多线程进行数据修改

        添加数据库手动事务后操作效率有明细提高,但还是比较长,接下来尝试多线程提交看是不是能够再快一些。

        先添加一个Service将批量修改操作整合一下,具体代码如下:

@Service
public class StudentServiceImpl implements StudentService {
    @Autowired
    private StudentMapper studentMapper;
 
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;
 
    @Autowired
    private TransactionDefinition transactionDefinition;
 
    @Override
    public void updateStudents(List<Student> students, CountDownLatch threadLatch) {
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        System.out.println("子线程:" + Thread.currentThread().getName());
        try {
            students.forEach(s -> {
                // 更新教师信息
                // String teacher = s.getTeacher();
                String newTeacher = "TNO_" + new Random().nextInt(100);
                s.setTeacher(newTeacher);
                studentMapper.update(s);
            });
            dataSourceTransactionManager.commit(transactionStatus);
            threadLatch.countDown();
        } catch (Throwable e) {
            e.printStackTrace();
            dataSourceTransactionManager.rollback(transactionStatus);
        }
    }
}

        多线程提交修改时,我们尝试了不同线程数对提交速度的影响,具体可以看下面表格,

多线程修改50000条数据时 不同线程数耗时对比(秒)
线程数12510100500100020005000
耗时25s14s15s15s15s17s19s23s29s

        根据表格,我们线程数增大提交速度并非一直增大,在当前情况下约在2-5个线程数时,提交速度最快(实际线程数还是需要根据服务器配置实际测试)

四、基于两个CountDownLatch控制多线程事务提交

        由于多线程提交时,每个线程事务时单独的,无法保证一致性,我们尝试给多线程添加事务控制,来保证每个线程都是在插入数据完成后在提交事务,

        这里我们使用两个 CountDownLatch 来控制主线程与子线程事务提交,并设置了超时时间为 30 秒。我们对代码进行了一点修改:

 @Override
    public void updateStudentsThread(List<Student> students, CountDownLatch threadLatch, CountDownLatch mainLatch, StudentTaskError taskStatus) {
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        System.out.println("子线程:" + Thread.currentThread().getName());
        try {
            students.forEach(s -> {
                // 更新教师信息
                // String teacher = s.getTeacher();
                String newTeacher = "TNO_" + new Random().nextInt(100);
                s.setTeacher(newTeacher);
                studentMapper.update(s);
            });
        } catch (Throwable e) {
            taskStatus.setIsError();
        } finally {
            threadLatch.countDown(); // 切换到主线程执行
        }
        try {
            mainLatch.await();  //等待主线程执行
        } catch (Throwable e) {
            taskStatus.setIsError();
        }
        // 判断是否有错误,如有错误 就回滚事务
        if (taskStatus.getIsError()) {
            dataSourceTransactionManager.rollback(transactionStatus);
        } else {
            dataSourceTransactionManager.commit(transactionStatus);
        }
    }
  /**
     * 由于每个线程都是单独的事务,需要添加对线程事务的统一控制
     * 我们这边使用两个 CountDownLatch 对子线程的事务进行控制
     */
    @Test
    void updateStudentWithThreadsAndTrans() {
        //查询总数据
        List<Student> allStudents = studentMapper.getAll();
        // 线程数量
        final Integer threadCount = 4;
 
        //每个线程处理的数据量
        final Integer dataPartionLength = (allStudents.size() + threadCount - 1) / threadCount;
 
        // 创建多线程处理任务
        ExecutorService studentThreadPool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch threadLatchs = new CountDownLatch(threadCount); // 用于计算子线程提交数量
        CountDownLatch mainLatch = new CountDownLatch(1); // 用于判断主线程是否提交
        StudentTaskError taskStatus = new StudentTaskError(); // 用于判断子线程任务是否有错误
 
        for (int i = 0; i < threadCount; i++) {
            // 每个线程处理的数据
            List<Student> threadDatas = allStudents.stream()
                    .skip(i * dataPartionLength).limit(dataPartionLength)
                    .collect(Collectors.toList());
            studentThreadPool.execute(() -> {
                studentService.updateStudentsThread(threadDatas, threadLatchs, mainLatch, taskStatus);
            });
        }
        try {
            // 倒计时锁设置超时时间 30s
            boolean await = threadLatchs.await(30, TimeUnit.SECONDS);
            if (!await) { // 等待超时,事务回滚
                taskStatus.setIsError();
            }
        } catch (Throwable e) {
            e.printStackTrace();
            taskStatus.setIsError();
        }
        mainLatch.countDown(); // 切换到子线程执行
        studentThreadPool.shutdown(); //关闭线程池
 
        System.out.println("主线程完成");
    }

        本想再次测试一下不同线程数对执行效率的影响时,发现当线程数超过10个时,执行时就报错。具体错误内容如下:

Exception in thread "pool-1-thread-2" org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30055ms.
	at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:309)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)
	at com.example.springbootmybatis.service.Impl.StudentServiceImpl.updateStudentsThread(StudentServiceImpl.java:58)
	at com.example.springbootmybatis.StudentTest.lambda$updateStudentWithThreadsAndTrans$3(StudentTest.java:164)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30055ms.
	at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
	at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128)
	at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:265)
	... 7 more

        错误的大致意思时,不能为数据库事务打开 jdbc Connection,连接在30s的时候超时了。由于前面启动的十个线程需要等待主线程完成后才能提交,所以一直占用连接未释放,造成后面的进程创建连接超时。

        看错误日志中错误的来源是 HikariPool ,我们来重新配置一下这个连接池的参数,将最大连接数修改为100,具体配置如下:

# 连接池中允许的最小连接数。缺省值:10
spring.datasource.hikari.minimum-idle=10
# 连接池中允许的最大连接数。缺省值:10
spring.datasource.hikari.maximum-pool-size=100
# 自动提交
spring.datasource.hikari.auto-commit=true
# 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟
spring.datasource.hikari.idle-timeout=30000
# 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒
spring.datasource.hikari.max-lifetime=1800000
# 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒

        再次执行测试发现没有报错,修改线程数为20又执行了一下,同样执行成功了。

五、基于TransactionStatus集合来控制多线程事务提交

        在同事推荐下我们使用事务集合来进行多线程事务控制,主要代码如下

@Service
public class StudentsTransactionThread {
 
    @Autowired
    private StudentMapper studentMapper;
    @Autowired
    private StudentService studentService;
    @Autowired
    private PlatformTransactionManager transactionManager;
 
    List<TransactionStatus> transactionStatuses = Collections.synchronizedList(new ArrayList<TransactionStatus>());
 
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
    public void updateStudentWithThreadsAndTrans() throws InterruptedException {
 
        //查询总数据
        List<Student> allStudents = studentMapper.getAll();
 
        // 线程数量
        final Integer threadCount = 2;
 
        //每个线程处理的数据量
        final Integer dataPartionLength = (allStudents.size() + threadCount - 1) / threadCount;
 
        // 创建多线程处理任务
        ExecutorService studentThreadPool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch threadLatchs = new CountDownLatch(threadCount);
        AtomicBoolean isError = new AtomicBoolean(false);
        try {
            for (int i = 0; i < threadCount; i++) {
                // 每个线程处理的数据
                List<Student> threadDatas = allStudents.stream()
                        .skip(i * dataPartionLength).limit(dataPartionLength).collect(Collectors.toList());
                studentThreadPool.execute(() -> {
                    try {
                        try {
                            studentService.updateStudentsTransaction(transactionManager, transactionStatuses, threadDatas);
                        } catch (Throwable e) {
                            e.printStackTrace();
                            isError.set(true);
                        }finally {
                            threadLatchs.countDown();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        isError.set(true);
                    }
                });
            }
 
            // 倒计时锁设置超时时间 30s
            boolean await = threadLatchs.await(30, TimeUnit.SECONDS);
            // 判断是否超时
            if (!await) {
                isError.set(true);
            }
        } catch (Throwable e) {
            e.printStackTrace();
            isError.set(true);
        }
 
        if (!transactionStatuses.isEmpty()) {
            if (isError.get()) {
                transactionStatuses.forEach(s -> transactionManager.rollback(s));
            } else {
                transactionStatuses.forEach(s -> transactionManager.commit(s));
            }
        }
 
        System.out.println("主线程完成");
    }
}
@Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
    public void updateStudentsTransaction(PlatformTransactionManager transactionManager, List<TransactionStatus> transactionStatuses, List<Student> students) {
        // 使用这种方式将事务状态都放在同一个事务里面
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); // 事物隔离级别,开启新事务,这样会比较安全些。
        TransactionStatus status = transactionManager.getTransaction(def); // 获得事务状态
        transactionStatuses.add(status);
 
        students.forEach(s -> {
            // 更新教师信息
            // String teacher = s.getTeacher();
            String newTeacher = "TNO_" + new Random().nextInt(100);
            s.setTeacher(newTeacher);
            studentMapper.update(s);
        });
        System.out.println("子线程:" + Thread.currentThread().getName());
    }

        由于这个中方式去前面方式相同,需要等待线程执行完成后才会提交事务,所有任会占用Jdbc连接池,如果线程数量超过连接池最大数量会产生连接超时。所以在使用过程中任要控制线程数量,

六、使用union连接多个select实现批量update

        有些情况写不支持,批量update,但支持insert 多条数据,这个时候可尝试将需要更新的数据拼接成多条select 语句,然后使用union 连接起来,再使用update 关联这个数据进行update,具体代码演示如下:

update student,(
	(select  1 as id,'teacher_A' as teacher) union
	(select  2 as id,'teacher_A' as teacher) union
	(select  3 as id,'teacher_A' as teacher) union
	(select  4 as id,'teacher_A' as teacher)
    /* ....more data ... */
    ) as new_teacher
set
	student.teacher=new_teacher.teacher
where
	student.id=new_teacher.id

        这种方式在Mysql 数据库没有配置 allowMultiQueries=true 也可以实现批量更新


总结

  • 对于大批量数据库操作,使用手动事务提交可以很多程度上提高操作效率
  • 多线程对数据库进行操作时,并非线程数越多操作时间越快,按上述示例大约在2-5个线程时操作时间最快。
  • 对于多线程阻塞事务提交时,线程数量不能过多。
  • 如果能有办法实现批量更新那是最好
  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Java作为一门编程语言,拥有强大的多线程支持,通过利用Java线程技术,可以有效地解决在数据库插入千万级数据时的性能瓶颈问题。 但是,在多线程插入数据库时,需要注意以下几点: 1.连接数限制 数据库通常会有连接数限制,过多的连接会导致数据库性能下降,通过使用连接池可以有效地控制连接数。 2.事务处理 在大数据量插入时,一次性插入可能会出现异常,这时候需要使用数据库事务管理技术,可以回滚错误的数据,确保数据的完整性。 3.线程数控制 线程数是需要根据机器的配置和系统负载情况合理调整的,过多的线程会浪费资源,过少的线程会导致性能下降。 4.数据批量提交 将需要插入的数据进行分批处理,进行批量提交,可以减少数据库交互次数,提高性能。 综上所述,通过合理利用Java线程技术以及上述注意事项,在数据库插入千万级数据时,可以大大提高系统的性能,确保数据的有效插入。 ### 回答2: Java线数据库插入千万级数据是一项比较复杂的任务。在此之前需要确保数据库的表结构已经建立好,并且在写入数据时需要进行数据分析和处理,尽可能地优化数据的结构和存储方式。 为了提高插入数据的效率,可以采用多线程方式进行数据库插入操作。多线程技术可以将数据分批处理,每个线程负责一部分数据的插入。这样可以有效地提高数据的插入速度和效率。但同时也需要考虑到线程之间的同步问题,以及数据库的并发访问能力。 在进行多线数据库插入时,需要注意以下几点: 1. 数据库连接池的使用:为了减少数据库连接和关闭的开销,可以采用数据库连接池的方式管理数据库连接。 2. 多线程的线程池:可以使用线程池来管理线程的数量,以控制系统资源的使用和避免过度消耗内存和CPU等资源。 3. 分批插入数据:可以将数据分成若干个批次进行插入,每个线程负责一部分数据的插入,这样可以减少单个事务的大小和对数据库的负载。 4. 数据库的并发控制:需要使用数据库的并发控制机制,以避免多个线程同时访问数据库时出现死锁等问题。 5. 数据结构和数据处理优化:需要对插入的数据进行预处理和优化,以充分利用数据库的性能优势,例如采用批量插入等方式。 总之,要在Java线数据库插入千万级数据时,需要对数据结构、线程安全和并发访问等方面进行深入的思考和优化,以达到最佳的性能和效率。 ### 回答3: Java线程对于数据库的插入千万级数据是一个非常明智的选择。使用多线程可以将数据插入过程分成多个小块,每个小块都可以被单独处理,从而加快整个过程,提高插入数据的效率。 以下是 Java线数据库插入千万级数据的一些重要步骤和技巧: 1. 数据分片:在将数据插入到数据库之前,需要将数据划分成多个小块。这些小块中的数据可以并行插入到数据库中,这样可以提高插入数据的速度。 2. 在每个小块中使用多线程:在每个小块中,可以使用多个线程同时进行数据插入。这样可以充分利用所有可用资源,从而提高整个过程的效率。 3. 使用连接池:连接池可以帮助我们管理数据库连接,并提供连接复用、线程安全等功能。这样可以减少连接创建和销毁的开销,缩短数据插入时间。 4. 使用 batch 方式插入数据:在插入数据时,可以使用 batch 方式批量插入数据,这样可以减少与数据库的交互次数,从而提高插入数据的效率。 5. 优化 SQL 语句:在插入数据时,需要使用高效的 SQL 语句。例如,可以使用批量插入语句、预处理语句、存储过程等方式,减少 SQL 语句的执行时间和开销。 6. 监控和优化:在数据插入过程中,需要实时监控系统资源的使用情况,及时检测并调整线程数、连接池大小等参数,以保证系统的稳定性和高效性。 7. 数据库性能优化:对于千万级数据的插入,数据库的性能优化也非常重要。例如,可以优化数据库的索引、表分区、缓存等,以提高数据库的查询和插入效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值