【JOB】如何写好补充类JOB和数据迁移类curl?

准备阶段

区分job和curl

curl和job进行区分:curl是一次性的job是定时的。所以在写这两种类型的代码时,方案考虑会有差异。

业务场景归类

  1. 补偿类job。补偿类job典型的特点是带有‘status’状态,比如:正常业务status应该从‘init’–>‘processing’–>‘sucess’,但是如果数据库中一直是init,说明该数据一直未被处理(原因可能之前处理的时候失败了,所以一直是init)。此时,补偿类会把init状态的数据查询到并进行处理:
    • 如果处理成功则状态从‘init’–>‘processing’–>‘sucess’,则job下次则不会查询已经处理成功的数据;如果处理失败,那么状态还是init,则job下次还会把该数据查询出来进行处理。
    • 可以在SQL条件中添加‘时间限制’,只查询一段时间内的处理失败的数据,超过这个时间的数据不再进行处理。
  2. 数据迁移类curl。数据迁移curl不需要‘status’状态,是完全的把A表数据复制到B表。 值得注意的是,如果只是做数据迁移且不涉及到数据的处理和聚合,那么迁移这个操作可以交给数仓来处理。

是否需要使用redis记录游标

redis记录失败游标的目的是:下一次执行时可以从redis中读取开始游标,然后继续执行。偏向于的场景是【定时重试】,job类型可根据自身场景考虑是否使用。

数据迁移是一次性代码,curl命令只执行一次,不存在【定时】场景。如果发生错误,log打印错误id,然后再提交一次curl,传入开始游标即可。

补偿类job

考虑因素

此类job的特点是:耗时短、数据量较少、带有status状态。

考虑因素如下:

  1. 并发考虑。添加分布式锁,保证同一时刻只有一个线程在处理数据。使用redisson方式的分布式锁,可以保证:自动续期、避免死锁。
  2. 考虑是否线程休眠。如果需要处理的数据量较大,那么每批次处理完后需要让线程休眠。但是一般而言,补偿类job作为兜底方案,数据量一般都不大。可配置到阿波罗中
  3. 幂等考虑。status转换问题,可能造成job重复拉取执行。当我把状态是init的查询出来后,在handle中做了处理,那么处理成功、失败后,该init状态是否需要转换为sucess、fialed,还是说依旧保持init。因为这关系到我下一次job执行是否还会再次处理该条数据。
  4. 时间条件限制。时间限制,对一直处理失败的数据进行放弃。因为确实存在这种情况:某几条状态是init的数据始终是处理失败。对于这种情况,我们需要添加时间限制,比如限制只查询前60分钟~10分钟的、状态是init的数据。
  5. 查询数量限制。避免查询大量数据到内存导致OOM。可配置到阿波罗中
  6. Switch开关。在while循环里面配置一个开关,一方面当出现死循环的时候可以快速下线功能,另一方面当发生未知代码错误的时候及时下线功能。

代码模板

    @PostMapping("/schedule/job")
    public void scheduleJob() {
        /* 规范:job均异步运行 */
        CompletableFuture.runAsync(() -> {
            log.info( begin...");
            schedule();
            log.info("end...");
        });
    }
 public void schedule() {
        log.info("[start.");
        /* 分布式锁解决并发执行问题 */
        RLock lock = redissonClient.getLock("keyLock");
        if (!lock.tryLock()) {
            log.info("lock conflict");
            return;
        }
        log.info("distribute lock success.");
        try {
            /* 开关。是否开启 */
            if (switch) {
                handler();
            }
        }catch (Exception exception){
            log.error("fail.", exception);
        }finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        log.info("end.");
    }

    private void handler() {
        log.info("handler start.");
        long amountCheckStart = 60 * 60 * 1000;
        long amountCheckEnd   = 10 * 60 * 1000;
        AssertUtil.isTrue(amountCheckStart > amountCheckEnd, "时间区间设置错误");
        Date curDate   = new Date();
        // 示例: 60分钟前的时间
        Date startTime = new Date(curDate.getTime() - amountCheckStart);
        // 示例: 10分钟前的时间
        Date endTime   = new Date(curDate.getTime() - amountCheckEnd);
        Long requestQueryId = 0L;
        // 滚动查询查询数据
        List<RequestDO> requestList =
                aigcRequestRepository.queryRolledRequestList(requestQueryId, startTime, endTime);

        log.info(" requestList.size:[{}]", requestList.size());

        /* 扫表 */
        while (!CollectionUtils.isEmpty(requestList)) {
            for(int i = 0 ; i < requestList.size(); i++) {
                RequestDO request = requestList.get(i);
                Long id = request.getId();
                String requestId      = request.getRequestId();
                String requestStatus  = request.getRequestStatus();
                String statusExplain  = request.getStatusExplain();
                try {
                    log.info("[check amount] 开始校对. id:[{}], requestId:[{}], status:[{}], statusExplain:[{}]",
                            id, requestId, requestStatus, statusExplain);
                    /* 核心业务处理 */
                    doHandler(request.getUserId(), requestId, requestStatus, statusExplain);
                    log.info("[check amount] 结束校对. id:[{}], requestId:[{}], status:[{}], statusExplain:[{}]",
                            id, requestId, requestStatus, statusExplain);
                }catch (Exception exception) {
                    log.error("[check amount] 权益校对失败. id:[{}], requestId:[{}], status:[{}], statusExplain:[{}]",
                            id, requestId, requestStatus, statusExplain, exception);
                }
            }
            // 获取下次查询的游标
            requestQueryId = requestList.get(requestList.size() - 1).getId();
            requestList    = aigcRequestRepository.queryRolledRequestList(requestQueryId, startTime, endTime);
            log.info("[check amount] requestList.size:[{}]", requestList.size());
        }

        log.info("[check amount] handler end");
    }
  }

SQL语句

   /**
     * SQL示例:
     * select * from request where id > 6 and
             created_time between '2023-03-24 00:00:00' and '2023-03-24 23:00:00' and status in () order by id asc limit 100 ;
     * @param id
     * @param startTime
     * @param endTime
     * @return
     */
    public List<RequestDO> queryRolledRequestList (Long id, Date startTime, Date endTime){
        LambdaQueryWrapper<RequestDO> queryCondition = new QueryWrapper().lambda();
        queryCondition.eq(RequestDO::getDeleted, 0);
        queryCondition.eq(RequestDO::getRequestStatus, RequestRecordStatus.PROCESSING.getCode());
        queryCondition.or().in(RequestDO::getStatusExplain,
                Lists.newArrayList(RequestStatusExplain.OCCUPY_UN_KNOW_ERROR.getCode(),
                RequestStatusExplain.RELEASE_UN_KNOW_ERROR.getCode()));
        queryCondition.between(RequestDO::getCreatedTime, startTime, endTime);
        queryCondition.gt(RequestDO::getId, id);
        queryCondition.orderByAsc(RequestDO::getId);
        queryCondition.last("limit 100");
        return aigcRequestDAO.selectList(queryCondition);
    }

数据迁移类curl

考虑因素

此类job的特点是:耗时长、可能会中断所以需要续跑、全表无差别复制。

耗时计算:

  1. 需要根据老表数据量,做一次迁移时长预估,且这段时间机器不能停机,否则得重新提交curl。机器配置xxx,使用batchinsert,100条数据差不多1s。(听说batchinsert比for循环insert性能高4倍,待验证)

代码相关因素:

  1. 并发考虑。添加分布式锁,保证同一时刻只有一个线程在处理数据。使用redisson方式的分布式锁,可以保证:自动续期、避免死锁。
  2. 线程休眠。一般而言,迁移类工作处理的数据量都比较大,那么每批次处理完后需要让线程休眠。可配置到阿波罗中
  3. 查询数量限制。避免查询大量数据到内存导致OOM。可配置到阿波罗中
  4. 单条插入或者批量插入。如果是停机迁移或者不停机迁移对存量数据迁移(存量数据很多,超过千万),可考虑批量插入batchInsert(需要仔细考虑是否有唯一冲突),优点是迁移工作会更快些;如果需要对每一条数据做额外的处理,那么可考虑单条插入insert,缺点是迁移工作会慢些。
  5. 是否redis记录游标。 数据迁移是一次性代码,curl命令只执行一次,不存在【定时】场景。如果发生错误,log打印错误id,然后再提交一次curl,传入开始游标即可。
  6. Switch开关。在while循环里面配置一个开关,一方面当出现死循环的时候可以快速下线功能,另一方面当发生未知代码错误的时候及时下线功能。

SQL相关因素:

  1. 新表是否有唯一约束。新表是否建立了唯一约束(不强制建立,但是建议建立),如果建立,代码中应该对唯一键异常进行捕获
  2. 谨防滚动查询SQL慢查询。SQL的条件需要谨慎,谨防慢查询。对于语句select * from table where id > 1000 and created_time between ‘2022-01-01 00:00:00’ and “2023-10-10 00:00:00” limit 100,id是主键索引,而createTime没有添加索引,那么如果数据量超过两千万条,这个SQL很有可能是慢查询。
  3. 谨防滚动查询SQL大分页。对于select * from table where id > 1000 and accountId = ‘001’ limt 100;那么accountId=‘001’的数据在数据库中的分布很有可能是:第一条数据id=1,第二条id=10w,第三条id=20w…. 分散不均匀,导致慢查询。---- 待验证。

代码模板

https://gitee.com/MyFirstMyYun/spring-boot-project-case/blob/master/job-scan-table-demo/src/main/java/com/xiaolyuh/controller/HistoryEventTrackingTask.java

https://gitee.com/MyFirstMyYun/spring-boot-project-case/blob/master/job-scan-table-demo/src/main/java/com/xiaolyuh/controller/SingleTableScanByNoAutoFieldJob.java

总结

略。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@来杯咖啡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值