边界判断缺失

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

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

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

阶段1、深入多线程

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

阶段3、深入juc源码解析

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

阶段5、深入jvm源码解析

事故场景

我们在做需求开发时,经常会遇到一些边界条件的判断:

  • 查询身高大于180cm、年龄大于18岁的学生
  • 筛选出公司P7以上的程序员

大多数时候,我们只需要把产品语言转化为程序语言即可:

SELECT * FROM t_student WHERE height>180 and age>18;
SELECT * FROM t_employee WHERE level>=7;

但产品语言与程序语言并不完全等同,直接翻译有时会引发意想不到的BUG。

假设现在有一个需求:

  • 下单24小时以后,为用户发放奖励

一般来说,我们可以采用定时任务完成这个需求,具体做法是:

  • 用户下单后,在order_task表插入一条记录(为了方便记忆,type和status都用字符串代替):
# 一张任务表不止一种类型的任务,所以用type区分,和当前需求关系不大
INSERT INTO order_task (order_id, task_type, task_status, gmt_create, gmt_modify) 
VALUES(10000001, 'complete_order_prize', 'wait', 'xxxx-xx-xx', 'xxxx-xx-xx');
  • 定时任务扫描需要发放下单奖励的任务、为用户发放奖励、更新任务状态:
# 伪代码 86400=24*60*60
SELECT * FROM order_task WHERE order_type='complete_order_prize' and task_status='wait' and gmt_create < now-86400;

注意时间条件,由于产品需求是下单24小时后才发放奖励,所以要满足条件:gmt_create < now-86400。

正常来说,上面的做法是没有问题的。但是,昨天产品找我说,用户反馈自己一个月以前的3笔订单奖励至今未发放。

这位产品很优秀,比较懂开发,甚至自己会写SQL,他的第一反应是定时任务挂了。

好了,现在问题出现了,如果是你,打算怎么开始排查呢?

解决方案

先介绍一下背景,我们公司由于业务调整,上面业务所在的平台已经很久没迭代,我抓包看了下,发现底层逻辑是PHP写的。于是找到对应的工程及代码,确认近期确实没有人更改过代码逻辑。

紧接着,我打开公司的定时任务平台,查看了该任务过去一个月的执行日志,发现全都执行失败了。

定时任务是否执行成功,其实有两个指标:

  • 调度成功
  • 执行成功

调度成功只能说明定时任务没问题,机器也没挂,但具体本次操作是否执行完毕,需要看执行结果。

从上图可以看到,定时任务每五分钟执行一次,可以调度成功(尽管也有调度失败的),但执行结果无一例外都是失败的。

这说明定时任务本身是好的,只是执行过程出了问题。查看定时任务的执行策略,我发现采用的是丢弃后续调度:

这是一种什么策略呢?举个例子,假设定时任务每隔5分钟执行一次,而上一个任务A本次执行超时了(超过5分钟还在执行),当下一个任务B启动时结果发现上一个任务还在进行中,那么任务B就会被丢弃,等待下次任务C执行。

那么,现在就有个矛盾摆在我们面前:

  • 定时任务是好的,但执行过程出问题了
  • 然而我们排查后,确定代码近期没有变更

代码没动过,定时任务也没挂,然而却没有达到预期。一个线上的功能好端端的突然崩了,无非几点原因:

  • 用户行为改变,恰好命中之前没测到的bug
  • 其他人发布新代码,对当前功能产生影响
  • 数据出问题了

订单奖励属于定时任务,不存在用户行为,这个原因基本可以排除。只有第二、第三个原因,其实归根到底都是数据问题,A功能要想影响B功能,一定是两者有共同的数据,也就是有共同的表。但不论哪种原因,我们可以得出结论:一个运行好几年的定时任务跑着跑着突然崩了,大概率是数据出问题了。

但我得出这个结论,却是在2小时后。一方面,恰好这个功能的PHP代码以及远程调用的Java服务都没有打印日志,对问题排查造成了很大的困难。由于系统已经基本停止维护,为了找到当前工程的日志,花了将近1小时。另一方面,电商系统太大了,只能上预发环境测试。等给工程打上日志、重新发布,已经过去10分钟。希望大家吸取教训,实际开发时在必要的地方打上日志,方便日后排查问题。

那么,最终是什么原因导致定时任务不执行了呢?

下面是一段PHP代码,做了简化处理,具体细节不用理解:

/**
 * 定时任务脚本 处理签到下单任务奖励金的实质发放
 * @return bool
 */
public function finishSignOrderTask() {
    load_module_model('xxx');
    $start_time = time() - 86400; // 24小时以后的订单

    // 查出符合条件的最老的一条的记录
    $oldest_record = $this->CI->xxx_sign_task->getWaitFinishTask(self::TASK_ID_ORDER_TASK, $start_time, \Xxx_sign_task::TASK_STATUS_COMPLETE, 'asc');
    if (empty($oldest_record)) {
        return TRUE;
    }
    $start = $oldest_record->id;

    // 查出符合条件最新的一条记录
    $end_id = $this->CI->xxx_sign_task->getWaitFinishTask(self::TASK_ID_ORDER_TASK, $start_time, \Xxx_sign_task::TASK_STATUS_COMPLETE, 'desc')->id;

    // 最新~最老记录之间都是需要执行的订单奖励,具体做法是 每次从中捞出100条执行(start_id + 100),直到退出(start_id+100>end_id)
    $page_size = 100;
    $xxxSignAwardService = XxxSignAwardService::getInstance();
    while (TRUE) {
        $where = [
            'id >= ' . $start,
            'id < ' . ($start + $page_size),
            'task_id = ' . self::TASK_ID_ORDER_TASK,
            'task_status < ' . \Xxx_sign_task::TASK_STATUS_FINISH,
            'gmt_begin < ' . $start_time,
        ];
        $records = $this->CI->xxx_sign_task->get_by_where($where, '*', 1, $page_size);
        if (!empty($records)) {
            foreach ($records as $record) {
                // 再次校验订单状态
                $order = $this->CI->xxx_order->get_by_oid($record->biz_value);
                if (empty($order)) {
                    $this->logger->error("oid_deal_not_found", [$record]);
                    // 订单不存在 关闭任务
                    $this->updateTaskStatus(FALSE, $record);
                    continue;
                }

                // 发放奖励...
            }
        }

        if ($start > $end_id) {
            break;
        }
        $start += $page_size;
    }

    return TRUE;
}

我在观察表数据时,发现了一个现象:最早一条处理失败的奖励任务竟然是2020年12月的,它的状态仍旧是task_status='wait'!

这会产生一个什么现象呢?(划线表示已执行)

  • 1000000 2020-12-27 任务1(start)
  • 1000001 2020-12-27 任务2
  • 1000002 2020-xx-xx 任务...
  • ...
  • 2000001 2021-04-27 任务N
  • ...(后续新的任务)
  • ...
  • 2100001 2021-06-04 任务Z(end)

由于上面PHP代码的做法是:查出start~end之间所有数据,并从start开始每次查询100条数据。随着时间越来越长,到了2021-04-27时,任务1和任务N之间id号已经差了100w,即使除以pageSize=100,也需要循环查1w次,而这1w次SQL查询是查不到数据的(因为之前被处理过了)!

换句话说,每次定时任务开始,虽然目标是执行2021-04-27 ~ 2021-06-04之间的奖励任务,但却不得从2020-12-27的数据开始,以id+100的形式查询1w次后,才能到达任务N。

注意,while并不是空转,而是真的去查数据库了!空转1w次根本不算什么,但循环查询数据库1w次是不可接受的,即使分页查询一次200ms,整个过程也要耗时2000s,要30多分钟,而定时任务频率为5分钟。然而耗时长并不是任务失败的主要原因,最关键的是单任务循环次数过多导致日志输出及内存占用增加,最终每次还没执行到需要的数据,PHP自己就先挂了。

解决方法是,查询奖励任务时,加一个边界判断:下单24小时后,且最近3个月内的订单。

至于为什么2020年的那个任务一直没被执行掉,是因为它的任务流水不知为何被删除了(或者当初插入失败了),而Java远程服务会做流水校验,如果流水为空则抛异常跳过本次执行,所以2020年的那个数据躲过了一轮又一轮的定时任务,最终出现在2021年的某一天,突然抡起一锤子砸向了俺,尽管这个需求不是俺做的...

坑啊!!

之后隔天观察一下,发现任务已经正常了:

个人建议

  • 注意边界判断,不要做产品语言的翻译机,要从开发的角度考虑设计是否合理
  • 养成打日志的习惯有利于问题排查

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

idea部署javaSE项目(awt+swing项目)_idea导入eclipse的javaSE项目

一.idea打开项目 选择需要部署的项目 二、设置JDK 三、引入数据库驱动包 四、执行sql脚本 四、修改项目的数据库连接 找到数据库连接文件 五.其他系统实现 JavaSwing实现学生选课管理系统 JavaSwing实现学校教务管理系统 JavaSwingsqlserver学生成绩管理系统 JavaSwing用…

R_handbook_作图专题

ggplot基本作图 1 条形图 library(ggplot2) ggplot(biopics) geom_histogram(aes(x year_release),binwidth1,fill"gray") 2 堆砌柱状图 ggplot(biopics, aes(xyear_release)) geom_bar(aes(fillsubject_sex)) 3 堆砌比例柱状图 ggplot(biopics, aes(xyear_rele…

Go语言中的HTTP重定向

大家好&#xff0c;我是你们可爱的编程小助手&#xff0c;今天我们要一起探讨如何使用Go语言实现HTTP重定向&#xff0c;让我们开始吧&#xff01; 大家都知道&#xff0c;网站开发中有时候需要将用户的请求从一个URL导向到另一个URL。比如说&#xff0c;你可能想将旧的URL结构…

ssm基于冲突动态监测算法的健身房预约系统的设计与实现论文

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装健身房预约系统软件来发挥其高效地信息处理的作用&#xff…

爬虫详细教程第1天

爬虫详细教程第一天 1.爬虫概述1.1什么是爬虫&#xff1f;1.2爬虫工具——Python1.3爬虫合法吗&#xff1f;1.4爬虫的矛与盾1.4.1反爬机制1.4.2反爬策略1.4.3robots.txt协议 2.爬虫使用的软件2.1使用的开发工具: 3.第一个爬虫4.web请求4.1讲解一下web请求的全部过程4.2页面渲染…

SaaS版Java基层健康卫生云HIS信息管理平台源码(springboot)

云his系统源码&#xff0c;系统采用主流成熟技术开发&#xff0c;B/S架构&#xff0c;软件结构简洁、代码规范易阅读&#xff0c;SaaS应用&#xff0c;全浏览器访问&#xff0c;前后端分离&#xff0c;多服务协同&#xff0c;服务可拆分&#xff0c;功能易扩展。多集团统一登录…

二手房交易流程及避坑指南

文章目录 一、写作目的二、主要流程1、查档2、签定金合同3、网签4、交首付5、解押过户6、出产证7、拿房款8、交房 一、写作目的 近几个月房价一直跌跌不休&#xff0c;对于投资客来说这段时间肯定不好过&#xff0c;但这段时间也正是置换房子的好时候&#xff0c;在这次的房产…

病理HE学习贴(自备)

目录 正常结构 癌症HE 在线学习 以胃癌的学习为例 正常结构 1&#xff1a;胃粘膜正常结构和细胞分化 ●表面覆盖小凹上皮细胞(主要标志物&#xff1a;MUC5AC)以保护黏膜。 ●胃底腺固有腺体由黏液颈细胞(MUC6)、主细胞(Pepsinogen l)和壁细胞(Proton pump α-subunit)组…

计算机网络【DHCP动态主机配置协议】

DHCP 出现 电脑或手机需要 IP 地址才能上网。大刘有两台电脑和两台手机&#xff0c;小美有一台笔记本电脑、一台平板电脑和两台手机&#xff0c;老王、阿丽、敏敏也有几台终端设备。如果为每台设备手动配置 IP 地址&#xff0c;那会非常繁琐&#xff0c;一点儿也不方便。特别是…

【形式语言与自动机/编译原理】CFG->Greibach->NPDA(1)

本文将详细讲解《形式语言与自动机》&#xff08;研究生课程&#xff09;或《编译原理》&#xff08;本科生课程&#xff09;中的上下文无关文法&#xff08;CFG&#xff09;转换成Greibach范式&#xff0c;再转成下推自动机&#xff08;NPDA&#xff09;识别语言是否可以被接受…

ES6之迭代器(Iterator)

✨ 专栏介绍 在现代Web开发中&#xff0c;JavaScript已经成为了不可或缺的一部分。它不仅可以为网页增加交互性和动态性&#xff0c;还可以在后端开发中使用Node.js构建高效的服务器端应用程序。作为一种灵活且易学的脚本语言&#xff0c;JavaScript具有广泛的应用场景&#x…

12.29_黑马数据结构与算法笔记Java

目录 305 旅行商问题 动态规划 实现2 306 旅行商问题 动态规划 实现3 307 分治 概述 308 快速选择算法 分治 309 快速选择算法 数组第k大数 Leetcode215 310 快速选择算法 数组中位数 311 快速幂 分治 312 快速幂 Leetcode50 313 平方根整数部分 Leetcode69-1 314 平方…

阿里云PolarDB数据库优惠价格表11元一天起

阿里云数据库PolarDB租用价格表&#xff0c;云数据库PolarDB MySQL版2核4GB&#xff08;通用&#xff09;、2个节点、60 GB存储空间55元5天&#xff0c;云数据库 PolarDB 分布式版标准版2核16G&#xff08;通用&#xff09;57.6元3天&#xff0c;阿里云百科aliyunbaike.com分享…

《Python机器学习原理与算法实现》学习笔记

以下为《Python机器学习原理与算法实现》&#xff08;杨维忠 张甜 著 2023年2月新书 清华大学出版社&#xff09;的学习笔记。 根据输入数据是否具有“响应变量”信息&#xff0c;机器学习被分为“监督式学习”和“非监督式学习”。 “监督式学习”即输入数据中即有X变量&…

ros2查看launch文件内需要提供的参数(接口):

格式&#xff1a;ros2 launch --show-args 包名称 launch文件名称 例如&#xff1a; ros2 launch --show-args ros_gz_sim gz_sim.python.py

区块链的三难困境是什么,如何解决?

人们需要保持社交、工作和睡眠之间的平衡&#xff0c;并且努力和谐相处。同样的概念也反映在区块链的三难困境中。 区块链三难困境是一个术语&#xff0c;指的是现有区块链的局限性&#xff1a;可扩展性、安全性和去中心化。这是一个存在了几十年的设计问题&#xff0c;其问题的…

【python高级用法】迭代器、生成器、装饰器、闭包

迭代器 可迭代对象&#xff1a;可以使用for循环来遍历的&#xff0c;可以使用isinstance()来测试。 迭代器&#xff1a;同时实现了__iter__()方法和__next__()方法&#xff0c;可以使用isinstance()方法来测试是否是迭代器对象 from collections.abc import Iterable, Iterat…

Hadoop安装笔记1单机/伪分布式配置_Hadoop3.1.3——备赛笔记——2024全国职业院校技能大赛“大数据应用开发”赛项——任务2:离线数据处理

将下发的ds_db01.sql数据库文件放置mysql中 12、编写Scala代码&#xff0c;使用Spark将MySQL的ds_db01库中表user_info的全量数据抽取到Hive的ods库中表user_info。字段名称、类型不变&#xff0c;同时添加静态分区&#xff0c;分区字段为etl_date&#xff0c;类型为String&am…

人工智能的第一性原理

今天跟大家分享一篇 北师大 - 图像处理研究中心主任 郭平教授的一篇文章 通过“四个问题”&#xff0c; 解释了人工智能的第一性原理 提出了如何运用第一性原理思维 来解决人工智能缺乏基本常识的问题 并且他建议将最小作用量原理 作为人工智能的第一性原理 什么是第一…

排序算法讲解

1&#xff09;排序思想&#xff1a; 2&#xff09;排序代码&#xff1a; 3&#xff09;注意点&#xff1a; 4&#xff09;时间/空间复杂度和稳定性 下面的排序是以实现升序讲解的。 &#xff08;一&#xff09;直接插入排序 1&#xff09;排序思想&#xff1a; 把待排序的…