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

目录标题

  • 准备阶段
    • 业务场景归类
    • 需要考虑的因素
      • 框架设计
      • 业务代码
  • 代码模板
    • 补偿类job代码模板
      • 业务代码
      • SQL语句
    • 数据迁移类job代码模板
  • 总结

准备阶段

业务场景归类

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

需要考虑的因素

框架设计

  1. Redis添加启动标识。考虑到该job可能重复执行、续跑,所以使用Redis添加标识。(补偿类job、数据迁移类job
  2. 幂等问题。必须考虑job重复拉取的问题,所以我们的业务代码需要做幂等处理。(补偿类job、数据迁移类job
  3. 添加开关。在while中添加开关,用于break循环。(补偿类job、数据迁移类job
  4. Redis记录处理数据的游标。如果’耗时较长’ 且 查询的数据还需要做’数据处理’、'数据聚合’等操作,考虑到可能出现【中断】异常,所以使用Redis记录异常时的游标,方便下次重新执行。比如:A表有两千多万条数据量,假设一条数据复制需要0.5s,那么总耗时达到2000 * 10000 * 0.5。(数据迁移类job
  5. 线程休眠。 如果处理时间较长,CPU消耗比较严重,必须让该线程休眠。(数据迁移类job

业务代码

  1. 滚动查询的SQL设计。(补偿类job、数据迁移类job
  2. 查询的时候注意防止大数据查询导致OOM。所以需要使用滚动查询,且限制好每次查询的size。(补偿类job、数据迁移类job
  3. 插入数据选择单条插入。虽然mybatis提供了batchInsert的API, 但是如果存在唯一冲突,或者单条插入失败的场景会导致该批次数据全部失败,所以选择单条数据插入。(数据迁移类job

代码模板

补偿类job代码模板

此类job的特点是:耗时短、数据量较少、带有status状态。
需要额外注意两点:

  1. 一个是status转换问题,可能造成job重复拉取执行。当我把状态是init的查询出来后,在handle中做了处理,那么处理成功、失败后,该init状态是否需要转换为sucess、fialed,还是说依旧保持init。因为这关系到我下一次job执行是否还会再次处理该条数据。
  2. 另一个是时间限制,对一直处理失败的数据进行放弃。因为确实存在这种情况:某几条状态是init的数据始终是处理失败。对于这种情况,我们需要添加时间限制,比如限制只查询前60分钟~10分钟的、状态是init的数据。

业务代码

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

数据迁移类job代码模板

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

  1. 插入数据选择单条插入。虽然mybatis提供了batchInsert的API, 但是如果存在唯一冲突,或者单条插入失败的场景会导致该批次数据全部失败,所以选择单条数据插入。
  2. 使用Redis记录游标。因为耗时长,所以极有可能中途出现错误,为了避免大量数据重复处理,所以记录游标。
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

总结

略。

你可能感兴趣的:(Java基础使用积累,JOB,数据同步)