SpringBoot は数万のデータをバッチで効率的に挿入します

準備

1. Maven プロジェクトの pom.xml ファイルによって導入される関連する依存関係は次のとおりです。

<dependencies>
    <!-- SpringBoot Web模块依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MyBatis-Plus 依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>

    <!-- 数据库连接驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <!-- 使用注解,简化代码-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

2. Application.yml 設定プロパティ ファイルの内容 (キー ポイント: バッチ処理モードを有効にする)

server:
    端口号 
    port: 8080

#  MySQL连接配置信息(以下仅简单配置,更多设置可自行查看)
spring:
    datasource:
         连接地址(解决UTF-8中文乱码问题 + 时区校正)
                (rewriteBatchedStatements=true 开启批处理模式)
        url: jdbc:mysql://127.0.0.1:3306/bjpowernode?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
         用户名
        username: root
         密码
        password: xxx
         连接驱动名称
        driver-class-name: com.mysql.cj.jdbc.Driver

3. エンティティエンティティクラス(テスト)

/**
 *   Student 测试实体类
 *   
 *   @Data注解:引入Lombok依赖,可省略Setter、Getter方法
 */
@Data
@TableName(value = "student")
public class Student {
    
    
    
    /**  主键  type:自增 */
    @TableId(type = IdType.AUTO)
    private int id;

    /**  名字 */
    private String name;

    /**  年龄 */
    private int age;

    /**  地址 */
    private String addr;

    /**  地址号  @TableField:与表字段映射 */
    @TableField(value = "addr_num")
    private String addrNum;

    public Student(String name, int age, String addr, String addrNum) {
    
    
        this.name = name;
        this.age = age;
        this.addr = addr;
        this.addrNum = addrNum;
    }
}

4. データベースの学生テーブルの構造 (注: インデックスなし)

50fb64b0ac314991bcf2ed8360220da0~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


テスト作業

1. ループ挿入の場合(シングル)(総所要時間:177秒)

概要: 平均テスト時間は約 177 秒で、これは本当に見るに耐えられません (顔が覆われています)。for ループを使用して単一の挿入を実行する場合、接続 (Connection) を取得するたびに、 (データ量が多い場合)リソースを非常に消費し、時間がかかります。

@GetMapping("/for")
public void forSingle(){
    
    
    // 开始时间
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 50000; i++){
    
    
        Student student = new Student("李毅" + i,24,"张家界市" + i,i + "号");
        studentMapper.insert(student);
    }
    // 结束时间
    long endTime = System.currentTimeMillis();
    System.out.println("插入数据消耗时间:" + (endTime - startTime));
}

テスト時間:
79e5b943df4f49109201cfb5d3a647f3~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


2. SQL ステートメントの結合 (合計所要時間: 2.9 秒)

簡潔: スプライシング形式: Student(xxxx) に挿入 value(xxxx),(xxxx),(xxxxx)...
概要: スプライシングの結果は、すべてのデータを SQL ステートメントの値に統合し、SQL ステートメントの値に送信します。サーバー 挿入ステートメントが減ると、ネットワーク負荷が軽減され、パフォーマンスが向上します。ただし、データ量が増加するとメモリオーバーが発生したり、SQL 文の解析に時間がかかる場合がありますが、1 点目に比べて大幅にパフォーマンスが向上します。

 /**
 * 拼接sql形式
 */
@GetMapping("/sql")
public void sql(){
    
    
    ArrayList<Student> arrayList = new ArrayList<>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 50000; i++){
    
    
        Student student = new Student("李毅" + i,24,"张家界市" + i,i + "号");
        arrayList.add(student);
    }
    studentMapper.insertSplice(arrayList);
    long endTime = System.currentTimeMillis();
    System.out.println("插入数据消耗时间:" + (endTime - startTime));
}

マッパー

public interface StudentMapper extends BaseMapper<Student> {
    
    

    @Insert("<script>" +
            "insert into student (name, age, addr, addr_num) values " +
            "<foreach collection='studentList' item='item' separator=','> " +
            "(#{item.name}, #{item.age},#{item.addr}, #{item.addrNum}) " +
            "</foreach> " +
            "</script>")

    int insertSplice(@Param("studentList") List<Student> studentList);
}

試験結果
7eebed7a22a6472fb1be3cfc6955ca47~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


3. saveBatch のバッチ挿入 (合計所要時間: 2.7 秒)

簡潔に: MyBatis-Plus を使用して、IService インターフェイスにバッチ saveBatch() メソッドを実装します。基礎となるソース コードを表示すると、実際には for ループの挿入であることがわかります。しかし、最初の点と比較して、パフォーマンスが向上しているのはなぜですか? ? シャーディング処理 (batchSize = 1000) + トランザクションをバッチで送信する操作は、Connection のパフォーマンスを消費するのではなく、パフォーマンスを向上させるために使用されるためです。(現時点では、個人的にはこの解決策がより最適であると考えています)

/**
 * mybatis-plus的批处理模式
 */
@GetMapping("/saveBatch1")
public void saveBatch1(){
    
    
    ArrayList<Student> arrayList = new ArrayList<>();
    long startTime = System.currentTimeMillis();
    // 模拟数据
    for (int i = 0; i < 50000; i++){
    
    
        Student student = new Student("李毅" + i,24,"张家界市" + i,i + "号");
        arrayList.add(student);
    }
    // 批量插入
    studentService.saveBatch(arrayList);
    long endTime = System.currentTimeMillis();
    System.out.println("插入数据消耗时间:" + (endTime - startTime));
}

15968993d9e04ee39eb9d260daf72605~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp
重要な注意事項: MySQL JDBC ドライバーはデフォルトで saveBatch() メソッドのexecuteBatch() ステートメントを無視し、バッチで処理する必要がある SQL ステートメントのグループを分割し、実行中にそれらを 1 つずつ MySQL データベースに送信します。実際の断片化挿入法、つまり単一挿入法と比較すると、改善はされていますが、実質的な性能の向上には至っていません。
テスト: データベース接続 URL アドレスに rewriteBatchedStatements = true パラメーターがありません。

#  MySQL连接配置信息
spring:
    datasource:
         连接地址(未开启批处理模式)
        url: jdbc:mysql://127.0.0.1:3306/bjpowernode?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
         用户名
        username: root
         密码
        password: xxx
         连接驱动名称
        driver-class-name: com.mysql.cj.jdbc.Driver

テスト結果: 10541、これは約 10.5 秒に相当します (バッチ モードはオンになっていません)。
a4314ddb36934434b6d947135ebc9ce1~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


4. ループ挿入 + バッチ処理モードをオンにする (合計所要時間: 1.7 秒) (強調: 1 回限りの送信)

簡潔: バッチ処理をオンにし、自動トランザクション送信をオフにして、同じ SqlSession を共有します。同じ SqlSession によりリソース関連の操作のエネルギー消費が節約され、トランザクション処理時間が短縮されるため、for ループでの 1 回の挿入のパフォーマンスが大幅に向上します。など、実行効率が大幅に向上します。(現時点では、個人的にはこの解決策がより最適であると考えています)

/**
 * 共用同一个SqlSession
 */
@GetMapping("/forSaveBatch")
public void forSaveBatch(){
    
    
    //  开启批量处理模式 BATCH 、关闭自动提交事务 false

    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
    //  反射获取,获取Mapper
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    long startTime = System.currentTimeMillis();
    for (int i = 0 ; i < 50000 ; i++){
    
    
        Student student = new Student("李毅" + i,24,"张家界市" + i,i + "号");
        studentMapper.insert(student);
    }
    // 一次性提交事务
    sqlSession.commit();
    // 关闭资源
    sqlSession.close();
    long endTime = System.currentTimeMillis();
    System.out.println("总耗时: " + (endTime - startTime));
}

5. ThreadPoolTask​​Executor (合計所要時間: 1.7 秒)

(現時点では、個人的にはこの解決策がより最適であると考えています)

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Autowired
    private PlatformTransactionManager transactionManager;

    @GetMapping("/batchInsert2")
    public void batchInsert2() {
    
    
        ArrayList<Student> arrayList = new ArrayList<>();
        long startTime = System.currentTimeMillis();
        // 模拟数据
        for (int i = 0; i < 50000; i++){
    
    
            Student student = new Student("李毅" + i,24,"张家界市" + i,i + "号");
            arrayList.add(student);
        }
        int count = arrayList.size();
        int pageSize = 1000; // 每批次插入的数据量
        int threadNum = count / pageSize + 1; // 线程数
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
    
    
            int startIndex = i * pageSize;
            int endIndex = Math.min(count, (i + 1) * pageSize);
            List<Student> subList = arrayList.subList(startIndex, endIndex);
            threadPoolTaskExecutor.execute(() -> {
    
    
                DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
                TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
                try {
    
    
                    studentMapper.insertSplice(subList);
                    transactionManager.commit(status);
                } catch (Exception e) {
    
    
                    transactionManager.rollback(status);
                    throw e;
                } finally {
    
    
                    countDownLatch.countDown();
                }
            });
        }
        try {
    
    
            countDownLatch.await();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

ThreadPoolTaskExecutorまず、スレッドのライフサイクルを管理し、タスクを実行するためのスレッドプール( )が定義されます。次に、指定されたバッチ サイズに従って、挿入するデータ リストを複数のサブリストに分割し、複数のスレッドを起動して挿入操作を実行します。
まず、TransactionManager を通じてトランザクション マネージャーを取得し、それを使用してTransactionDefinitionトランザクション プロパティを定義します。次に、各スレッドでtransactionManager.getTransaction()メソッドを通じてトランザクション状態を取得し、その状態を使用して挿入操作中にトランザクションを管理します。挿入操作が完了したら、transactionManager.commit()またはtransactionManager.rollback() メソッドを呼び出して、操作の結果に基づいてトランザクションをコミットまたはロールバックします。各スレッドの実行が終了するとCountDownLatch 的 countDown() メソッドが呼び出され、メイン スレッドはすべてのスレッドの実行が終了するまで待機してから戻ります。

おすすめ

転載: blog.csdn.net/weixin_44030143/article/details/130825037