SpringBoot inserta eficientemente decenas de miles de datos en lotes

Preparación

1. Las dependencias relevantes introducidas por el archivo pom.xml en el proyecto Maven son las siguientes:

<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. Contenido del archivo de propiedades de configuración Application.yml (Punto clave: habilitar el modo de procesamiento por lotes)

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. Clase de entidad de entidad (prueba)

/**
 *   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. Estructura de la tabla de estudiantes de la base de datos (nota: sin índice)

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


trabajo de prueba

1. Para inserción de bucle (único) (tiempo total necesario: 177 segundos)

Resumen: El tiempo promedio de prueba es de aproximadamente 177 segundos, lo cual es realmente insoportable de ver (cubrirse la cara), porque cuando se usa un bucle for para realizar una sola inserción, cada vez que es necesario obtener la conexión (Conexión), suelte el conexión y cierre el recurso (si la cantidad de datos es grande), consume muchísimos recursos y lleva mucho tiempo.

@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));
}

tiempo de prueba:
79e5b943df4f49109201cfb5d3a647f3~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


2. Empalme de sentencias SQL (tiempo total necesario: 2,9 segundos)

Conciso: Formato de empalme: insertar en estudiante (xxxx) valor (xxxx), (xxxx), (xxxxx) ...
Resumen: El resultado del empalme es integrar todos los datos en el valor de una declaración SQL, que se envía al servidor Con menos instrucciones de inserción, se reduce la carga de la red y se mejora el rendimiento. Sin embargo, cuando aumenta la cantidad de datos, puede producirse un desbordamiento de la memoria y un análisis de declaraciones SQL que requiere mucho tiempo, pero en comparación con el primer punto, el rendimiento ha mejorado considerablemente.

 /**
 * 拼接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));
}

mapeador

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);
}

Resultados de la prueba
7eebed7a22a6472fb1be3cfc6955ca47~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


3. Insertar lote saveBatch (tiempo total necesario: 2,7 segundos)

Concisamente: use MyBatis-Plus para implementar el método saveBatch () por lotes en la interfaz IService. Al ver el código fuente subyacente, puede encontrar que en realidad es una inserción de bucle for. Pero en comparación con el primer punto, ¿por qué mejora el rendimiento? ? Porque el procesamiento de fragmentación (batchSize = 1000) + la operación de enviar transacciones en lotes se utiliza para mejorar el rendimiento, en lugar de consumir el rendimiento de Connection. (Actualmente, personalmente creo que la solución es más óptima)

/**
 * 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
Nota importante: el controlador MySQL JDBC ignora la instrucción ejecutarBatch() en el método saveBatch() de forma predeterminada , dividiendo un grupo de declaraciones SQL que deben procesarse en lotes y enviándolas a la base de datos MySQL una por una durante la ejecución, lo que resulta en en la inserción fragmentada real, es decir, en comparación con el método de inserción única, hay una mejora, pero el rendimiento no ha mejorado sustancialmente.
Prueba: A la dirección URL de conexión de la base de datos le falta el parámetro 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

Resultado de la prueba: 10541, que equivale aproximadamente a 10,5 segundos (el modo por lotes no está activado)
a4314ddb36934434b6d947135ebc9ce1~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


4. Inserción de bucle + activar el modo de procesamiento por lotes (tiempo total necesario: 1,7 segundos) (énfasis: envío único)

Conciso: active el procesamiento por lotes, desactive el envío automático de transacciones y comparta la misma SqlSession . El rendimiento de la inserción única en el bucle for ha mejorado sustancialmente , porque la misma SqlSession ahorra el consumo de energía de las operaciones relacionadas con los recursos y reduce el tiempo de procesamiento de las transacciones. etc., mejorando así en gran medida la eficiencia de ejecución. (Actualmente, personalmente creo que la solución es más óptima)

/**
 * 共用同一个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 (tiempo total empleado: 1,7 segundos)

(Actualmente, personalmente creo que la solución es más óptima)

    @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();
        }
    }

ThreadPoolTaskExecutorPrimero, se define un grupo de subprocesos ( ) para gestionar el ciclo de vida de los subprocesos y ejecutar tareas. Luego, dividimos la lista de datos que se insertará en varias sublistas de acuerdo con el tamaño de lote especificado e iniciamos varios subprocesos para realizar la operación de inserción.
Primero obtenga el administrador de transacciones a través de TransactionManager y utilícelo para TransactionDefinitiondefinir las propiedades de la transacción. Luego, en cada hilo, obtenemos transactionManager.getTransaction()el estado de la transacción a través del método y usamos ese estado para administrar la transacción durante la operación de inserción. Una vez completada la operación de inserción, llamamos transactionManager.commit()a un transactionManager.rollback() método para confirmar o revertir la transacción según los resultados de la operación. Una vez que cada subproceso termina de ejecutarse, CountDownLatch 的 countDown() se llama al método para que el subproceso principal espere a que todos los subprocesos terminen de ejecutarse antes de regresar.

Supongo que te gusta

Origin blog.csdn.net/weixin_44030143/article/details/130825037
Recomendado
Clasificación