写个定时任务也这么多BUG?

作者简介:大家好,我是码哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析

阶段4、深入jdk其余源码解析

阶段5、深入jvm源码解析

在日常开发中,相信几乎每个人都写过定时任务。即使哪一天你离职了,你的定时任务可能都还在跑着:

通常来说,定时任务有一套固定的模式:

  • 循环分页 ,直到没有符合条件的数据
  • 处理符合条件的每一条数据

这种固定的模式体现在代码上就是:

@Slf4j
@Component
public class XxxTask {@Resourceprivate XxxService xxxService;/*** 任务入口*/@XxlJob("xxxTask")public void handle() {// 时间条件 [当前时间, 当前时间+N分钟]final DateTime notifyStart = xxx;final DateTime notifyEnd = xxx;// 分页条件final int pageSize = 200;int pageNo = 1;while (true) {// 分页获取符合条件的数据List<XxxDO> list = this.xxxService.lambdaQuery().ge(XxxDO::getStartTime, notifyStart)  // 条件:某个时间范围内、状态为 已发布 的数据.le(XxxDO::getStartTime, notifyEnd).eq(XxxDO::getStatus, PUBLISHED.getStatus()).page(new Page<>(pageNo, pageSize, false)).getRecords();if (CollectionUtils.isEmpty(list)) {XxlJobHelper.log("task is done, ends at pageNo:{}", pageNo);break;}for (XxxDO pojo : list) {// 处理数据this.processPageData(pojo);}// 下一轮pageNo++;XxlJobHelper.log("next round, pageNo:{}", pageNo);}}
}

你会发现,上面的代码逻辑是很套路化的:按某种条件对数据库的某张表进行分页查询,然后处理数据,最后pageNo++进行下一轮数据处理,直到本次任务结束。

然而,即便是如此简单的代码,也可能会出现意想不到BUG。

警惕“一边修改一边分页”

通常来说,我们的定时任务极大概率会修改分页查询得到的XxxDO(查出来却不做任何操作几乎不可能,除非当前分页是为了处理另一张表做铺垫)。而修改XxxDO时,又有很大的概率恰好会修改“条件字段”。

什么是“条件字段”?比如

select * from t_user where status=1 limit 0, 4;

如果我们的需求是查询出所有status=1的数据,把它们修改为status=0(删除),那么此时 status作为条件字段,却刚好被修改了,会出现“数据被跳过”的BUG。

待处理数据:

正常分页:limit 0, 4(第一页)

正常分页:limit 4, 4(第二页)

按照上面的分页套路,查询出 status=1 limit 0, 4的第一页数据,并把id为1、3、4、6的数据status更新为0:

然后while(true){}会进入第二页:status=1 limit 4, 4 发现结果为空

按我们的预期,应该会出现id为7、8的两条数据,但它们似乎被“跳过了”。原因很简单,对于status=1 limit 4, 4来说,由于上一次做了修改,导致原本status=1的4条数据已经变成status=0,所以id为7、8的数据前面已经没有status=1的数据了。此时我们拿着status=1 limit 4, 4做查询,实际的含义是:给我status=1、pageSize=4的第二页。现在整个表就两条status=1的数据(7和8),第1页都没满,而你居然想要第2页,当然就是空...整个定时任务结束后,会发现id为7、8的两条数据没有被处理。

解决办法很简单,就是用ID偏移查询(下一小节介绍),或者始终查询第一页:

while(true) {// pageNo写死,永远查询第一页List<XxxDO> list = this.page(1, pageSize);if (CollectionUtils.isEmpty(list)) {break;}// 允许随便更新条件字段this.update(list);
}

这种做法,其实就是相当于玩俄罗斯方块,上面的数据被处理了以后,下面的数据就会“上来填充”,这样我们只要一直在第一页等着就行了:

尽可能使用ID分页

我记得有一道面试是这样问的:如何保证“深分页”的性能?

什么是“深分页”?举个简单的例子,假设一张表总共2000w数据,其中 height>180 && weight>65 && city='杭州' 的共有1500w条。现在前台页面提供了一个分页,用来展示这些数据。即使pageSize=100,那么也有15w页。如果使用传统的limit offset, size,那么越往后性能会越差,这与limit分页的底层机制有关(比如 limit 10000, 10实际上会选取10010条数据然后丢弃前面1w条,返回10000~10010之间的数据)。

同理,如果定时任务要处理的数据很多,那么随着分页的不断进行,就会变成“深分页”:

while (true) {// 分页获取符合条件的数据List<XxxDO> list = this.xxxService.lambdaQuery().condition(...)  // 条件:height>180 && weight>65 && city='杭州'.page(new Page<>(pageNo, pageSize, false)).getRecords();if (CollectionUtils.isEmpty(list)) {XxlJobHelper.log("task is done, ends at pageNo:{}", pageNo);break;}for (XxxDO pojo : list) {// 处理数据this.processPageData(pojo);}// 下一轮pageNo++; // 分页:总共1500w条,会出现深分页XxlJobHelper.log("next round, pageNo:{}", pageNo);
}

上面的定时任务,实际开发中的表现一般是:前面几千、几万条数据很快,大概几分钟就搞定了,但是到了10w、20w时,基本每100条要花十几秒甚至几分钟,最后任务会直接因为深分页卡死。

最简单的解决办法是:使用ID分页。

while (true) {// 分页获取符合条件的数据List<XxxDO> list = this.channelLiveConfIService.lambdaQuery().ge(XxxDO::getId, startId)  // ID也作为分页条件.condition(...)             // 业务条件:height>180 && weight>65 && city='杭州'.page(new Page<>(pageNo, pageSize, false)).getRecords();if (CollectionUtils.isEmpty(list)) {XxlJobHelper.log("task is done, task id ends at:{}", startId);break;}for (XxxDO pojo : list) {// 处理数据this.processPageData(pojo);}// 下一轮startId = list.get(list.size() - 1).getId() + 1;XxlJobHelper.log("task next round, startId:{}", startId);
}

使用ID作为分页的条件时,MySQL在筛选数据时会直接跳到startId所在行,然后往后“数符合条件”的pageSize条数据,而下一轮的startId为上一轮筛选结果的最后一条dataId+1。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/310623.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

初识SpringBoot(2023最后一篇文章)

初识SpringBoot 1、SpringBoot概述 Spring是什么&#xff1f; Spring是一个于2003 年兴起的一个轻量级开源Java开发框架&#xff0c;由Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》。Spring是为了解决企业级应用开发的复杂性而创建的&#xff0c;使…

软件工程期末复习习题

知识点总结 第一章&#xff1a;软件工程概述 1、软件的定义&#xff1a;在运行中能提供所希望的功能与性能的程序使程序能够正确运行的数据及其结构描述软件研制过程和方法所用的文档。 2、软件危机&#xff1a;软件开发的生产率远远不能满足客观需要。开发的软件产品往往不能…

学习动态规划解决不同路径、最小路径和、打家劫舍、打家劫舍iii

学习动态规划|不同路径、最小路径和、打家劫舍、打家劫舍iii 62 不同路径 动态规划&#xff0c;dp[i][j]表示从左上角到(i,j)的路径数量dp[i][j] dp[i-1][j] dp[i][j-1] import java.util.Arrays;/*** 路径数量* 动态规划&#xff0c;dp[i][j]表示从左上角到(i,j)的路径数量…

迅软科技助力高科技防泄密:从华为事件中汲取经验教训

近期&#xff0c;涉及华为芯片技术被窃一事引起广泛关注。据报道&#xff0c;华为海思的两个高管张某、刘某离职后成立尊湃通讯&#xff0c;然后以支付高薪、股权支付等方式&#xff0c;诱导多名海思研发人员跳槽其公司&#xff0c;并指使这些人员在离职前通过摘抄、截屏等方式…

车牌识别系统设计与实现

车牌识别系统设计与实现 项目概述 本项目旨在设计和实现一套车牌识别系统&#xff0c;通过使用车牌字符数据集进行训练&#xff0c;应用OpenCV、CNN&#xff08;卷积神经网络&#xff09;和PyQt5技术&#xff0c;实现车牌图像的预处理、位置选定、定位、字符分割和最终的车牌…

【Matlab】CNN卷积神经网络时序预测算法

资源下载&#xff1a; https://download.csdn.net/download/vvoennvv/88681558 一&#xff0c;概述 CNN&#xff08;Convolutional Neural Network&#xff0c;卷积神经网络&#xff09;是一种前馈神经网络&#xff0c;主要用于处理具有类似网格结构的数据&#xff0c;例如图像…

24、Web攻防——通用漏洞SQL注入MYSQL跨库ACCESS偏移

文章目录 一、SQL注入原理   脚本代码在与数据库进行数据通讯时&#xff08;从数据库取出相关数据进行页面显示&#xff09;&#xff0c;使用预定义的SQL查询语句进行数据查询。能通过参数传递自定义值来实现SQL语句的控制&#xff0c;执行恶意的查询操作&#xff0c;例如查询…

大模型推理部署:LLM 七种推理服务框架总结

自从ChatGPT发布以来&#xff0c;国内外的开源大模型如雨后春笋般成长&#xff0c;但是对于很多企业和个人从头训练预训练模型不太现实&#xff0c;即使微调开源大模型也捉襟见肘&#xff0c;那么直接部署这些开源大模型服务于企业业务将会有很大的前景。 本文将介绍七中主流的…

【机器学习合集】深度生成模型 ->(个人学习记录笔记)

深度生成模型 深度生成模型基础 1. 监督学习与无监督学习 1.1 监督学习 定义 在真值标签Y的指导下&#xff0c;学习一个映射函数F&#xff0c;使得F(X)Y 判别模型 Discriminative Model&#xff0c;即判别式模型&#xff0c;又称为条件模型&#xff0c;或条件概率模型 生…

flex--伸缩性

1.flex-basis flex-basis 设置的是主轴方向的基准长度&#xff0c;会让宽度或高度失效。 备注&#xff1a;主轴横向&#xff1a;宽度失效&#xff1b;主轴纵向&#xff1a;高度失效 作用&#xff1a;浏览器根据这个属性设置的值&#xff0c;计算主轴上是否有多余空间&#x…

电表通讯协议DLT645-2007编程

1、协议 电表有个电力行业推荐标准《DLT645-2007多功能电能表通信协议》&#xff0c;电表都支持&#xff0c;通过该协议读取数据&#xff0c;不同的电表不需要考虑编码格式、数据地址、高低位转换等复杂情况&#xff0c;统一采集。 不方便的地方在于这个协议定义得有点小复杂…

4.33 构建onnx结构模型-Expand

前言 构建onnx方式通常有两种&#xff1a; 1、通过代码转换成onnx结构&#xff0c;比如pytorch —> onnx 2、通过onnx 自定义结点&#xff0c;图&#xff0c;生成onnx结构 本文主要是简单学习和使用两种不同onnx结构&#xff0c; 下面以 Expand 结点进行分析 方式 方法一…