当前位置: 首页 > article >正文

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

在工作需求中遇到了一个导入客户数据的需求,因为项目本身使用若依框架,所以直接使用了若依框架自带的easypoi进行导出.
但是后续发现,easypoi在数据量较大的时候会占用非常多的内存,导入10M的excel,占用内存接近1G,这是不可接受的.所以后续先是优化成了阿里的easyExcel,easyExcel对内存方面进行了优化,分片操作,提升了内存和速度,整体要比easypoi优秀很多.代码为:

    @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转换为对象列表之后触发该事件,监听者监听到该事件,然后去触发导入的逻辑操作,从而实现后台录入数据并分配,前台直接返回数据操作中,并通过消息来返回结果.
具体代码为:

    @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为:

@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);
    }
}

监听器代码:

@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再配上分次批量存储即可.


http://www.kler.cn/a/458060.html

相关文章:

  • 基于深度学习(HyperLPR3框架)的中文车牌识别系统-Qt开发UI
  • 万兴PDF专家电脑绿色便携版
  • RGB、HSV颜色模型及MATLAB互换应用实例
  • Python 爬虫
  • 【游戏设计原理】36 - 环境叙事
  • 云手机服务器如何做到群控多台手机的?
  • socket建立客户端服务器程序
  • Docker安装和使用RabbitMQ
  • 【Python】什么是字典(Dictionary)?
  • springboot489基于springboot的七彩云南文化旅游网站的设计与实现(论文+源码)_kaic
  • 玉米中的元基因调控网络突出了功能上相关的调控相互作用\mo.20a3.R
  • PyTorch中提升模型训练速度的17种策略
  • uni-app开发-识图小程序-个人中心页面
  • Windows远程连接桌面报错“由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开。请跟服务器管理员联系
  • ELK入门教程(超详细)
  • 【算法】复杂性理论初步
  • Wordpress Tutor LMS插件存在SQL注入漏洞(CVE-2024-10400)
  • 【机器学习】SVM支持向量机(二)
  • mysql建立主从集群
  • 38. 日志