You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Spring中如何批量创建/插入数据以优化CSV导入性能?

Java Spring中如何批量创建/插入数据以优化CSV导入性能?

看起来你现在的CSV导入逻辑是循环单条调用createUser插入,数据量大的时候确实会因为频繁的数据库交互拖慢速度,我来给你几个实用的批量插入优化方案,都是Spring生态里常用的:

1. 先配置JPA/Hibernate的批量插入属性(如果用JPA的话)

首先得让Hibernate支持批量处理,在application.properties或者application.yml里加这几个配置:

# 每批处理的条数,根据你的数据库和内存调整,一般50-200合适
spring.jpa.properties.hibernate.jdbc.batch_size=50
# 让Hibernate把同类型的插入语句排序,合并成批量SQL
spring.jpa.properties.hibernate.order_inserts=true
# 同理,更新语句也排序,这里主要用在插入,加上也没坏处
spring.jpa.properties.hibernate.order_updates=true

这些配置能让Hibernate把多条插入语句合并成一个批量SQL发送给数据库,减少网络交互次数。

2. 手动分批次处理+定时flush/clear缓存

Hibernate的Session(一级缓存)会缓存所有待插入的实体,数据量太大时会占满内存,还会拖慢插入速度。所以要分批次处理,每插入N条就手动flush到数据库,然后清空缓存:

修改你的导入方法,示例代码如下:

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
// 其他必要的import

@Override
@Transactional
public void importUserData(final UserDataDTO toBeSavedDTO) {
    final String file = decodeFile(toBeSavedDTO);
    final List<UserDTO> imports = parser.parseImport(file);
    
    if (imports == null || imports.isEmpty()) {
        throw new IllegalArgumentException("导入数据为空,请检查CSV文件");
    }

    // 删除现有条目
    dao.deleteUserEntries();

    int batchSize = 50;
    int count = 0;
    // 这里通过@PersistenceContext注入EntityManager
    for (UserDTO userDTO : imports) {
        // 把UserDTO转换成JPA实体类
        User user = convertDtoToEntity(userDTO);
        entityManager.persist(user);
        count++;
        
        // 每到批量大小就flush并清空缓存
        if (count % batchSize == 0) {
            entityManager.flush(); // 把当前批次的SQL发送到数据库执行
            entityManager.clear(); // 清空一级缓存,释放内存
        }
    }
    // 处理最后一批不足batchSize的数据
    entityManager.flush();
    entityManager.clear();
}

这样每50条就批量提交一次,既减少了数据库交互,又避免了内存溢出的问题。

3. 用Spring Data JPA的saveAll()+分批次处理

如果你的DAO是Spring Data JPA的Repository接口,可以用saveAll()方法,但要注意:直接给saveAll()传超大列表会把所有实体都存在缓存里,还是会有内存问题,所以要把大列表切成小批次处理:

import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.Lists; // 可以用Guava的分区工具,或者自己实现分批次逻辑

@Override
@Transactional
public void importUserData(final UserDataDTO toBeSavedDTO) {
    final String file = decodeFile(toBeSavedDTO);
    final List<UserDTO> imports = parser.parseImport(file);
    
    if (imports == null || imports.isEmpty()) {
        throw new IllegalArgumentException("导入数据为空,请检查CSV文件");
    }

    dao.deleteUserEntries();

    int batchSize = 50;
    // 把大列表分成多个小批次
    List<List<UserDTO>> batches = Lists.partition(imports, batchSize);
    
    for (List<UserDTO> batch : batches) {
        // 把DTO转成实体
        List<User> userEntities = batch.stream()
                                       .map(this::convertDtoToEntity)
                                       .collect(Collectors.toList());
        // 批量保存
        userRepository.saveAll(userEntities);
        // 手动flush+clear,避免缓存堆积
        entityManager.flush();
        entityManager.clear();
    }
}

结合之前的Hibernate配置,saveAll()会利用批量插入的优化,性能会比单条插入好很多。

4. 用JdbcTemplate做原生批量插入(极致性能)

如果追求最高性能,可以绕开ORM层,直接用JdbcTemplate的批量更新方法,这样没有ORM的额外开销,速度最快:

import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;

@Override
@Transactional
public void importUserData(final UserDataDTO toBeSavedDTO) {
    final String file = decodeFile(toBeSavedDTO);
    final List<UserDTO> imports = parser.parseImport(file);
    
    if (imports == null || imports.isEmpty()) {
        throw new IllegalArgumentException("导入数据为空,请检查CSV文件");
    }

    dao.deleteUserEntries();

    // 写原生的插入SQL,替换成你自己的表和字段
    String insertSql = "INSERT INTO user (username, email, phone) VALUES (?, ?, ?)";
    
    jdbcTemplate.batchUpdate(insertSql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(java.sql.PreparedStatement ps, int i) throws java.sql.SQLException {
            UserDTO userDTO = imports.get(i);
            // 给PreparedStatement设置参数,注意下标从1开始
            ps.setString(1, userDTO.getUsername());
            ps.setString(2, userDTO.getEmail());
            ps.setString(3, userDTO.getPhone());
        }

        @Override
        public int getBatchSize() {
            return imports.size();
        }
    });
}

这种方式直接把所有参数打包发给数据库,数据库端执行批量插入,性能是几种方案里最高的,适合超大规模的CSV导入。

几个要注意的坑

  • 主键生成策略:如果用的是GenerationType.IDENTITY(比如MySQL的自增主键),Hibernate的批量插入会失效!因为每次插入都要立即从数据库获取主键,只能单条执行。解决方法是改成GenerationType.SEQUENCE或者TABLE策略,让Hibernate可以批量生成主键。
  • 事务边界:如果把整个导入放在一个大事务里,数据量特别大时会导致数据库事务日志暴涨,甚至超时。如果业务允许,可以把每批次放在单独的事务里,但要注意数据一致性(比如删除现有数据后,某批次失败会导致部分数据插入)。
  • 不要在循环里单独flush:之前的单条插入慢的原因就是每次createUser可能触发了flush,导致每条都发一次SQL,所以一定要批量flush。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 10:24:52