springboot配置執行緒池使用多執行緒插入資料

Half發表於2019-01-19

前言:

最近在工作中需要將一大批資料匯入到資料庫中,因為種種原因這些資料不能使用同步資料的方式來進行復制,而是提供了一批文字,文字里面有很多行url地址,需要的欄位都包含在這些url中。最開始是使用的正常的普通方式去寫入,但是量太大了,所以就嘗試使用多執行緒來寫入。下面我們就來介紹一下怎麼使用多執行緒進行匯入。

1.文字格式

格式就是類似於這種格式的url,當然這裡只是舉個例子,大概有300多個文字,每個文字里面有大概25000條url,而每條url要插入兩個表,這個量還是有點大的,單執行緒跑的非常慢。

  • https://www.test.com/?type=1&code=123456&goodsId=321

2.springboot配置執行緒池

我們需要建立一個ExecutorConfig類來設定執行緒池的各種配置。

@Configuration
@EnableAsync
public class ExecutorConfig {
    private static Logger logger = LogManager.getLogger(ExecutorConfig.class.getName());

    @Bean
    public Executor asyncServiceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //配置核心執行緒數
        executor.setCorePoolSize(5);
        //配置最大執行緒數
        executor.setMaxPoolSize(10);
        //配置佇列大小
        executor.setQueueCapacity(400);
        //配置執行緒池中的執行緒的名稱字首
        executor.setThreadNamePrefix("thread-");
        // rejection-policy:當pool已經達到max size的時候,如何處理新任務
        // CALLER_RUNS:不在新執行緒中執行任務,而是有呼叫者所在的執行緒來執行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //執行初始化
        executor.initialize();
        return executor;
    }
}

3.建立非同步任務介面

我們需要建立一個介面,再這個介面裡面宣告瞭我們需要呼叫的非同步方法

public interface AsyncService {

    /**
     *  執行非同步任務
     */
    void writeTxt();
}

4.建立非同步實現類

再建立一個非同步類實現上面的非同步介面,重寫介面裡面的方法,最重要的是我們需要在方法上加@Async("asyncServiceExecutor")註解,它是剛剛我們線上程池配置類的裡的那個配製方法的名字,加上這個後每次執行這個方法都會開啟一個執行緒放入執行緒池中。我下面這個方法是開啟多執行緒遍歷資料夾中的檔案然後為每個檔案都複製一個副本出來。

@Service
public class AsyncServiceImpl implements AsyncService {
    private static Logger logger = LogManager.getLogger(AsyncServiceImpl.class.getName());

    @Async("asyncServiceExecutor")
    public void writeTxt(String fileName){
        logger.info("執行緒-" + Thread.currentThread().getId() + "在執行寫入");
        try {
            File file = new File(fileName);

            List<String> lines = FileUtils.readLines(file);

            File copyFile = new File(fileName + "_copy.txt");
            lines.stream().forEach(string->{
                try {
                    FileUtils.writeStringToFile(copyFile,string,"utf8",true);
                    FileUtils.writeStringToFile(copyFile,"
","utf8",true);
                } catch (IOException e) {
                    logger.info(e.getMessage());
                }
            });
        }catch (Exception e) {
            logger.info(e.getMessage());
        }
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class BootApplicationTests {

@Autowired
private AsyncService asyncService;

@Test
public void write() {
    File file = new File("F://ac_code_1//test.txt");
    try {
        FileUtils.writeStringToFile(file, "ceshi", "utf8");
        FileUtils.writeStringToFile(file, "
", "utf8");
        FileUtils.writeStringToFile(file, "ceshi2", "utf8");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

5.修改為阻塞式

上面的步驟已經基本實現了多執行緒的操作,但是當我真的開始匯入資料的時候又發現一個問題,就是每次執行後才剛開始匯入就自動停止了,原因是我在Junit中執行了程式碼後它雖然開始匯入了,但是因為資料很多時間很長,而Juint跑完主執行緒的邏輯後就把整個JVM都關掉了,所以匯入了一點點就停止了,上面的測試方法之所以沒問題是因為幾個檔案的複製速度很快,在主執行緒跑完之前就跑完了,所以看上去沒問題。最開始我用了一個最笨的方法,直接在主執行緒最後呼叫Thread.sleep()方法,雖然有效果但是這也太low了,而且你也沒法判斷到底資料導完沒有。所以我又換了一個方式。

6.使用countDownLatch阻塞主執行緒

CountDownLatch是一個同步工具類,它允許一個或多個執行緒一直等待,直到其他執行緒執行完後再執行。它可以使主執行緒一直等到所有的子執行緒執行完之後再執行。我們修改下程式碼,建立一個CountDownLatch例項,大小是所有執行執行緒的數量,然後在非同步類的方法中的finally裡面對它進行減1,在主執行緒最後呼叫await()方法,這樣就能確保所有的子執行緒執行完後主執行緒才會繼續執行。

@RunWith(SpringRunner.class)
@SpringBootTest
public class BootApplicationTests {

    private final CountDownLatch countDownLatch = new CountDownLatch(10);

    @Autowired
    private AsyncService asyncService;

    @Test
    public void mainWait() {
        try {
            for (int i = 0; i < 10; i++) {
                asyncService.mainWait(countDownLatch);
            }
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@Service
public class AsyncServiceImpl implements AsyncService {
    private static Logger logger = LogManager.getLogger(AsyncServiceImpl.class.getName());

    @Override
    @Async("asyncServiceExecutor")
    public void mainWait(CountDownLatch countDownLatch) {
        try {
            System.out.println("執行緒" + Thread.currentThread().getId() + "開始執行");
            for (int i=1;i<1000000000;i++){
                Integer integer = new Integer(i);
                int l = integer.intValue();
                for (int x=1;x<10;x++){
                    Integer integerx = new Integer(x);
                    int j = integerx.intValue();
                }
            }
            System.out.println("執行緒" + Thread.currentThread().getId() + "執行結束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
    }
}

7.匯入程式碼

雖然上面的多執行緒是重點,不過還是把匯入資料的程式碼展示出來給大家參考一下,當然這是簡化版,真實的要比這個多了很多判斷,不過那都是基於業務需求做的判斷。

@RunWith(value = SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
    private static Log logger = LogFactory.getLog(ApplicationTests.class);
    
    private final CountDownLatch countDownLatch;

    @Autowired
    AsyncService asyncService;

    @Test
    public void writeCode() {
        try {
            File file = new File("F:\ac_code_1");
            File[] files = file.listFiles();
            //計數器數量就等於檔案數量,因為每個檔案會開一個執行緒
            countDownLatch = new CountDownLatch(files.length);

            Arrays.stream(files).forEach(file1 -> {
                File child = new File(file1.getAbsolutePath());
                String fileName = child.getAbsolutePath();
                logger.info(asyncService.writeCode(fileName,countDownLatch));
            });
            countDownLatch.await();
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@Service
public class AsyncServiceImpl implements AsyncService {
    private static Log logger = LogFactory.getLog(AsyncServiceImpl.class);

    @Autowired
    IExampleService exampleService;

    @Override
    @Async("asyncServiceExecutor")
    public String writeCode(String fileName,CountDownLatch countDownLatch) {
        logger.info("執行緒-" + Thread.currentThread().getId() + "在匯入-" + fileName);
        try {
            File file = new File(fileName);
            List<String> list = FileUtils.readLines(file);
            for (String string : list) {
                String[] parmas = string.split(",");
                ExampleVo vo = new ExampleVo();
                vo.setParam1(parmas[0]);
                vo.setParam1(parmas[1]);
                vo.setParam1(parmas[2]);
                exampleService.save(vo);
            }
            return "匯入完成-" + fileName;
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }finally {
            //匯入完後減1
            countDownLatch.countDown();
        }
    }
}

總結:

到這裡就已經講完了多執行緒插入資料的方法,目前這個方法還很簡陋。因為是每個檔案都開一個執行緒效能消耗比較大,而且如果執行緒執行緒池的執行緒配置太多了,頻繁切換反而會變得很慢,大家如果有更好的辦法都可以留言討論。

相關文章