首页 > 其他分享 >批量操作DB

批量操作DB

时间:2024-11-28 11:36:30浏览次数:5  
标签:insert 批量 saveBatch DB sqlSession ............ 操作 public

批量操作DB

记录一次批量操作数据库,sqlServer服务器参数过多问题。

1.案例引入

对接另一个批发系统B,B需要把订单那些数据弄到系统A中。就是一个批量插入,批量更新的问题。系统保密需要,本文只做示例,具体代码都是模拟的。 (SpringBoot项目)

涉及到的操作类和数据库表

Order_master表【主订单表】

Order_detail表【自订单表,记录主订单里面各个商品品种购买详情,字段数量假设有30个】

Order实体类

OrderMapper

OrderSercice

假设一下批量采购了7500个商品,是不同的种类,

2.old方案

都知道是批量插入数据的吧,构建好数据之后,然后批量插入数据库里面就完事儿了。

//Service
public void insertFun(xxx) {
    ....... 构建数据
    orderDetailMapper.insertOrderList(xxx);
}
<!-- xml -->
<insert id="insertOrderList" parameterType="orderDetail">
        INSERT order_detail (xxxx)VALUES
        <foreach collection="list" item="info" separator="," open="(" close=")">
            #{info.1}, #{info.2}, #{info.3}, #{info.4}, #{info.5}......
        </foreach>
    </insert>

但是这样会有问题。数据库服务器会报错

究其原因: 因为sql server对参数有控制

上面的方法,最终的sql语句为

INSERT order_detail (xxxx)VALUES
(?, ?, ?, ?, ?, ............),
(?, ?, ?, ?, ?, ............),
(?, ?, ?, ?, ?, ............),
.....
(?, ?, ?, ?, ?, ............)

-- 每个对象有30个参数,30 * 7500 = 225000 个参数。。肯定超过了其限制涩。

3.解决方案一

最扯淡的方案:

// service
public void insertFun(xxx) {
    ....... 构建数据
	for ( int i = 0; i < size; ++i ) {
       orderDetailMapper.insert(list.get(i));
    }
}

确实可以解决2100参数的问题。。。。

靠谱一点儿的方案

2100 / 30 = 70 . 我每七十个为一个List,然后批量插入进入不就行了吗?

// service
public void insertFun(xxx) {
    ....... 构建数据
    // 分批,每70个为一批次
	List<List<OrderDetail>> ps = Lists.partition(list, 70);
	for ( List<OrderDetail> t : ps ) {
       orderDetailMapper.insertOrderList(t);
    }
}

这样虽然解决了这个问题,但是,每一次循环伴随SqlSession的打开和关闭,都是一个单独的事务提交

有7500个商品,7500 / 70≈107。就意味着107SqlSession的打开和关闭,这个是很耗时间的呀。看来还是不能这样搞。

begin
INSERT order_detail (xxxx)VALUES
(?, ?, ?, ?, ?, ............),
......
(?, ?, ?, ?, ?, ............)
commit

begin
INSERT order_detail (xxxx)VALUES
(?, ?, ?, ?, ?, ............),
......
(?, ?, ?, ?, ?, ............)
commit

......
begin
INSERT order_detail (xxxx)VALUES
(?, ?, ?, ?, ?, ............),
......
(?, ?, ?, ?, ?, ............)
commit

那我们就要想办法减少这个连接次数了。

4.saveBatch

然后发现了mybatis-plus的IService接口中的saveBatch。

我们点进去源码查看一下。

// IService.class==========================
public interface IService<T> {
    @Transactional(rollbackFor = {Exception.class}) // 这里有这个注解哦。
    default boolean saveBatch(Collection<T> entityList) {
        return this.saveBatch(entityList, 1000); // 调用了这个saveBatch,可见默认size是1000
    }
    boolean saveBatch(Collection<T> entityList, int batchSize); // 就是这个
}


//ServiceImpl.class==================
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
    @Transactional(rollbackFor = {Exception.class})
    public boolean saveBatch(Collection<T> entityList, int batchSize) {
        // SqlMethod.INSERT_ONE是一个枚举
        /*
         INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>\nINSERT INTO %s %s VALUES %s\n</script>"),
        */
        String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);
        // 继续向下执行这个, 第三个参数类型BiConsumer,接收两个参数,不返回东西
        return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {
            sqlSession.insert(sqlStatement, entity);
        });
    }
    
    // 执行这个
    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        // 调用了SqlHelpr的方法
        return SqlHelper.executeBatch(this.entityClass, this.log, list, batchSize, consumer);
    }
}


//SqlHelper.class====================
public final class SqlHelper {
    public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);
        // 第三个参数Consumer。
        return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {
            int size = list.size();
            int i = 1;
			// 遍历实体对象的List,
            for(Iterator var6 = list.iterator(); var6.hasNext(); ++i) {
                E element = var6.next();
                consumer.accept(sqlSession, element); // 执行传过来的逻辑sqlSession.insert(sqlStatement, entity);
                if (i % batchSize == 0 || i == size) { // 每一个批次执行一下。(这里是1000)
                    sqlSession.flushStatements();
                }
            }

        });
    }
    
    
     public static boolean executeBatch(Class<?> entityClass, Log log, Consumer<SqlSession> consumer) {
        .......
            
        sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); // 使用 `ExecutorType.BATCH`表示使用批量操作模式,MyBatis 会将多个 SQL 语句累积起来,而不是每执行一条 SQL 就与数据库交互。
        if (!transaction) {
            log.warn("SqlSession [" + sqlSession + "] was not registered for synchronization because DataSource is not transactional");
        }
        boolean var7;
        try {
            consumer.accept(sqlSession); //---执行传过来的逻辑,就是上面的那个Consumer
            sqlSession.commit(!transaction);
            var7 = true;
        } catch (Throwable var13) {
            .......
        } finally {
            sqlSession.close();
        }

        return var7;
    }
}

从源码中可以发现saveBatch是下面这种情况的

open sqlsession........
begin
sqlSession.insert(sqlStatement, entity);
sqlSession.insert(sqlStatement, entity);
...
sqlSession.insert(sqlStatement, entity); // 1000个
sqlSession.flushStatements();

sqlSession.insert(sqlStatement, entity);
sqlSession.insert(sqlStatement, entity);
...
sqlSession.insert(sqlStatement, entity); // 1000个
sqlSession.flushStatements();
如此循环。。。。。

commit

close sqlsession............

sqlSession.flushStatements()的作用

flushStatements() 方法主要用于批量操作时手动提交累积的 SQL 语句。当使用 ExecutorType.BATCH 时,所有的 SQL 语句会被缓存,直到你调用 flushStatements(),MyBatis 会将缓存中的 SQL 语句一次性提交给数据库执行。这个并不会提交事务哦

我们什么时候可以调用 flushStatements()

  • 批量大小控制:如果你的批量数据量比较大,可以控制每批次提交的数据量。通常情况下,合理的批量大小应该根据数据库和应用服务器的配置来调整,太大的批量会导致内存占用过高,太小的批量又不能有效利用批量操作的优势。
  • 减少内存压力:如果一次性积累的 SQL 语句过多,会占用大量内存,甚至导致内存超限错误。通过手动调用 flushStatements() 来减少内存的占用,每次提交一个小批次的 SQL 语句。我想这就是默认1000个的原因吧。批量操作时需要分批提交 SQL 语句,减少内存占用、提高性能

这样就可以批量插入数据,同时又避免了2100个参数的问题

this.saveBatch(整个list)

5.更快的?

结合数据库sqlserver有2100参数限制的问题,还有上面的saveBatch实际上是多条insert语句一起提交到数据库中的,我们可以想到一个更加优秀的解决方案。

  • 首先,把一条一条执行insert改为批量的
  • 同时,要注意每一条sql的参数限制
  • 仿照mybatis-plus源码大致思路执行

每行记录有30个字段,那么,2100/30 = 70,也就是说,要想办法将sql变成下面这个样子的

insert into table1 (x,x,x,....x) values
(x,x,x, .......),(x,x,x, .......),(x,x,x, .......)
(x,x,x, .......).....【70个】

那么,mapper.xml里面的东西可以先不变,就如同第2节里面的<foreach>标签拼接

下面给出实现大致细节。数据源配置,和maven依赖就不放了。

首先,mybatis-plus配置类

@Configuration
public class MybatisPlusConfig {
    //SqlSessionFactory 是 MyBatis 会话工厂,负责创建和配置 SqlSession 实例。这样我们就可以拿到sqlsession了
    @Bean
    public SqlSessionFactory sqlSessionFactory(org.apache.ibatis.session.Configuration configuration) throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setConfiguration(configuration);
        factoryBean.setDataSource(dataSource()); // 注入数据源
        return factoryBean.getObject();
    }

    //SqlSessionTemplate 是 MyBatis 的 SqlSession 的封装,简化了事务的管理。
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory); // 创建 SqlSessionTemplate
    }
}

serviceImpl类 手动控制

@Service
public class MyService {
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Autowired
    private OrderDetailMapper orderDetailMapper;

    public void myBatchInsert(List<OrderDetail> details) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            OrderDetailMapper mapper = sqlSession.getMapper(OrderDetailMapper.class);

            List<List<OrderDetail>> pats = Lists.partition(details, 65); // 这里调小一丢丢
            int batchCount = 0;
            for (List<OrderDetail> tmp : pats ) {
                mapper.insertOrderList(tmp);
                batchCount++;
                if ( batchCount % 16 == 0 ) { // 16 * 65 = 1040,一个批次插入1040个
                    sqlSession.flushStatements(); // 提交当前批次
                }
            }
            sqlSession.commit(); // 提交事务,持久化到数据库
        } catch(Exception e ) {
            ................
        }
    } 
}

手动挡开着还是爽一些啊,。。

后面我还发现,有这样一个配置jdbc url参数上加rewriteBatchedStatements=true,这个就是把saveBatch由一条一条insert变成了一大条insert,但是一条insert语句太长了的话,sql解析起来估计会更慢了,还是要综合考虑sqlsession的打开与关闭次数sql数据库解析sql的速度这三者,才能实现最快最高效的插入数据。 如何综合考虑,得出一个最优方案,估计只有不断测试了,因为每台服务器的配置都不相同。

标签:insert,批量,saveBatch,DB,sqlSession,............,操作,public
From: https://www.cnblogs.com/jackjavacpp/p/18573941

相关文章

  • IndexedDB
    IndexedDB简介MDN官网是这样解释IndexedDB的:IndexedDB是一种底层API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该API使用索引实现对数据的高性能搜索。虽然WebStorage在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而I......
  • MongoDB测试环境搭建分片脚本
    搭建1个config节点,一个mongos节点,两个分片,每个分片3个节点。传入参数为mongos节点端口号,config节点端口号,分片节点端口号可根据需求修改存放的目录basedir#!/bin/bash#定义usage函数usage(){echo"Usage:mongos_portconfig_portshard_node_port_1....shard_......
  • 046_pdb_debug_调试赋值语句_先声明赋值_再使用
    pdb_debug_调试赋值语句_先声明赋值_再使用回忆上次内容上次讲了赋值assignment亲手将值指派到变量中   添加图片注释,不超过140字(可选) 赋值语句能运行起来吗?......
  • Oracle生成awr报告操作步骤
    1、cmd命令窗口 以sysdba身份登录Oracle 2、执行@?/rdbms/admin/awrrpt命令,并选择报告类型为HTML。输入天数以选择生成报告的时间段,一般默认为最近7天。输入报告开始和结束时间对应的快照ID。输入报告名称,如awr.html,系统将自动生成并显示报告名。 3.查看AWR报告。AWR报告......
  • hhdb数据库介绍(9-13)
    计算节点特色功能SQL流控计算节点支持对高并发SQL语句进行自动的流量控制功能,可以自动识别高消耗SQL且限制同一时间内高消耗SQL执行的并发数和效率,从而稳定数据库服务。也提供查询入口使用户尽可能多的识别高消耗SQL语句并优化SQL以提升数据库整体服务性能。当正在执行的SQL缓......
  • hhdb数据库介绍(9-14)
    函数与操作符计算节点对函数的支持此文档仅列出部分经特殊处理的函数,若需要了解所有计算节点支持的函数,请向官方获取《计算节点最新功能清单》。函数名称支持状态是否拦截说明ABS()支持否ACOS()支持否ADDDATE()支持否ADDTIME()支持否AES_DE......
  • 【计算机视觉】图像基本操作
    图像基本操作数字图像表示一幅尺寸为M·N的图像可以用矩阵表示,每个矩阵元素代表一个像素,元素的值代表这个位置图像的亮度;其中,彩色图像使用3维矩阵M·N·3表示;对于图像显示来说,一般使用无符号8位整数来表示图像亮度,取值范围[0,255]图像数据按照自左向右、自上向下的顺......
  • hhdb数据库介绍(9-12)
    计算节点特色功能在线修改分片字段支持在服务端口直接使用SQL语句进行在线修改分片字段的操作,业务表在变更期间不会锁表,业务可对原表进行正常的SIUD操作。使用方法alter修改分片字段语法如下:altertabletable_namechangeshardcolumnnew_column;例如将源表sbtest1分片字......
  • Linux操作系统3:用户,用户组,权限
    Linux用户,用户组,权限用户和用户组基础1.计算机通过用户名和密码识别用户;2.Linux中新建用户后,需要设置密码;3.系统中有一个特殊的用户root;4.用户组:把几个用户归在一起,这样的组被称之为用户组,规定文件所属用户组的权限可以使该组所有成员拥有这些权限。普通用户和最高管......
  • 文件操作.C
    一、文件如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤⽂件磁盘(硬盘)上的⽂件是⽂件但是在程序设计中,我们⼀般谈的⽂件有两种:程序⽂件、数据⽂件......