大批量数据导入接口的优化

在工作需求中遇到了一个导入客户数据的需求,因为项目本身使用若依框架,所以直接使用了若依框架自带的easypoi进行导出.

但是后续发现,easypoi在数据量较大的时候会占用非常多的内存,导入10M的excel,占用内存接近1G,这是不可接受的.所以后续先是优化成了阿里的easyExcel,easyExcel对内存方面进行了优化,分片操作,提升了内存和速度,整体要比easypoi优秀很多.代码为:

csharp 复制代码
    @RepeatSubmit
    @ApiOperation("2.3.2个人线索excel导入")
    @PostMapping("/importClue")
    public R importClue(@RequestPart("file") MultipartFile file) throws Exception {
        Long userId = SecurityUtils.getUserId();
        // 使用 EasyExcel 读取数据
        ClientImportDTOListener listener = new ClientImportDTOListener();
        EasyExcel.read(file.getInputStream(), ClientImportDTO.class, listener).sheet().doRead();
        // 获取所有数据
        List<ClientImportDTO> userList = listener.getAllDataList()
        }
@Slf4j
public class ClientImportDTOListener implements ReadListener<ClientImportDTO> {
    private static final int BATCH_COUNT = 50;
    private List<ClientImportDTO> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    @Getter
    private List<ClientImportDTO> allDataList = new ArrayList<>();
    private static final int MAX_RECORD_COUNT = 20000; // 最大记录数
    private int totalRecordCount = 0; // 记录总数
    public void invoke(ClientImportDTO data, AnalysisContext context) {
        totalRecordCount++;  // 计数每一条记录
        // 如果总记录数超过2万条,抛出异常并停止读取
//        if (totalRecordCount > MAX_RECORD_COUNT) {
//            throw new IllegalArgumentException("数据超过两万条, 请拆分至两万条以内, 分批上传!");
//        }
        // 将数据缓存到临时列表中
        cachedDataList.add(data);
        // 如果缓存达到批次大小,将数据加入到 allDataList 中,并清空缓存
        if (cachedDataList.size() >= BATCH_COUNT) {
            allDataList.addAll(cachedDataList);
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        allDataList.addAll(cachedDataList);
        log.info("所有数据解析完成,收集到 {} 条数据", allDataList.size());
    }

}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ClientImportDTO {
    @Excel(name = "客户姓名")
    @ExcelProperty("客户姓名")
    private String name;

    @Excel(name = "手机号")
    @ExcelProperty("手机号")
    private String phone;

    @Excel(name = "获客渠道")
    @ExcelProperty("获客渠道")
    private String source;
}

这里因为篇幅原因,删除了部分ClientImportDTO 的字段.这里也不是重点.

然后在导入接口处打了日志.发现把excel转对象确实非常快,2万条只要1秒左右,但是转成对象之后保存数据库还有大量的逻辑,这样的逻辑导致接口速度非常的慢,2万条要2分钟才能响应,对应前端就是用户在导入之后要转圈两三分钟才能获取到导入成功的结果,整体体验较差.

于是在这里又进行了进一步的优化,这里采用的方案是事件发布机制.

创建一个监听者,监听导入事件的发布,在导入接口从excel转换为对象列表之后触发该事件,监听者监听到该事件,然后去触发导入的逻辑操作,从而实现后台录入数据并分配,前台直接返回数据操作中,并通过消息来返回结果.

具体代码为:

csharp 复制代码
    @RepeatSubmit
    @ApiOperation("2.3.2个人线索excel导入")
    @PostMapping("/importClue")
    public R importClue(@RequestPart("file") MultipartFile file) throws Exception {
        Long userId = SecurityUtils.getUserId();
        // 使用 EasyExcel 读取数据
        ClientImportDTOListener listener = new ClientImportDTOListener();
        EasyExcel.read(file.getInputStream(), ClientImportDTO.class, listener).sheet().doRead();
        // 获取所有数据
        List<ClientImportDTO> userList = listener.getAllDataList();
        // 调用异步方法发布事件clueService.importBatch2Async
        clueAsyncService.importClueAsync(userList, userId);
        return R.ok(null,"数据导入已开始,处理过程将在后台完成!");
    }

在这个过程中,又出现了循环依赖的问题,因为启动项目的载入顺序,导致无法获取到对应的service,后来的解决方案是采用构造器注入.

对应的异步service为:

csharp 复制代码
@Service
public class ClueAsyncService {

    private final ApplicationEventPublisher eventPublisher;

    @Autowired
    public ClueAsyncService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    /**
     * 异步导入个人线索
     */
    @Async
    public void importClueAsync(List<ClientImportDTO> userList, Long loginId) {
        // 发布事件,触发异步处理
        ImportClueEvent event = new ImportClueEvent(this, userList, loginId);
        eventPublisher.publishEvent(event);
    }
}

监听器代码:

csharp 复制代码
@Component
public class ImportClueEventListener {
@Resource
private ClueService clueService;
    @Async
    @Transactional(rollbackFor = Exception.class)
    @EventListener
    public void handleImportClueEvent(ImportClueEvent event) {
        // 获取事件数据
        List<ClientImportDTO> userList = event.getUserList();
        Long loginId = event.getLoginId();
        // 执行实际的异步处理逻辑,此时你可以在此方法内部调用同步方法
        clueService.importBatch2Async(userList,loginId);
    }
}

这样就能够保证监听,触发,执行逻辑的逻辑通顺.而用户会在很快的时间内直接返回"数据导入已开始,处理过程将在后台完成!".结果通过websocket发送到消息中:

该方法适用于不仅导入数据量大,导入后逻辑还复杂的场景.

如果仅仅是导入数据量大,但无后续复杂逻辑,直接入库,则不需要使用该方法,直接easyExcel再配上分次批量存储即可.

相关推荐
love静思冥想3 分钟前
Apache Commons ThreadUtils 的使用与优化
java·线程池优化
君败红颜4 分钟前
Apache Commons Pool2—Java对象池的利器
java·开发语言·apache
意疏12 分钟前
JDK动态代理、Cglib动态代理及Spring AOP
java·开发语言·spring
小王努力学编程15 分钟前
【C++篇】AVL树的实现
java·开发语言·c++
找了一圈尾巴26 分钟前
Wend看源码-Java-集合学习(List)
java·学习
逊嘘1 小时前
【Java数据结构】链表相关的算法
java·数据结构·链表
爱编程的小新☆1 小时前
不良人系列-复兴数据结构(二叉树)
java·数据结构·学习·二叉树
m0_748247801 小时前
SpringBoot集成Flowable
java·spring boot·后端
小娄写码1 小时前
线程池原理
java·开发语言·jvm
陌上花开࿈6 小时前
调用第三方接口
java