浅谈MyBatis批量插入优化方案

浅谈MyBatis批量插入优化方案

1. 传统方案

MyBatis批量插入的主流方案主要有以下:

序号 方案 优点 缺点 使用情形
1 for循环单条数据依次插入 1. 代码简单
2. 容易实现和理解
1. 性能较差,因每次插入都需要一次数据库连接和提交
2. 对数据库负载大
适用于数据量较少且对性能要求不高的场景
2 在Mapper.xml的insert使用foreach,循环list生成大型insert一次执行 1. 性能较好,减少了数据库连接和提交的次数
2. 实现较为简单
1. SQL语句长度受限,可能会因为SQL语句过长导致失败
2. 需要注意参数数量和数据库的限制
适用于数据量适中,SQL长度在可控范围内的场景
3 使用SqlSession手动控制事务,一次性提交大量的insert 1. 性能最佳,减少了大量数据库连接和提交次数
2. 可以处理大批量数据
1. 实现较为复杂,需要手动管理事务
2. 如果事务中有错误,处理较为麻烦
适用于数据量大,对性能要求高,并且能接受较复杂实现的场景

2. 思考

  • 第1种方案效率太低,通常不会使用;

  • 第2种方案在数据量太大时生成的sql会超出mysql的最大允许包大小(max_allowed_packet)导致失败,通常会采取分批次执行

  • 第3种方案比较合适,但是每一处使用批量插入都要手写一堆代码来处理吗?有没有更合适的方式来实现呢?

需要解决的问题:

  • 通用
  • 效率
  • 事务

2.1 通用

每一个Entity在Mapper中的实现都有基础的增删查改,如何去复用这里的“增”来实现我们的批量新增呢?

注解 ,我们只要加上一个注解标注出来就可以了

2.2 效率

说到效率大部分人都会想到多线程,理论上在insert时会锁表,那么我们使用多线程进行插入时,不同线程之间发生冲突会不会造成线程等待,导致多线程不起效果呢?我觉得还是得要实践才能出真知。

2.3 事务

暂时不考虑

3. 实现

注解类:

/**
 * 描述:
 *
 * @author : zzq
 * @date : 2024-06-02 12:36
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BatchInsert {
}

import com.beust.jcommander.ParameterException;
import com.ruoyi.common.utils.StringUtils;
import com.xxl.job.executor.core.config.XxlJobSampleConfig;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * 描述: 批量插入工具类
 * 使用@BatchInsert注解进行批量插入
 * 使用方法:
 * 1. 使用@BatchInsert标记Mapper接口中的insert方法
 *  [若使用分表,请确保分表字段在参数中,并且作为第二个参数传入,例如
 *      /@BatchInsert
 *     public int insertNwForecastDataByTableName(@Param("nwForecastData") NwForecastData nwForecastData, @Param("tableName") String tableName);]
 * 2. 使用工具类进行批量插入
 *
 * @author : zzq
 * @date : 2024-06-02 12:35
 **/
public class BatchInsertUtil {
    private static final Logger logger = LoggerFactory.getLogger(BatchInsertUtil.class);
    private static final int THREAD_COUNT = Runtime.getRuntime().availableProcessors();
    public static final int BATCH_INSERT_SIZE = 3000;
    private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

    /**
     * 批量插入
     * @param list 待插入列表
     * @param mapperType Mapper类
    */
    public static <T> void batchInsert(List<T> list, Class<?> mapperType) {
        batchInsert(list, mapperType, null);
    }

    /**
     * 批量插入
     * @param list 待插入列表
     * @param mapperType Mapper类
     * @param tableName 分表名
     */
    public static <T> void batchInsert(List<T> list, Class<?> mapperType, String tableName) {
        SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(SqlSessionFactory.class);
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            Object mapper = sqlSession.getMapper(mapperType);
            Method method = findBatchInsertMethod(mapperType);
            if (method != null) {
                performBatchInsert(sqlSession, mapper, method, list, tableName);
            }
        } catch (Exception e) {
            logger.error("批量插入失败", e);
        }
    }

    private static Method findBatchInsertMethod(Class<?> mapperType) {
        for (Method method : mapperType.getMethods()) {
            if (method.isAnnotationPresent(BatchInsert.class)) {
                return method;
            }
        }
        return null;
    }

    private static <T> void performBatchInsert(SqlSession sqlSession, Object mapper, Method method, List<T> list, String tableName) throws Exception {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length == 1) {
            insertWithSingleParameter(sqlSession, mapper, method, list);
        } else if (parameterTypes.length == 2) {
            if (StringUtils.isBlank(tableName)) {
                throw new ParameterException("Table name cannot be empty");
            }
            insertWithTwoParameters(sqlSession, mapper, method, list, tableName);
        } else {
            throw new IllegalArgumentException("Method " + method.getName() + " does not have the correct parameter count for batch insert");
        }
    }

    private static <T> void insertWithSingleParameter(SqlSession sqlSession, Object mapper, Method method, List<T> list) throws Exception {
        int count = 0;
        for (T item : list) {
            method.invoke(mapper, item);
            count++;
            if (count % BATCH_INSERT_SIZE == 0) {
                sqlSession.commit();
            }
        }
        if (count % BATCH_INSERT_SIZE != 0) {
            sqlSession.commit();
        }
    }

    private static <T> void insertWithTwoParameters(SqlSession sqlSession, Object mapper, Method method, List<T> list, String tableName) throws Exception {
        int count = 0;
        for (T item : list) {
            method.invoke(mapper, item, tableName);
            count++;
            if (count % BATCH_INSERT_SIZE == 0) {
                sqlSession.commit();
            }
        }
        if (count % BATCH_INSERT_SIZE != 0) {
            sqlSession.commit();
        }
    }

    /**
     * 多线程同步批量插入
     * @param list 待插入列表
     * @param mapperType Mapper类
     */
    public static <T> void syncBatchInsert(List<T> list, Class<?> mapperType) {
        syncBatchInsert(list, mapperType, null);
    }

    /**
     * 多线程同步批量插入
     * @param list 待插入列表
     * @param mapperType Mapper类
     * @param tableName 分表名
     */
    public static <T> void syncBatchInsert(List<T> list, Class<?> mapperType, String tableName) {
        SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(SqlSessionFactory.class);
        logger.info("可用线程数:" + THREAD_COUNT);
        List<Future<Void>> futures = new ArrayList<>();

        int batchCount = (list.size() + BATCH_INSERT_SIZE - 1) / BATCH_INSERT_SIZE;
        for (int i = 0; i < batchCount; i++) {
            int start = i * BATCH_INSERT_SIZE;
            int end = Math.min(start + BATCH_INSERT_SIZE, list.size());
            List<T> sublist = list.subList(start, end);

            Future<Void> future = executorService.submit(() -> {
                try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
                    Object mapper = sqlSession.getMapper(mapperType);
                    Method method = findBatchInsertMethod(mapperType);
                    if (method != null) {
                        performBatchInsert(sqlSession, mapper, method, sublist, tableName);
                    }
                } catch (Exception e) {
                    logger.error("同步批量插入失败", e);
                }
                return null;
            });

            futures.add(future);
        }

        waitForAll(futures);
    }

    /**
     * 多线程异步批量插入
     * @param list 待插入列表
     * @param mapperType Mapper类
     */
    public static <T> List<Future<Void>> asyncBatchInsert(List<T> list, Class<?> mapperType) {
        return asyncBatchInsert(list, mapperType, null);
    }

    /**
     * 多线程异步批量插入
     * @param list 待插入列表
     * @param mapperType Mapper类
     * @param tableName 分表名
     */
    public static <T> List<Future<Void>> asyncBatchInsert(List<T> list, Class<?> mapperType, String tableName) {
        int size = list.size();
        List<Future<Void>> futures = new ArrayList<>();
        for (int i = 0; i <= size / BATCH_INSERT_SIZE; i++) {
            int startIndex = i * BATCH_INSERT_SIZE;
            int endIndex = Math.min(startIndex + BATCH_INSERT_SIZE, size);
            List<T> subList = new CopyOnWriteArrayList<>(list.subList(startIndex, endIndex));
            futures.addAll(executeBatchInsert(subList, mapperType, tableName));
        }
        list.clear();
        return futures;
    }

    private static List<Future<Void>> executeBatchInsert(List<?> list, Class<?> mapperType, String tableName) {
        SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(SqlSessionFactory.class);
        List<Future<Void>> futures = new ArrayList<>();
        Future<Void> future = executorService.submit(() -> {
            try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
                Object mapper = sqlSession.getMapper(mapperType);
                Method method = findBatchInsertMethod(mapperType);
                if (method != null) {
                    performBatchInsert(sqlSession, mapper, method, list, tableName);
                }
            } catch (Exception e) {
                logger.error("异步批量插入失败", e);
            }
            return null;
        });
        futures.add(future);
        return futures;
    }

    /**
     * 判断异步任务是否执行完毕
     * @param futures 异步任务列表
    */
    public static void waitForAll(List<Future<Void>> futures) {
        for (Future<Void> future : futures) {
            try {
                future.get();
            } catch (ExecutionException | InterruptedException e) {
                logger.error("等待任务失败", e);
            }
        }
    }
}

4. 使用说明

步骤1:在Mapper接口中使用@BatchInsert注解

在需要进行批量插入的方法上添加@BatchInsert注解。例如:

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;

public interface MyMapper {
    @BatchInsert
    @Insert("INSERT INTO my_table (column1, column2) VALUES (#{item.column1}, #{item.column2})")
    int insertItem(@Param("item") MyItem item);

    @BatchInsert
    @Insert("INSERT INTO ${tableName} (column1, column2) VALUES (#{item.column1}, #{item.column2})")
    int insertItemByTableName(@Param("item") MyItem item, @Param("tableName") String tableName);
}
步骤2:使用工具类进行批量插入

可以选择同步或异步批量插入方法,具体取决于需求。

5. 使用案例

假设我们有一个MyItem类和一个MyMapper接口。我们将演示如何使用BatchInsertUtil进行批量插入操作。

示例代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

public class BatchInsertDemo {
    public static void main(String[] args) {
        // 创建待插入的对象列表
        List<MyItem> itemList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            MyItem item = new MyItem();
            item.setColumn1("Value1_" + i);
            item.setColumn2("Value2_" + i);
            itemList.add(item);
        }

        // 使用批量插入工具类进行批量插入
        BatchInsertUtil.batchInsert(itemList, MyMapper.class);

        // 使用多线程同步批量插入
        BatchInsertUtil.syncBatchInsert(itemList, MyMapper.class);

        // 使用多线程异步批量插入
        List<Future<Void>> futures = BatchInsertUtil.asyncBatchInsert(itemList, MyMapper.class);
        BatchInsertUtil.waitForAll(futures);

        // 分表情况下的批量插入
        String tableName = "my_table_partition";
        BatchInsertUtil.batchInsert(itemList, MyMapper.class, tableName);
        BatchInsertUtil.syncBatchInsert(itemList, MyMapper.class, tableName);
        futures = BatchInsertUtil.asyncBatchInsert(itemList, MyMapper.class, tableName);
        BatchInsertUtil.waitForAll(futures);
    }
}

// MyItem类的定义
class MyItem {
    private String column1;
    private String column2;

    // Getters and Setters
    public String getColumn1() {
        return column1;
    }

    public void setColumn1(String column1) {
        this.column1 = column1;
    }

    public String getColumn2() {
        return column2;
    }

    public void setColumn2(String column2) {
        this.column2 = column2;
    }
}

详细步骤

  1. 创建待插入的对象列表:创建一个包含需要插入数据的列表itemList

  2. 调用批量插入方法

    • 简单批量插入:使用BatchInsertUtil.batchInsert方法进行简单批量插入。
    • 多线程同步批量插入:使用BatchInsertUtil.syncBatchInsert方法进行多线程同步批量插入。
    • 多线程异步批量插入:使用BatchInsertUtil.asyncBatchInsert方法进行多线程异步批量插入,并使用BatchInsertUtil.waitForAll方法等待所有异步任务完成。
  3. 分表情况下的批量插入:在需要分表插入的情况下,提供表名参数。

注意事项

  • 确保SpringContextUtil已正确配置并能够获取到SqlSessionFactory
  • 确保在Mapper接口中正确使用了@BatchInsert注解。
  • 在多线程异步批量插入时,确保任务列表futures中的所有任务都已完成。

通过以上步骤和示例代码,可以有效地使用BatchInsertUtil进行批量插入操作,提高数据库操作的性能和效率。

6. 总结

MyBatis的批量插入使用这个工具类效率可以极大提升,当然基于这个思路还有更多优化的地方,本文仅抛砖引玉,希望大家提出更好的方案,让我们一起进步!

感谢观看 (* ̄︶ ̄)

你可能感兴趣的:(mybatis,oracle,数据库)