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)을 얻어야 할 때마다 연결(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초)

간결함: 접합 형식: 학생(xxxx) 값(xxxx),(xxxx),(xxxxx)에 삽입...
요약: 접합 결과는 모든 데이터를 SQL 문의 값으로 통합하여 server insert 문 수가 적어지면 네트워크 부하가 줄어들고 성능이 향상됩니다. 다만, 데이터량이 증가할 경우 메모리 오버플로, SQL문 분석에 시간이 많이 소요되는 현상이 발생할 수 있으나, 첫 번째 점과 비교하면 성능이 크게 향상된다.

 /**
 * 拼接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 루프 삽입임을 알 수 있습니다. 그러나 첫 번째 항목과 비교하면 성능이 향상되는 이유는 무엇입니까? ? Connection에서 성능을 소모하는 것이 아니라 샤딩 처리(batchSize = 1000) + 트랜잭션을 일괄 제출하는 작업을 사용하여 성능을 향상시키기 때문입니다. (현재 개인적으로 솔루션이 더 최적이라고 생각합니다)

/**
 * 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() 메서드의 excuteBatch() 문을 무시하고 일괄 처리해야 하는 SQL 문 그룹을 분할하여 실행 중에 하나씩 MySQL 데이터베이스로 보냅니다. 실제 Fragmented Insertion 즉, Single Insertion 방식과 비교하여 개선은 있으나 성능은 크게 개선되지 않았습니다.
테스트: 데이터베이스 연결 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초)(강조: 일회성 제출)

간결함: 일괄 처리를 켜고 자동 트랜잭션 제출을 끄고 동일한 SqlSession을 공유합니다 . for 루프의 단일 삽입 성능이 크게 향상됩니다 . 동일한 SqlSession이 리소스 관련 작업의 에너지 소비를 절약하고 트랜잭션 처리 시간을 단축하기 때문입니다. 등으로 인해 실행 효율성이 크게 향상됩니다. (현재 개인적으로 솔루션이 더 최적이라고 생각합니다)

/**
 * 共用同一个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. ThreadPoolTaskExecutor (총 소요 시간: 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