使用SpringJdbcTemplate进行批量的更新/新增(高效率)

refer:几种批量删除分析比较:https://blog.youkuaiyun.com/fbysss/article/details/4152703?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.control

参考2:https://www.cnblogs.com/lzfhope/p/12069758.html

使用JDBC的进行Batch操作的缘由

由于数据的批量处理(比如往RDBMS插入几万条数据)非常耗资源,因此建议在所有场合能够借助于JDBC完成,而不要去采用Hibernate API。本文主要围绕Spring内置的各类JDBC API抽象支持展开的。

方式一:JdbcTemplate内置的batchUpdate()方法

batchUpdate(String sql, final BatchPreparedStatementSetter pss)

这一方法也仅仅适合于小批量数据的CUD(增、更新、删除)操作,但始终是同一SQL(参数具有多样性)。由于其内部使用了PreparedStatement语句,所以数据的操作效率还是不错的。下面给出了操作示例:

    public Object batchUpdateUser(List<User> users){
        jdbcTemplate.batchUpdate(" UPDATE user SET number = ? WHERE name = ? ",
                new BatchPreparedStatementSetter(){
                    @Override
                    public void setValues(PreparedStatement ps, int i)
                            throws SQLException {
                        ps.setString(1, users.get(i).getNumber());
                        ps.setString(2, users.get(i).getName());
                    }
                    @Override
                    public int getBatchSize() {
                        return users.size();
                    }
                });
        return "OK";
    }

    public Object insert(List<User> users){
        jdbcTemplate.batchUpdate(" insert into user(name,number) values (?,?) ",
                new BatchPreparedStatementSetter(){
                    @Override
                    public void setValues(PreparedStatement ps, int i)
                            throws SQLException {
                        ps.setString(1, users.get(i).getName());
                        ps.setString(2, users.get(i).getNumber());
                    }
                    @Override
                    public int getBatchSize() {
                        return users.size();
                    }
                });
        return "OK";
    }

方式二:BatchSqlUpdate

这一方法适合于各种批量数据的CUD(增、更新、删除)操作,但始终是同一SQL(参数具有多样性)。由于其内部使用了PreparedStatement语句,所以数据的操作效率还是不错的。下面给出了操作示例:

public Object insert2(List<User> users){
        DataSource dataSource = jdbcTemplate.getDataSource();
        BatchSqlUpdate bsu = new BatchSqlUpdate(dataSource, " insert into user(name,number) values (?,?)");
        bsu.setBatchSize(1000);
        bsu.setTypes(new int[]{Types.VARCHAR, Types.VARCHAR});

        for (User user : users) {
            bsu.update(new Object[]{user.getName(), user.getName()});
        }
        bsu.flush();
        return "OK";
    }

JdbcTemplate内置的batchUpdate(String sql, final BatchPreparedStatementSetter pss)相比,BatchSqlUpdate会自动分批待批量处理的数据。比如,如果需要批量操作10万条数据,则可以控制其batchSize,从而在时间(RDBMS可知性)和空间(内存)上达到平衡。

务必注意,在使用BatchSqlUpdate的最后,不要忘记手工调用其暴露的flush()方法。

使用sql语句 case when

/**
     * 批量更新人员Status
     *
     * @param personList personList
     */
    public void updatePersonStatusBatch(List<EcssPerson> personList) {

        String sql = " Update t_ecss_person Set ";
        sql += " n_last_update_status = case n_id ";
        StringBuilder stringBuilder = new StringBuilder();
        StringBuilder stringBuilder2 = new StringBuilder();
        StringBuilder ids = new StringBuilder();
        int i = 1;
        for (EcssPerson person : personList) {
            stringBuilder.append("  when " + person.getId() +
                    "  then " + person.getLastUpdateStatus());
            if (i == personList.size()) {
                stringBuilder.append(" end,");
                stringBuilder.append(" c_ec_code= ");
            }
            if(i==1){
                stringBuilder2.append(" case n_id ");
            }
            stringBuilder2.append(
                    "   when " + person.getId() +
                            "  then " + person.getEcCode());
            if (i == personList.size()) {
                stringBuilder2.append(" end ");
            }
            ids.append(person.getId());
            ids.append(",");
            i++;
        }
        stringBuilder.append(stringBuilder2);
        sql += stringBuilder.toString()+"  where n_id in (" + ids.toString().substring(0, ids.length() - 1) + ")";
        System.out.println(sql);
        this.executeSql(sql, new MapSqlParameterSource());
    }
<think>嗯,用户想了解在Spring Boot结合JPA的情况下,如何实现分批次更新数据库表项的task,并且要确保已经更新的数据不会被重复处理。首先,我需要理清楚可能的几种方法,然后逐一分析它们的优缺点和适用场景。 首先,分批次更新通常是因为数据量太大,一次性处理可能会导致内存溢出或性能问题。所以需要分批次处理。但关键是如何保证更新过的数据不被重复更新。这涉及到状态管理和事务控制的问题。 常见的策略可能有以下几种: 1. **分页查询**:使用分页技术,每次查询一定数量的数据,处理完后再处理下一页。但这里需要注意,如果在处理过程中有数据被修改或新增,可能会影响分页的准确性。比如,使用LIMIT和OFFSET时,如果数据有变化,OFFSET可能会跳过或重复某些记录。不过,如果数据是静态的,或者处理期间没有新数据插入,这可能有效。此外,可以通过在查询时添加条件,比如标记已处理的数据,避免重复。 2. **游标查询(Cursor)**:使用数据库游标逐条读取数据,这样即使数据变化,游标也能保持一致性。不过JPA本身可能不支持游标,可能需要使用JdbcTemplate或者Hibernate的ScrollableResults来实现。这种方法适合大数据量,但实现起来可能复杂一些。 3. **版本控制或时间戳**:在实体中添加版本号或时间戳字段,每次处理时根据这些字段筛选未处理的数据。处理完后更新这些字段,这样下次查询时就不会再选中已处理的数据。这种方法需要修改表结构,添加相应的字段,但能有效避免重复处理。 4. **状态标记**:在处理数据时,立即将记录标记为已处理(比如设置一个状态字段)。这样每次查询时只选择未处理的数据。但需要保证标记操作和业务处理的原子性,可能需要事务支持,避免在处理过程中失败导致状态不一致。 5. **基于ID范围的分批处理**:如果数据的主键是递增的,可以按ID范围分批查询和处理。例如,每次处理ID在1-1000,然后1001-2000等。这要求ID是连续的,且没有删除操作导致ID不连续。如果有删除,可能会有间隙,但可能不影响,只要处理到最大ID即可。 接下来需要考虑如何保证不重复更新。比如分页查询可能因为数据变动导致重复,而状态标记或版本控制可以解决这个问题。此外,事务的管理也很重要,确保在处理过程中如果出现异常,已经处理的部分能够正确回滚或标记,避免部分失败导致的数据不一致。 另外,需要考虑性能问题。比如,分页查询在大数据量时,越后面的页数OFFSET效率越低。而游标或ID范围可能更高效。状态标记可能需要索引来加快查询速度,否则每次查询未处理的记录会变慢。 对于实现方式,可能需要结合Spring Batch这样的批处理框架,或者自行编写定时任务(比如使用@Scheduled)。Spring Batch提供了分页、游标等处理方式,以及事务管理和跳过失败记录等机制,可能更适合复杂的批处理场景。但如果需求简单,自行实现可能更轻量。 还有,处理过程中要确保事务的粒度合适。比如,每个批次作为一个事务,这样在失败时只回滚当前批次,而不是整个处理过程。但需要根据业务需求来决定,比如是否允许部分成功。 可能的代码实现步骤包括: - 定义实体类,包含状态字段或版本字段。 - 编写分页查询或游标查询的方法,每次获取一批数据。 - 处理数据并更新状态或版本。 - 使用事务管理确保操作的原子性。 - 定时任务触发批处理任务。 还需要考虑并发情况,如果有多个实例同时运行任务,可能会导致重复处理。这时候可能需要分布式锁或者数据库的悲观锁/乐观锁机制来避免。 总结可能的几种方式: 1. **分页查询+状态标记**:每次分页查询未处理的数据,处理并标记,事务控制每个批次。 2. **游标查询+实时更新**:逐条处理,实时更新状态,适合大数据量。 3. **版本控制+条件查询**:根据版本或时间戳过滤已处理数据,每次处理完更新版本。 4. **ID范围分批处理**:适用于有序ID,按区间处理,无需额外字段但依赖ID结构。 5. **Spring Batch框架**:利用现有的批处理框架处理分页、事务、重试等机制。 需要根据具体情况选择最合适的方法,比如数据量大小、是否允许修改表结构、是否需要并发处理等因素。</think>在Spring Boot + JPA中实现分批次更新且避免重复更新,可通过以下5种方式逐步实现: --- ### 方式1:分页查询 + 状态标记法 ```java // 实体类添加状态字段 @Entity public class Item { @Id private Long id; private boolean processed = false; // 新增状态字段 } ``` ```java // 分页处理逻辑 @Transactional public void batchUpdate() { int page = 0; int size = 100; Pageable pageable = PageRequest.of(page, size, Sort.by("id")); Page<Item> itemsPage; do { itemsPage = itemRepository.findByProcessedFalse(pageable); List<Item> items = itemsPage.getContent(); items.forEach(item -> { // 业务处理逻辑 item.setProcessed(true); // 立即标记为已处理 }); itemRepository.saveAll(items); // 批量更新 pageable = pageable.next(); } while (!itemsPage.isEmpty()); } ``` **优点**:避免重复处理,原子性操作 **缺点**:需修改表结构,频繁更新状态字段可能影响性能 --- ### 方式2:版本控制 + 条件更新 ```java // 实体类添加版本字段 @Entity public class Item { @Id private Long id; @Version private Long version; // 乐观锁版本 private LocalDateTime lastModified; } ``` ```java // 按时间段批量更新 @Transactional public void batchUpdateByTime() { LocalDateTime now = LocalDateTime.now(); List<Item> items = itemRepository.findByLastModifiedBefore(now.minusMinutes(30)); items.forEach(item -> { // 业务处理 item.setLastModified(now); }); itemRepository.saveAll(items); } ``` **优点**:利用乐观锁防止并发冲突 **缺点**:时间窗口选择需谨慎 --- ### 方式3:游标查询(ScrollableResults) ```java // 使用Hibernate游标(适合大数据量) public void processWithCursor() { Session session = entityManager.unwrap(Session.class); ScrollableResults scroll = session.createQuery("FROM Item WHERE processed = false") .setFetchSize(100) .scroll(ScrollMode.FORWARD_ONLY); int count = 0; while (scroll.next()) { Item item = (Item) scroll.get(0); // 业务处理 item.setProcessed(true); if (++count % 100 == 0) { session.flush(); // 每100条提交一次 session.clear(); } } scroll.close(); } ``` **优点**:内存占用低,适合超大数据 **缺点**:需直接操作Hibernate API --- ### 方式4:ID区间分段处理 ```java // 按ID范围分批 public void batchUpdateByIdRange() { Long maxId = itemRepository.findMaxId(); long batchSize = 1000; for (long from = 0; from <= maxId; from += batchSize) { long to = from + batchSize; List<Item> items = itemRepository.findByIdBetween(from, to); items.forEach(item -> { if (!item.isProcessed()) { // 二次校验防止重复 // 业务处理 item.setProcessed(true); } }); itemRepository.saveAll(items); } } ``` **优点**:无需额外字段 **缺点**:依赖ID连续性,需二次校验 --- ### 方式5:Spring Batch集成 ```xml <!-- pom.xml添加依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> ``` ```java // 配置批处理Job @Bean public Job importJob(JobBuilderFactory jobs, Step step) { return jobs.get("batchUpdateJob") .incrementer(new RunIdIncrementer()) .flow(step) .end() .build(); } @Bean public Step step(StepBuilderFactory steps) { return steps.get("step") .<Item, Item>chunk(100) .reader(itemReader()) .processor(itemProcessor()) .writer(itemWriter()) .build(); } // 自定义Reader(使用JPA分页) public ItemReader<Item> itemReader() { return new JpaPagingItemReaderBuilder<Item>() .name("itemReader") .queryString("SELECT i FROM Item i WHERE i.processed = false") .pageSize(100) .entityManagerFactory(entityManagerFactory) .build(); } ``` **优点**:自带重试/跳过机制,完善的监控 **缺点**:框架学习成本较高 --- ### 防重复核心要点 1. **原子性操作**:状态标记与业务更新要在同一事务中 2. **悲观锁控制**(可选): ```java @Query("SELECT i FROM Item i WHERE i.processed = false ORDER BY id FOR UPDATE") List<Item> findUnprocessedWithLock(); ``` 3. **唯一性校验**:即使使用状态标记,处理时也应做二次验证 4. **幂等设计**:确保重复执行不会破坏数据一致性 根据数据量、并发要求和基础设施选择最适合的方案,中小规模推荐方式1,超大数据量建议方式3或5。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值