数据处理
在项目开发中,数据处理是一个至关重要的环节,包括数据的导入、存储和查询等。本文将详细介绍数据处理中的一些关键技术和优化方法。
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表格内容为:
读取结果:
这种方式的优点是可以将数据直接映射到对象中,方便后续的处理和使用。同时,通过监听器的方式,可以单独抽离处理逻辑,代码清晰易于维护,并且适用于数据量大的场景,一条一条地处理数据,避免一次性加载大量数据导致内存占用过高。
第二种方式
使用同步读取:
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);
}
}
运行结果:
这种方式无需创建监听器,一次性获取完整数据,方便简单。但在数据量大的时候,可能会出现卡顿的情况,因为它需要一次性将所有数据加载到内存中。
两种读取模式:
- 监听器:先创建监听器,在读取文件时绑定监听器,单独抽离处理逻辑,代码清晰易于维护,一条一条处理,适用于数据量大的场景
- 同步读,无需创建监听器,一次性要获取完整数据,方便简单,数据量大的时候卡顿
1000万数据导入
在处理大量数据导入时,需要考虑效率和性能问题。以下是几种常见的导入数据的方式:
- 可视化界面:适合一次性导入,数据量可控。但对于大规模数据导入,可能不太方便。
- 写程序:使用 for 循环进行导入,建议分批处理,以保证可控性。
- 执行 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());
}
耗时比较大,主要花在数据库链接上
优化,分批插入
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());
}
这次插入了1000条数据,耗时只有1秒多
测试十万条:14s
分批插入的方式可以减少数据库 连接的次数,提高插入效率。通过将数据分成若干批进行插入,可以降低每次插入的数据量,减少数据库的压力。
并发执行
十万条数据导入耗时为:9s
修改 BatchSize 为 10000 时:6s
其他办法:
@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());
}
}
并发执行可以充分利用多核处理器的优势,提高数据导入的速度。通过使用线程池和异步任务,可以同时处理多个批次的数据插入,减少总耗时。