跳到主要内容

数据处理

在项目开发中,数据处理是一个至关重要的环节,包括数据的导入、存储和查询等。本文将详细介绍数据处理中的一些关键技术和优化方法。

EasyExcel导入数据

EasyExcel 是一个方便的 Excel 数据导入工具,官网为:https://easyexcel.opensource.alibaba.com/。

        <dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>

EasyExcel 提供了两种读方式:

  • 确定表头:建立对象,将 Excel 中的数据映射到对象的属性中。
  • 不确定表头:每一行数据映射为Map<String, Object>

第一种方式

创建对象:

@Data
public class iKun {
@ExcelProperty("ikun编号")
private String ikunCode;
@ExcelProperty("ikun名称")
private String username;
}

创建监听器:

@Slf4j
public class TableListener implements ReadListener<iKun> {


/**
* 这个每一条数据解析都会来调用
*/
@Override
public void invoke(iKun data, AnalysisContext context) {
System.out.println("解析到一条数据:{}" + data);

}

/**
* 所有数据解析完成了 都会来调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("以解析完成");

}
}

主程序:

@Slf4j
public class ImportExcel {
public static void main(String[] args) {
// 写法1:JDK8+ ,不用额外写一个DemoDataListener
// since: 3.0.0-beta1
String fileName = "/Users/houyunfei/资料/备战秋招/ikun伙伴匹配系统/ikunfriend-back/src/main/resources/testExcel.xlsx";
// 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
EasyExcel.read(fileName, iKun.class, new TableListener()).sheet().doRead();
}
}

Excel表格内容为:

image-20231021105337004

读取结果:

image-20231021105355932

这种方式的优点是可以将数据直接映射到对象中,方便后续的处理和使用。同时,通过监听器的方式,可以单独抽离处理逻辑,代码清晰易于维护,并且适用于数据量大的场景,一条一条地处理数据,避免一次性加载大量数据导致内存占用过高。

第二种方式

使用同步读取:

    public static void main(String[] args) {
synchronousRead ();
}

public static void synchronousRead() {
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 同步读取会自动finish
List<iKun> totalList = EasyExcel.read(fileName).head(iKun.class).sheet().doReadSync();
for (iKun iKun : totalList) {
System.out.println(iKun);
}
}

运行结果:

image-20231021105902589

这种方式无需创建监听器,一次性获取完整数据,方便简单。但在数据量大的时候,可能会出现卡顿的情况,因为它需要一次性将所有数据加载到内存中。

两种读取模式:

  1. 监听器:先创建监听器,在读取文件时绑定监听器,单独抽离处理逻辑,代码清晰易于维护,一条一条处理,适用于数据量大的场景
  2. 同步读,无需创建监听器,一次性要获取完整数据,方便简单,数据量大的时候卡顿

1000万数据导入

在处理大量数据导入时,需要考虑效率和性能问题。以下是几种常见的导入数据的方式:

  1. 可视化界面:适合一次性导入,数据量可控。但对于大规模数据导入,可能不太方便。
  2. 写程序:使用 for 循环进行导入,建议分批处理,以保证可控性。
  3. 执行 SQL 语句:适用于小数据量的导入。

普通插入

    @Test
public void doInsertUsers() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
final int NUM = 10;
for (int i = 0; i < NUM; i++) {
User user = new User();
user.setUsername("假ikun");
user.setUserAccount("fakeIkun");
user.setAvatarUrl("https://s2.loli.net/2023/10/16/QRiUYmDLB2vZuE6.webp");
user.setGender(0);
user.setUserPassword("12345678");
user.setPhone("123");
user.setEmail("123@qq.com");
user.setUserStatus(0);
user.setUserRole(0);
user.setIkunCode("1212121");
user.setTags("[]");
userMapper.insert(user);
}
stopWatch.stop();
System.out.println("总时间:" + stopWatch.getTotalTimeMillis());
}

image-20231022145404843

耗时比较大,主要花在数据库链接上

优化,分批插入

    public void doInsertUsers() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
final int NUM = 1000;
List<User> userList = new ArrayList<>();
for (int i = 0; i < NUM; i++) {
User user = new User();
user.setUsername("假ikun");
user.setUserAccount("fakeIkun");
user.setAvatarUrl("https://s2.loli.net/2023/10/16/QRiUYmDLB2vZuE6.webp");
user.setGender(0);
user.setUserPassword("12345678");
user.setPhone("123");
user.setEmail("123@qq.com");
user.setUserStatus(0);
user.setUserRole(0);
user.setIkunCode("1212121");
user.setTags("[]");
userList.add(user);
}
userService.saveBatch(userList,100);
stopWatch.stop();
System.out.println("总时间:" + stopWatch.getTotalTimeMillis());
}

image-20231022150102289

这次插入了1000条数据,耗时只有1秒多

测试十万条:14s

image-20231022150538228

分批插入的方式可以减少数据库连接的次数,提高插入效率。通过将数据分成若干批进行插入,可以降低每次插入的数据量,减少数据库的压力。

并发执行

十万条数据导入耗时为:9s

image-20231022151327498

修改 BatchSize 为 10000 时:6s

image-20231022151647078

其他办法:

@SpringBootTest
class InsertUsersTest {
@Resource
private UserMapper userMapper;

@Resource
private UserService userService;

private ExecutorService executorService = new ThreadPoolExecutor(60, 1000, 10000,
TimeUnit.MINUTES, new ArrayBlockingQueue<>(10000));


/**
* 批量插入用户
*/
@Test
public void doInsertUsers() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
final int NUM = 100000;
int batchSize = 5000;
//分10组
int j = 0;
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < 20; i++) {
List<User> userList = new ArrayList<>();
while (true) {
j++;
User user = new User();
user.setUsername("假ikun");
user.setUserAccount("fakeIkun");
user.setAvatarUrl("https://s2.loli.net/2023/10/16/QRiUYmDLB2vZuE6.webp");
user.setGender(0);
user.setUserPassword("12345678");
user.setPhone("123");
user.setEmail("123@qq.com");
user.setUserStatus(0);
user.setUserRole(0);
user.setIkunCode("1212121");
user.setTags("[]");
userList.add(user);
if (j % batchSize == 0) break;
}
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Thread name:" + Thread.currentThread().getName());
userService.saveBatch(userList, batchSize);
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{})).join();
stopWatch.stop();
System.out.println("总时间:" + stopWatch.getTotalTimeMillis());
}
}

并发执行可以充分利用多核处理器的优势,提高数据导入的速度。通过使用线程池和异步任务,可以同时处理多个批次的数据插入,减少总耗时。