大数据量写入场景问题

乐云一
  • 业务
  • 业务
About 3055 wordsAbout 10 min

大数据量写入场景问题

背景

有做对账、数据同步、转移库等等业务或系统的同学可能都经历一个问题:数据量过大,引起的一系列慢、坏、缺的问题

针对大数据写入这一问题,小到一个方法调用,大到程序设计,在语言程序编写上来看一直以来是一种美学学问

因此,披露大数据写入的细节,对于程序员的我们来说,无非是一种并发理解的享受;

对于这一经典的并发场景问题,一直以来争论的是三点:

  • 事务
  • 数据量

不过可惜的是,像是数学中的三角图形一样,无论如何设计都无法去保证这个三角形的绝对完美。

追求快,那么就难保证事务;

追求事务,则无法使之快;

数据量则是两者的催化剂;

但是在各个 “顶点”来看,大数据量写入的需求,可以去结合本身的特殊性进行分类讨论,使之功能趋近完美;

分类讨论

模拟一个只需要快的需求:

爬虫,比如领导下达一个任务:帮我把公司所有人在GitLab上的代码提交信息与详情保存记录下来,并且进行每日更新

爬取GitLab每个人的提交记录,可能大家不知道,这确实是领导为了管理总结可以提出的离谱需求,至少我已经遇到了

首先已知,从Git上拉取下来的数据超10W条,因为只需内部查阅问题,所以不要求数据的准确与稳定。

那么在这个三角形中,事务不保证下的大数据量写入,完美的方案只有快。

对于快来说,大体上有两种对策:

  1. 多线程插入
  2. 文件移植

多线程插入

根据需求讨论,一般来说会出现两种情况:

  • 可异步等待的大数据插入业务
  • 同步等待的大数据插入业务
可异步等待的大数据插入业务

前者则是类似于网页版的文件上传功能,当批量上传时,可以直观的看到哪些数据在插入中,哪些插入完成以及等待插入;

因此对于这种允许等待的异步插入数据形式的功能,往往偏向业务插入功能:在大数据插入的问题上还需要进行业务标识的写入写出的控制

同时还需要分批次的进行插入,多的还需关注数据上传时的原本数据顺序;

看到这不知大伙脑子有没有针对这类功能的第一方案?

顺序,业务标识写入写出,大数据量分批次插入

当前,消息队列完全可以做到以上几点:自带高可用、幂等重复消费处理、可定制顺序消费....

因此根据以上可以写出以下简单的伪代码:

public void bitWrite(){
    List<数据> list = new ArrayList();
   	//数据分批次
    list.subList();
	/**
		根据方法内等待 / 页面等待
		选择使用线程池/响应式函数/Http回写等方式
		进行mq数据的传输
	*/
    ()->{rabbitTemplate.send()};
}

@RabbitListener(bindings = {
    @QueueBinding(value = @)
})
public void messageConsumer() {
    //消息消费
}

当然了,以上只是大数据插入的前驱动作,真正写入数据库中的 后续会讨论到

同步等待的大数据插入业务

不允许等待功能,例如银行中的数据库对账、数据迁移、页面loading等等等等,大部分写入需求都趋近于在不影响使用体验下的不允许等待。

在其中有一个因素干扰:无论是否保证事务,保证快的同时,一定要确保数据的失败时可进行补偿

即,一个数据插入失败时,可以记录为后续补偿机制再次写入。

问题演变为: 主线程等待大数据插入 + 支持补偿机制

同时如果插入需要注重顺序,那么在开启多线程插入操作的同时,还需要进行A\B\C锁或奇偶数锁等...,进行1\2\3\4 的顺序数组可交替执行数据库插入动作。

  • 对于补偿机制,记录本身就是一种很简单的事,是开启辅助线程try-catch 对失败数据进行写表记录,还是直接在主线程方法中声明List都可。

因此根据以上可以写出以下简单的伪代码:

public void mian() {
        List<数据> list = new ArrayList();
    
		final CountDownLatch countDownLatch = new CountDownLatch(4);
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i <= 3; i++) {
            //拆分数据
            list.subList();
            executorService.submit(() -> {
                        try {
                            //写入数据
                            dao.write();
                            System.out.println("子线程完成" + count+"当前:"+countDownLatch.getCount());
                            countDownLatch.countDown();
                        } catch (InterruptedException e) {
							/**
								记录失败数据
							**/
                        }
                    }
            );
        }
        countDownLatch.await();
}
数据库插入

最终,不管哪种入口进行的大数据写入,都逃不开如何做到大数据量插入 这个问题

批量插入:使用JDBC的批量插入功能,而不是逐一插入。Spring JdbcTemplate提供了batchUpdate方法来实现这一点。 异步处理:考虑使用Spring的@Async注解来异步处理数据写入,这样主线程不会被阻塞,可以继续处理其他任务。 数据库连接池:使用数据库连接池(如HikariCP、c3p0或Tomcat连接池)来管理数据库连接。这可以重用连接,避免频繁地建立和关闭连接。 使用适当的JDBC驱动:确保您使用的JDBC驱动与您的数据库版本兼容,并优化了性能。 调整数据库配置:根据您的数据库,调整一些参数以获得更好的写入性能。例如,对于MySQL,您可以考虑调整innodb_buffer_pool_size、bulk_insert_buffer_size等参数。 合理的数据模型设计:使用合适的数据模型和索引策略可以显著提高写入性能。避免全表扫描和考虑使用适当的索引。 数据分批处理:不要一次性插入所有数据。考虑将数据分成小批次插入,这样可以减少每次插入的数据量,从而减少数据库的负载。 关闭自动提交事务:关闭JdbcTemplate的自动提交事务功能,手动控制事务可以提高批量插入的性能。 使用适当的SQL语句:避免使用复杂的SQL语句或触发器等,简单的INSERT语句通常更快。 监控和调优:使用数据库的性能监控工具,并根据监控结果进行相应的调优。

以上是来自 人工智能 的回答 ;

首先我们在Sql语句操作这以层级上,可以直接使用MyBatis-plus的批量插入操作,因为他是基于原JDBC中的executeBatch()优化开发的,当然了各类框架的批量处理插入的方法都已经是 insert 的最优解了。

然后是针对Mysql数据库配置的优化,这一点就很揽括了,简单概括就是:最大的连接池大小、最大的缓存区大小、关闭自动提交采用手动提交事务、使用InnoDB引擎、针对表不设置外键、索引等等约束、缩小表字段体积......

文件移植

如果大数据写入数据库这一步,担心事务的同时又害怕数据过慢,何不来考虑直接将数据文件写入到数据库中。

数据有两种存在形式:

  1. 经过业务方法后形成的数据
  2. 已经是文件形式存在的数据

前者

准确的来说是如何将前者转变为后者,因为我们的目标是如何将数据文件写入到数据库中。

那么我们可以使用目前市面上主流的各类导出功能的插件,POIEasyExcel ...等等,也可以直接将其写入txt文本中,使用\n区分一条条数据

后者

文件如何导入数据库中,两种方式:

  • 使用数据库工具自带的导入功能
  • LOAD FILE()函数

第一种不提,第二种则是Mysql中的函数,因为其他数据库未涉及过大数据写入的功能;

LOAD FILE()函数,相当于Mysql将你写入的文件,由它自己进行管理与解析,我们只需要告诉引擎,这个文件怎么分行数据、这个字符对应的是什么字段;

由于是直接进行的IO传输,所以完全不用考虑性能耗损。并且交予Mysql管理,可以做到事务的一致。

因此使用LOAD FILE() 函数是大数据写入的最快的方式;

除此之外,也可以使用 存储过程的方式写入数据,不过因为这个技术已经被淘汰甚至被禁用,所以不多赘述。

事务

如果即要快又要保证事务,这就有点强人所难了,首先说难点;

多线程事务 = 分布式事务

而分布式事务本身是一个节点多,管理漫长且回滚效率低下的事。

因此不管多线程事务的事务管理者设计的有多好,一个个节点操作的这个事实是无法改变的,

因此针对大数据量插入时的事务问题,如果有一种好的方式,他不会是常见的手动开启-提交事务一条回滚或全部失败的原子性...等事务的基本处理方式

推荐采取业务型的事务回滚,比如:

一条失败,全部失败 = 插入时记录操作标识,失败时将符合这个标识的数据全部删除

补偿 = 记录

...

所以大数据写入的事务公式可以这样套用:

小批次范围内 = 手动开启提交事务
多批次事务下 = 业务级数据回滚

但是,在设计为主键自增的数据表中,这样的业务级操作多少会有些污染数据库了,并且插入额外的错误数据,大量时会影响到快的动作,所以必要的时候可以进行针对性的升级处理:

  • 准备空表,临时表,数据写入后进行merge into,合并表
  • 一条数据失败时,将数据交给消息中间件消费,分解成一个一个的事务
  • ...

以上是对于不需要保证数据顺序的讨论,如果需要保证顺序的同时还支持事务,除了上锁+等待事务提交,还真想不出什么完美策略应对

总结

记忆里,从入行开始的第一个面试就是问:100W的数据如何快速导入数据库中;

当时的我会给出一个很坚定的技术方案:IO流一行行解析,批处理插入

接触到了形形色色的需求后,面对这个经典的问题真的会犯难 : 可不可以告诉我具体的需求细节

经典的尽头永远都是分类讨论,经典也因此永不过时

Last update:
Contributors: LeYunone
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.14.7