使用多线程处理大数据量传输的尝试

遇到的问题

​ 在最近工作中遇到了一个千万级数据量从数据库导出到excel中去的业务,由于考虑到内存溢出的问题,所以使用的方案是10万条数据放在一个excel中,讲所有excel的文件存储路径存在一个list中,最后将所有的excel文件放入一个压缩包中,返回给页面。

​ 在最后进行性能测试时无法满足要求,于是想到了使用多线程的方式来进行性能优化,以下是多线程方面的一些尝试。

多线程的一些尝试

1 使用线程池+callable

首先想到的是使用callable的方式,因为业务在进行操作结束后,需要将excel的存储路径返回,所以先想到了callable的方式进行处理,(可以使用,但是要注意防止主线程阻塞)代码如下:

首先创建了callable实现类:

package com.codebull;

import java.util.Random;
import java.util.concurrent.Callable;

/**
 * @Author: CodeBull
 * @Date: 2021/6/8
 * @version: 1.0
 */
public class CallableImpl implements Callable<String> {
    /** 线程名称 */
    private String name;
    /** 开始页码 */
    private int pageNum;
    /** 结束页码 */
    private int count;

    public CallableImpl(String name, int pageNum, int end) {
        this.name = name;
        this.pageNum = pageNum;
        this.count = end;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStart() {
        return pageNum;
    }

    public void setStart(int start) {
        this.pageNum = start;
    }

    public int getEnd() {
        return count;
    }

    public void setEnd(int count) {
        this.count = count;
    }

    @Override
    public String call() throws Exception {
        //生成一个随机数,模拟从操作时间
        int randem = new Random().nextInt(10) + 1;
        //线程睡眠
        Thread.sleep(randem * 1000);
        return "我是" + name + ",我保存第" + pageNum + "页的" + count + "条数据,花费时间是:" + randem + "秒";
    }

}

然后使用线程池的方式进行了模拟操作:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //创建集合,接受返回值
    List<String> list = new ArrayList<>();
    //创建线程池
    ExecutorService es = Executors.newFixedThreadPool(10);
    //记录开始时间
    long start = System.currentTimeMillis();
    //使用循环模拟多线程
    for (int i = 0; i < 10; i++) {
        Future<String> future = es.submit(new CallableImpl("我是线程" + i,i,100));
        list.add(future.get());
    }
    //关闭线程池
    es.shutdown();
    //记录结束时间
    long end = System.currentTimeMillis();
    //打印获取的结果
    list.forEach(System.out::println);
    //总耗时
    System.err.println("总耗时:" + ((end - start)/1000));
}

执行后获取执行结果:

总耗时:64
我是我是线程0,我保存第0页的100条数据,花费时间是:5秒
我是我是线程1,我保存第1页的100条数据,花费时间是:6秒
我是我是线程2,我保存第2页的100条数据,花费时间是:1秒
我是我是线程3,我保存第3页的100条数据,花费时间是:8秒
我是我是线程4,我保存第4页的100条数据,花费时间是:2秒
我是我是线程5,我保存第5页的100条数据,花费时间是:9秒
我是我是线程6,我保存第6页的100条数据,花费时间是:10秒
我是我是线程7,我保存第7页的100条数据,花费时间是:9秒
我是我是线程8,我保存第8页的100条数据,花费时间是:7秒
我是我是线程9,我保存第9页的100条数据,花费时间是:7秒

结论:进行数据模拟后,发现由于需要获取线程执行结果,虽然表明上使用了多线程来进行数据的操作,但最后的结果还是等同于使用单一线程进行处理,所以上面这种方式是不可取的。

2 使用线程池+runnable+共享变量的方式

由于使用callable不能满足需求,所以想到了使用共享变量的方式来进行参数的传递

首先创建共享变量的工具类:

package com.codebull;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: CodeBull
 * @Date: 2021/6/8
 * @version: 1.0
 */
public class Date {
    /**
     * 创建共享变量
     */
    private static List<String> list;

    private static int number;

    //初始化变量
    static {
        list = new ArrayList<>();
    }

    /**
     * 预计结果集数量(实际业务中可根据数据总量以及分页条件计算获得)
     */
    public static void setNumber(int number) {
        Date.number = number;
    }

    /**
     * 添加元素到共享变量中
     * @param date 添加的元素
     */
    public static synchronized void addList(String date) {
        list.add(date);
    }

    public static void printList() {
        while (true){
            if (list.size() == number){
                //当结果集数量与预计数量相同时,进行操作(此处模拟操作为打印)
                list.forEach(System.out::println);
                break;
            }else {
                //当结果集数量与预计数量不相同时,等待子线程执行完成后再进行判断
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

接下来是线程实现类:

package com.codebull;

import java.util.Random;

/**
 * @Author: CodeBull
 * @Date: 2021/6/8
 * @version: 1.0
 */
public class RunnableImpl implements Runnable {

    /** 线程名称 */
    private String name;
    /** 开始页码 */
    private int pageNum;
    /** 结束页码 */
    private int count;

    public RunnableImpl(String name, int pageNum, int end) {
        this.name = name;
        this.pageNum = pageNum;
        this.count = end;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStart() {
        return pageNum;
    }

    public void setStart(int start) {
        this.pageNum = start;
    }

    public int getEnd() {
        return count;
    }

    public void setEnd(int count) {
        this.count = count;
    }


    @Override
    public void run(){
        //生成一个随机数,模拟从操作时间
        int randem = new Random().nextInt(10) + 1;
        //线程睡眠
        try {
            Thread.sleep(randem * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String code = "我是" + name + ",我保存第" + pageNum + "页的" + count + "条数据,花费时间是:" + randem + "秒";
        Date.addList(code);
    }
}

最后进行相关测试

public static void main(String[] args) {
    //创建线程池
    ExecutorService es = Executors.newFixedThreadPool(10);
    //记录开始时间
    long start = System.currentTimeMillis();
    //设置预计数量总数
    Date.setNumber(10);
    //使用循环模拟多线程
    for (int i = 0; i < 10; i++) {
        es.execute(new RunnableImpl("我是线程" + i,i,100));
    }
    //关闭线程池
    es.shutdown();
    //打印获取的结果
    Date.printList();
    //记录结束时间
    long end = System.currentTimeMillis();
    //总耗时
    System.err.println("总耗时:" + ((end - start)/1000));
}

测试结果如下

总耗时:10
我是我是线程4,我保存第4页的100条数据,花费时间是:1秒
我是我是线程3,我保存第3页的100条数据,花费时间是:2秒
我是我是线程6,我保存第6页的100条数据,花费时间是:3秒
我是我是线程1,我保存第1页的100条数据,花费时间是:4秒
我是我是线程9,我保存第9页的100条数据,花费时间是:4秒
我是我是线程2,我保存第2页的100条数据,花费时间是:7秒
我是我是线程5,我保存第5页的100条数据,花费时间是:8秒
我是我是线程7,我保存第7页的100条数据,花费时间是:8秒
我是我是线程0,我保存第0页的100条数据,花费时间是:10秒
我是我是线程8,我保存第8页的100条数据,花费时间是:10秒

可以发现,使用共享变量的方式,测试情况下(实际业务中应该和测试有误差,尤其是线程数量大时对数据库的压力),最终时间基本与花费时间最多的线程时间相同。

还有一个问题,就是共享变量的锁问题。测试例子中使用的是强锁synchronized,如果在线程量大时应该会对性能产生影响,当然,影响是基本可以忽略的。因为共享变量的操作是相对简单操作,而子线程业务随机性也较大。

3 关于线程安全的一些考虑

由于实际业务是多线程同事操作的,所以难免会在多用户同事操作时出现线程安全问题。我在这里想到了使用ThreadLocal来处理各个线程的共享变量相关的问题。也就是说将【测试2】中的静态共享变量设置为普通变量,在使用时创建该对象进行使用,在获取对象操作其中的共享变量时,从ThreadLocal中进行获取和操作,从而避免业务中的多线程问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 可能的原因是,在多线程环境下,多个线程同时写入文件,导致数据被覆盖或丢失。解决方法可以使用线程同步机制,例如使用synchronized关键字或Lock对象来保证多个线程对文件的访问互斥,以避免数据丢失的问题。同时,也需要注意文件的读写权限、路径是否正确等问题。 ### 回答2: 在Java中,多线程执行load data local infile操作可能会导致数据丢失的原因有以下几点: 1. 并发写入导致数据覆盖:如果多个线程同时执行load data local infile操作并将数据写入同一个文件中,可能会导致数据冲突,从而导致部分数据被覆盖丢失。 2. 读写冲突引发的数据丢失:在多线程环境下,如果同时进行读取文件和写入文件的操作,可能会发生读写冲突,导致数据丢失。 3. 数据处理不及时或错误:在多线程执行load data local infile操作时,如果处理数据的速度跟不上数据生成的速度,或者处理数据时出现错误而导致中断,都有可能导致部分数据丢失。 为避免数据丢失,可以采取以下措施: 1. 保证数据写入的文件是互斥的:通过加锁或者使用单线程的方式,确保多线程不会同时写入同一个文件,避免数据覆盖。 2. 合理处理读写冲突:通过对读取和写入操作进行同步,例如使用锁机制或者采用线程安全的数据结构,确保在进行读取和写入操作时不会发生冲突,从而避免数据丢失。 3. 增加数据处理的速度:通过优化算法或者增加处理资源,确保数据处理速度能够跟上数据生成的速度,避免数据积压或者丢失。 4. 异常处理数据备份:在进行数据处理时,及时捕获异常并进行相应的处理,例如进行数据备份、重试等,以减少数据丢失的可能性。 总之,多线程执行load data local infile操作时,需要注意并发写入、读写冲突和数据处理的及时性,以避免数据丢失发生。 ### 回答3: 当Java多线程执行load data local infile操作时,数据丢失可能是由于以下几个原因引起的: 1. 网络问题:在数据传输过程中,网络连接可能出现问题,导致数据丢失。这可能包括网络中断、丢包等情况。要解决这个问题,可以尝试重新执行操作,或者检查网络连接是否稳定。 2. 并发冲突:多线程同时执行load data local infile操作时,如果没有正确处理并发冲突,就可能导致数据丢失。这可能是因为多个线程同时读取和写入数据导致的。为了解决这个问题,可以使用同步机制(如锁)来确保每个线程访问数据的原子性。 3. 数据库配置问题:数据丢失可能是由于数据库配置不正确导致的。例如,数据库的缓冲区设置不合适、超时时过短等。需要检查数据库的配置参数是否正确,并进行必要的调整。 4. 数据写入错误:在进行load data local infile操作时,可能存在数据写入错误的情况。例如,写入的数据格式不正确、写入的数据太大导致溢出等。要解决这个问题,可以检查数据的格式和大小,并对数据写入进行适当的处理和验证。 综上所述,数据丢失可能是由于网络问题、并发冲突、数据库配置问题或数据写入错误导致的。要解决这个问题,可以检查并修复引起数据丢失的具体原因,并采取相应的措施来防止数据丢失的发生。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值