【定时任务处理中的分页问题】

这篇具有很好参考价值的文章主要介绍了【定时任务处理中的分页问题】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

最近要做一个定时任务处理的需求,在分页处理上。发现了大家容易遇到的一些"坑",特此分析记录一下。

场景

现在想象一下这个场景,你有一个定时处理任务,需要查询数据库任务表中的所有待处理任务,然后进行处理。

举个例子:生成用户的月度账单,并且要尽可能确保每个用户都能生成自己的账单,推送到用户的邮箱中。

分析

拿到这样一个任务之后,我们很自然的就想到了加一个定时任务,每隔一段时候处理这些任务。

任务肯定是先查询,再处理。处理完成之后,再更新任务状态。

关于查询

一般开始一个任务时,都是要有一个范围的,比如特定时间或特定用户。如果不界定范围,由于产线上的数据不断更新,我们的程序就会变得不可控!因此我们先要界定一个范围,然后再进行处理。

由于任务基数可能比较大,所以查询任务的时候,不能一次性全部读取到内存中,因此需要进行分页处理。

关于更新

任务更新的时候,考虑到并发,我们一般都要进行待状态更新,这样才能确定更新结果符合预期。如果更新结果不符合预期,还可以适当告警。

分页1.0

根据上面的需求,我们很容易就写出了如下v1.0代码(使用了PageHelper进行分页)。文章来源地址https://www.toymoban.com/news/detail-653783.html

// 查询一段时间以内待处理的任务
Date startTime = getStartTime();
Date endTime = getEndTime();

Integer pageNum = 1;
while (true) {
    PageHelper.startPage(pageNum, 1000);
    List<TaskDTO> taskDTOList = taskService.queryTask(TaskStatusEnum.INIT.getCode(), startTime, endTime);
    if (CollectionUtils.isEmpty(taskDTOList)) {
        break;
    }

    for (TaskDTO taskDTO : taskDTOList) {
        // 处理任务...

        // 更新任务状态为成功
        taskService.updateTaskStatus(taskDTO.getTaskId(), TaskStatusEnum.INIT.getCode(), TaskStatusEnum.SUCCESS.getCode());
    }
    pageNum++;
}

程序没问题?拉出来跑一跑

乍一看,这样的处理代码没什么问题。但是如果跑起来,你就会发现出现了“跳读”现象,即一个调度处理完成后,数据库中仍然会存在一些待处理的任务,这些数据被 跳读 了!

分页2.0 —— 解决跳读问题

问题发生在哪里呢?

问题出在分页查询的逻辑错了。

PageHelper固然能够帮助我们简化分页的处理,但是它的应用场景是原始数据不变的场景。

前面,我们虽然根据起止时间界定了任务的范围。但是,当我们一边根据任务状态查询遍历,一边更新任务状态时,实际上待处理的任务总量是实时变化的!不可避免的,会跳过部分待处理的任务。这就是上面那段代码存在的问题。

如何解决这个问题?

其实,解决这个问题,最简单的方式只需要修改一行代码。就是将上面循环体内的pageNum++去掉,即一直只查第一页。

因为我们默认每次处理完成之后,都是会更新任务状态为成功。这样,我们只要一直查待处理的第一页,这样就不会有 跳读 的问题了。

// 查询一段时间以内待处理的任务
Date startTime = getStartTime();
Date endTime = getEndTime();

while (true) {
    PageHelper.startPage(1, 1000);
    List<TaskDTO> taskDTOList = taskService.queryTask(TaskStatusEnum.INIT.getCode(), startTime, endTime);
    if (CollectionUtils.isEmpty(taskDTOList)) {
        break;
    }

    for (TaskDTO taskDTO : taskDTOList) {
        // 处理任务...

        // 更新任务状态为成功
        taskService.updateTaskStatus(taskDTO.getTaskId(), TaskStatusEnum.INIT.getCode(), TaskStatusEnum.SUCCESS.getCode());
    }
}

这样就没问题了?其实还有坑

上面解决问题的代码方案,其实是基于正常的场景下的。

现在考虑一个异常的场景:假如我们在处理任务的时候,发生 异常 了(如调用外部系统异常,网络抖动,某些数据有问题),导致某些任务失败。

如果出现这样的问题,当前面积攒的失败任务过多时,程序就会一直重复处理某些数据。极端场景下,第一页的1000条数据全部失败,程序就一直无法进行处理下去了,而且循环还无法停止。。。

那么针对这种场景,该如何处理呢?

分页3.0 —— 异常处理

添加失败态

一种思路是添加失败态。我们可以将处理异常的任务给一个失败的终止态,这样下次查询的时候就不会查出这个失败的任务了。

当一个任务执行异常之后,马上就置为失败可能会有点过于激进。为了减少失败次数,我们可以在task表中添加一个重试次数的字段。每次处理失败,重试次数都+1。当达到我们预期的一个值时,例如3次,就置为失败态,后续可以告警或人工处理。

代码如下:

// 查询一段时间以内待处理的任务
Date startTime = getStartTime();
Date endTime = getEndTime();

Integer maxRetryTimes = 3;
while (true) {
    PageHelper.startPage(1, 1000);
    List<TaskDTO> taskDTOList = taskService.queryTask(TaskStatusEnum.INIT.getCode(), startTime, endTime);
    if (CollectionUtils.isEmpty(taskDTOList)) {
        break;
    }

    for (TaskDTO taskDTO : taskDTOList) {
        try {
            // 处理任务...

            // 更新任务状态为成功
            taskService.updateTaskStatus(taskDTO.getTaskId(), TaskStatusEnum.INIT.getCode(), TaskStatusEnum.SUCCESS.getCode());
        } catch (Exception e) {
            // 重试次数+1,超过3次置为失败
            taskService.updateRetryTimes(taskDTO.getTaskId(), taskDTO.getRetryTimes() + 1);
            if (taskDTO.getRetryTimes() + 1 >= maxRetryTimes) {
                taskService.updateTaskStatus(taskDTO.getTaskId(), TaskStatusEnum.INIT.getCode(), TaskStatusEnum.FAILED.getCode());
            }
        }
    }
}

但是仔细想想上面这种写法,重试是在一次任务执行调度期间发生的。一般来说,当一个异常发生后,马上再次调用时,大概率依然还是会发生异常的,因此多数场景下,该任务只会做失败处理。

添加定位标识

另一种思路是添加定位标识。这种思路并不更改任务状态,而是通过添加定位标识的方式,来完成全部遍历的要求。

简单来说,就是每次查询出待处理任务时,都带出对应的taskId,并按照正序排序。一个批次处理结束后,获取上一个批次处理的最大taskId。下一次查询的时候,带上大于此taskId的条件,这样循环处理,就能完成全部待处理任务的遍历。

代码如下:

// 查询一段时间以内待处理的任务
Date startTime = getStartTime();
Date endTime = getEndTime();

Integer startId = 0;
while (true) {
    PageHelper.startPage(1, 1000);
    List<TaskDTO> taskDTOList = taskService.queryTask(TaskStatusEnum.INIT.getCode(), startTime, endTime, startId);
    if (CollectionUtils.isEmpty(taskDTOList)) {
        break;
    }

    for (TaskDTO taskDTO : taskDTOList) {
        // 处理任务...

        // 更新任务状态为成功
        taskService.updateTaskStatus(taskDTO.getTaskId(), TaskStatusEnum.INIT.getCode(), TaskStatusEnum.SUCCESS.getCode());
        startId = taskDTO.getTaskId();
    }
}

使用定位标识进行定位,解决了可能无法遍历全部任务的问题。并且对于处理异常的任务,在下一次调度拉起的时候,又能够重新进行执行。(对于像网络抖动、系统调用异常这样的问题,可以待网络或下游系统恢复后,在下一次调度执行时自动执行完成,这是其优点)

综合使用两种方案

上述两种方式都可以解决异常处理问题。在实际问题中,往往两者综合起来进行使用。即:

  1. 使用定位标识来进行遍历,从而在一个调度执行期间,能够对所有待处理任务进行处理;并且对于处理异常的任务,能够在下一次调度启动时自动拉起执行。
  2. 另一方面,任务实体中添加一个重试次数字段。当达到最大重试次数后,任务翻为失败,由人工进行处理。

具体的重试次数和调度执行时间间隔,可以由具体的业务场景来决定。这样就能尽可能减少人工干预的次数,减少人力成本。

总结

上面所说的一些场景,是我所遇到过的一些“坑”。在真正的业务场景中,可能还会有更多更复杂的分页问题。一般来说,具体问题还是需要具体看待,不能照搬照抄,但是可以借鉴参考。

总结一下,在我看来,遇到这种定时任务处理场景下的分页问题,需要:

  1. 以不变应万变。控制数据总量不变,这样才能准确分页。
  2. 考虑异常场景。防止异常场景下,程序不断重试,阻塞后续正常任务执行。
  3. 添加监控。线上问题层出不穷,做好监控可以及时发现并处理。

最近要做一个定时任务处理的需求,在分页处理上。发现了大家容易遇到的一些"坑",特此分析记录一下。

场景

现在想象一下这个场景,你有一个定时处理任务,需要查询数据库任务表中的所有待处理任务,然后进行处理。

举个例子:生成用户的月度账单,并且要尽可能确保每个用户都能生成自己的账单,推送到用户的邮箱中。

分析

拿到这样一个任务之后,我们很自然的就想到了加一个定时任务,每隔一段时候处理这些任务。

任务肯定是先查询,再处理。处理完成之后,再更新任务状态。

关于查询

一般开始一个任务时,都是要有一个范围的,比如特定时间或特定用户。如果不界定范围,由于产线上的数据不断更新,我们的程序就会变得不可控!因此我们先要界定一个范围,然后再进行处理。

由于任务基数可能比较大,所以查询任务的时候,不能一次性全部读取到内存中,因此需要进行分页处理。

关于更新

任务更新的时候,考虑到并发,我们一般都要进行待状态更新,这样才能确定更新结果符合预期。如果更新结果不符合预期,还可以适当告警。

分页1.0

根据上面的需求,我们很容易就写出了如下v1.0代码(使用了PageHelper进行分页)。

到了这里,关于【定时任务处理中的分页问题】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 重构优化第三方查询接口返回大数据量的分页问题

    # 问题描述      用户线上查询其上网流量详单数据加载慢,且有时候数据没有响应全~      1、经排除是调用第三方数据量达10w条响应会超时,数据没正常返回      2、现有线上缓存分页也是加载慢数据不能正常展示      3、第三方接口返回类似报文jsonj:           4、我

    2024年02月09日
    浏览(37)
  • java springboot整合MyBatis实现分页查询以及带条件的分页查询

    之前的文章 java springboot整合MyBatis做数据库查询操作操作了springboot整合MyBatis,然后简单做了个按id查询的操作 那么 我们按上文搭建起的环境继续 我们直接在staffDao接口中声明一个分页函数 这里 我们直接在 sql语句中写limit 分页逻辑 参数是方法接收的 这个函数接收两个参数

    2024年02月10日
    浏览(34)
  • Github Actions 执行Python定时任务(时区及缓存问题处理)

    文档地址:https://docs.github.com/en/actions 这两天一直在学习Github Actions,很是兴奋,才发现这么好的东西,相见恨晚。 它是什么呢,简单讲,就是让你的Github项目具备持续集成的能力,类似于传统工具jenkins,Gitlab的CI/CD功能等,但Github Actions更加强大。 我尝试简单实践了一下,

    2024年02月09日
    浏览(51)
  • 关于element-ui的分页器设置每一页显示数量(page-size)后页码并没有变化的问题与解决

    问题:element-ui的分页器在设置后,总页面数异常,不匹配总条数与单页显示数,设置page-size后页面也无变化 对应的参数:   对应的样式: 原因: page-size设置的值,是page-sizes数组中有的值,如果page-size的数值是page-sizes的数组里面没有值的,那么就会默认取page-sizes当中的第一个

    2024年02月11日
    浏览(37)
  • Android 多任务窗口中的界面高斯模糊处理问题

    应用开发过程中安全问题的确需要系统考虑也要开发者自己多考虑,一个小的细节可能会让你的应用变的更安全,最近在用招商银行App的时候无意中发现了 iPhone上多任务窗口 ,看到招商银行的应用有一个特别的地方就是当应用出现在多任务中的时候界面被高斯模糊处理了,

    2024年02月11日
    浏览(27)
  • MyBatis的分页原理

    最近看到了一篇MyBatis的分页实现原理,文章里描述到使用ThreadLocal,其实想主要想看看ThreadLocal的巧妙使用,并且看一下分页是如何实现的。 ChaiRongD/Demooo - Gitee.com 其实一个简单的分页如下面代码所示,使用PageHelp对象设置分页的参数,然后把查询到的List对象作为参数传入P

    2024年02月09日
    浏览(30)
  • JavaWeb12(实现基础分页&模糊查询的分页)

    目录 一. 效果预览 ​编辑 二. 实现基本分页 2.1 分页sql  --每页3条  取第二页 --由于伪列不能作用与大于符号也不能作用于between....and --因此需要将伪列-----名列  2.2 万能公式  2.3 首页上一页下一页实现 ②前端代码 2.4 末页实现优化 ①底层代码 ②前端优化  三.实现模糊查询

    2024年02月06日
    浏览(34)
  • MyBatisPlus(SpringBoot版)的分页插件

    目录  一、前置工作:         1.整体项目目录结构         2.创建普通javamaven项目。         3.导入依赖,改造成springboot项目         4.配置启动类         5.创建service接口及其实现类        6.创建接口Mapper                  7.配置数据源         8.创建数

    2024年04月27日
    浏览(38)
  • 分布式任务调度,定时任务的处理方案

    适用场景: Spring 定时任务是 Spring 框架提供的一种轻量级的任务调度方案,它的特点是简单易用、轻量级。Spring 定时任务的执行是在 单个节点 上进行的,如果需要分布式任务调度,需要自己实现相应的解决方案。 1.导入依赖版本自己控制 2.启动类加上@EnableScheduling 3.编写业

    2023年04月14日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包