当前位置:首页 » 操作系统 » 抢单任务源码

抢单任务源码

发布时间: 2024-04-02 11:10:49

Ⅰ #抬抬小手学Python# 用别人代码完成我的工作,愉快摸鱼“附源码

模块是一个概念,它包含 1~N 个文件,如果文件是 Python 代码文件(就是.py 结尾的文件),那每个文件中可以包含函数,类等内容。

在公司工作,很多项目都是协作开发来完成,一个项目后面可能存在很多工程师,为了开发方便,每个人负责的功能函数或者类都尽量封装在一个 模块 中,模块英文请记住 mole ,有的地方叫做 库 ,也有的地方叫做 包(package) ,对于现阶段的你来说,当成一样的内容就好。

互联网上存在大量的开源模块,这些模块最大的优势就是免费,很多时候使用这些模块能极大的提高编码效率,这也是很多人喜欢 Python 的原因之一。

模块学习的过程,不能按照语法结构来学习,它是一种抽象的知识,是一种代码的设计方式。例如将写好的函授放到模块中。

接下来就将上面的函数整合到一个模块中去,建立一个新的文件 stir_fry.py 然后将两个函数复制到新的文件中。

stir_fry.py 文件包含那两个函数

好了,完成任务,一个模块创建完毕了,这个 stir_fry.py 文件就是一个模块。

你现在脑中肯定出现黑人问号脸了,What?这就完了。是的,完了,一个低配模块完成。

下面就可以拿着这个模块给别人使用去了。会写模块成为大佬之后,就可以给新入行的菜鸟指点江山,写模块了。

在另一个文件中,可以通过 import 模块名 导入一个模块,例如导入刚才创建的 stir_fry 模块。

注意要新建一个文件,文件名随意但是不要与模块同名。

如果想要使用模块中的函数,只需要参考下述语法格式即可。

通过 stir_fry 调用模块中的函数。

当通过 import stir_fry 导入模块之后,该模块内的所有函数都一次性导入到新文件中了。

如果不想导入模块的所有函数,而只导入某个函数,使用一下语法可以解决该问题。

修改上一节案例:

直接导入模块中的函数,使用时不需要通过 模块名. 的方式调用,直接书写函数名即可。

导入模块中多个函数

语法格式如下:

导入模块所有函数

语法格式如下:

刚才通过模块导入函数你应该发现一个潜在的问题,就是函数名称太长怎么办,除了名称太长,还存在一种情况,模块中的函数名称与当前文件中函数的名称,存在重名的风险。此时可以学习一个新的内容,通过 as 给模块导入进来的函数起个别名,然后在该文件都使用别名进行编码。

语法格式如下:

上述内容应用到案例中如下述代码:

as 别名也可直接作用于模块,语法格式如下:

随着程序设计变的越来越复杂,只把函数放到模块中已经不能满足要求了,需要将更高级的内容放到模块中,也就是类。

首先在 dog_mole.py 文件中定义一个类。

此时的 dog_mole 就是模块的名称,而在该模块中只有一个类 Dog ,也可以在该模块中多创建几个类,例如:

与导入模块的函数部分知识一样,如果希望导入一个模块中的类,可以直接通过下述语法格式实现:

使用模块中的类,语法格式如下:

具体代码不在演示,自行完成吧。

导入模块的类和导入模块的的函数用法是一致的。

新建一个 demo.py 文件,在该文件导入 dog_mole 模块中的类。

从模块中导入多个类

该方式与函数的导入也一致,语法格式如下:

导入模块中所有类

学到这里,你应该已经发现导入模块中的函数与导入模块中的类,从代码编写的角度几乎看不出区别,对比着学习即可。

导入类的时候也可以应用别名,同样使用 as 语法。

学习到这里你对模块是什么,模块怎么用已经有了一个基本认知,接下来先不用自己写一个特别牛的模块,我们先把一些常见的模块应用起来。

通过随机数模块可以获取到一个数字,它的使用场景非常广,例如 游戏 相关开发、验证码相关、抽奖相关,学习了随机数之后可以完成一些非常不错的小案例。

randint 方法

导入随机数模块之后,可以通过 randint 方法随机生成一个整数,例如下述代码:

反复运行代码会得到一个 1~10 之间的数字,由此可以 randint 方法中的参数含义。

choice 方法

通过 choice 方法可以配合列表实现一些效果,choice 可以随机返回列表中的一个元素。

如果你想知道 choice 方法的具体用法,还记得怎么查询吗?

shuffle 方法

该方法可以将一个列表的顺序打乱。

简单挑选了 random 模块中的三个方法做为说明,对于模块的学习,后面将为每个模块单开一篇文章书写。

时间模块是 Python 中非常重要的一个内置模块,很多场景都离不开它,内置模块就是 Python 安装好之后自带的模块。

time 方法
time 模块主要用于操作时间,该方法中存在一个 time 对象,使用 time 方法之后,可以获取从 1970年1月1日 00:00:00 到现在的秒数,很多地方会称作时间戳。

输出内容:

sleep 方法
该方法可以让程序暂停,该方法的参数是的单位是

使用语法格式为:

asctime 与 localtime 方法

以上两个方法都可以返回当前系统时间,只是展示的形式不同。

time 模块涉及的方法先只涉及这么多,后续滚雪球学习过程中在继续补充。

Python 还内置了很多模块,例如 sys 模块、os 模块、json 模块、pickle 模块、shelve 模块、xml 模块、re 模块、logging 模块等等内容,后续都将逐步学习到,有可能需要分开专题给大家讲解。

Python 模块,快速编码的一种途径,很多时候第三方模块可以帮你解决大多数常见编码场景,让你在编码的道路上飞奔。

Ⅱ 开发一套微信小程序抢单系统价格多少

要看选择的开发方式,比如:
1、自己组建技术团队自己开发,需要的人员有产品经理、框架工程师、java、PHP、前端、后端、测试工程师,开发周期在1-2个月。人员成本10-20万,后期维护成本没算。(不推荐)
2、购买别人的小程序源码,并且自己配置服务器,再找个技术人员专职维护。源码费用一般10000-30000,服务器一年至少3000,维护成本每月6000以上。(不推荐)
3、使用第三方小程序,购买第三方小程序使用账号,总费用根据自身需求,费用2000以内到几千元不等,不用担心技术维护、不用建服务器,拿过来就可以使用,还可以根据自己的搭建要求设计店铺和绑定公众号。(推荐)

Ⅲ 求java题源代码,最好有注释,

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;

/**
* 多线程,小球演示. 打开Windows任务管理器,可看到线程变化。 可搜索到,run()方法/.start()
*
* : 程序技巧体会: 所谓产生一个小球,即是 new 其类对象,其属性携带画小球的 坐标、颜色、所在容器 等参数。
*
* 一个类,属性用来作为参数容器用, 方法....完成功能。
* */
// 运行类
public class BouncePress {
//
public static void main(String[] args) {
JFrame frame = new BouncePressFrame(); // 生成窗口。执行构造。-----业务逻辑。
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // similar to
// window
// listener
frame.show();
}
}

class BouncePressFrame extends JFrame {
private BallPressCanvas canvas;

public BouncePressFrame() {
setSize(600, 500); // 窗口大小
setTitle("Bounce Ball");
Container contentPane = getContentPane(); // Swing的窗口不能直接放入东西,只能在其上的ContentPane上放。
canvas = new BallPressCanvas(); // 生成一个新面板。-----canvas
contentPane.add(canvas, BorderLayout.CENTER); // 窗口中心 加入该面板。
JPanel buttonPanel = new JPanel(); // 再生成一个新面板。----buttonPanel

// 调用本类方法addButton。
addButton(buttonPanel, "Start", // 生成一个按钮"Start"---加入面板buttonPanel
new ActionListener() { // |------>按钮绑上 action监听器。
public void actionPerformed(ActionEvent evt) { // | 小球容器对象的
addBall(Thread.NORM_PRIORITY - 4, Color.black); // 事件处理时,执行---addBall()方法。--->产生小球(参数对象)--->加入List中--->开始画球。
}
}); // 按一次,addBall()一次--->产生一个新小球--->加入List中--->开始画此新小球。
// --->画球线程BallPressThread的run()--->小球(参数对象).move()--->每次画时,先移动,再判断,再画。
// --->BallPressCanvas类的canvas对象.paint()--->自动调BallPressCanvas类的paintComponent(Graphics
// g)方法。
// --->该方法,从List中循环取出所有小球,第i个球,--->调该小球BallPress类
// .draw()方法--->调Graphics2D方法画出小球。--使用color/
addButton(buttonPanel, "Express", new ActionListener() {
public void actionPerformed(ActionEvent evt) {
addBall(Thread.NORM_PRIORITY + 2, Color.red);
}
});

addButton(buttonPanel, "Close", new ActionListener() {
public void actionPerformed(ActionEvent evt) {
System.exit(0);
}
});

contentPane.add(buttonPanel, BorderLayout.SOUTH);
}

public void addButton(Container c, String title, ActionListener listener) {
JButton button = new JButton(title); // 生成一个按钮。
c.add(button); // 加入容器中。
button.addActionListener(listener); // 按钮绑上 action监听器。
}

/** 主要业务方法。 */
public void addBall(int priority, Color color) {
// 生成 小球(参数对象)
BallPress b = new BallPress(canvas, color); // 生成BallPress对象,携带、初始化
// 画Ball形小球,所需参数:所在容器组件,所需color--black/red.
// 小球加入 List中。
canvas.add(b); // 面板canvas 的ArrayList中 加入BallPress对象。

BallPressThread thread = new BallPressThread(b); // 生成画小球的线程类BallPressThread对象。传入BallPress对象(携带了画球所需
// 容器、color参数)。
thread.setPriority(priority);
thread.start(); // call run(), ball start to move
// 画球线程开始。--->BallPressThread的run()--->小球(参数对象).move()--->先移动,再画。canvas.paint--->BallPressCanvas类的
}
}

// 画球的线程类。
class BallPressThread extends Thread {
private BallPress b;

public BallPressThread(BallPress aBall) {
b = aBall;
}

// 画球开始。
public void run() {
try {
for (int i = 1; i <= 1000; i++) { // 画1000次。
b.move(); // 每次画时,先移动,再判断,再画。
sleep(5); // 所以移动比Bounce.java的球慢。
}
} catch (InterruptedException e) {
}
}
}

// swing面板类.
// 作用1) 本类面板对象.paint()方法---->自动绘制面板,且自动调paintComponent(Graphics
// g)方法,--->重写该方法,绘制面板(及其上组件)。
// 作用2) 该类对象 属性ArrayList balls---兼作小球(参数对象)的容器。
class BallPressCanvas extends JPanel {
private ArrayList balls = new ArrayList();

public void add(BallPress b) {
balls.add(b); // 向ArrayList中添加球。当按下按钮,添加多个球时,都保存在这个List中。
}

// 重写了 javax.swing.JComponent的 paintComponent()方法。
// paint()方法自动调用该方法。
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
for (int i = 0; i < balls.size(); i++) { // 循环
BallPress b = (BallPress) balls.get(i); // 从List中取出第i个球,
b.draw(g2); // 画此球。
}
}
}

/**
* 画出球。
*
* 在 canvas上画出,color色的小球图形。
*
* 属性,可用于携带画小球所需参数。
*
*
*
* @author congan
*
*/
class BallPress {
private Component canvas;
private Color color;
private int x = 0;
private int y = 0;
private int dx = 2;
private int dy = 2;

// 构造 初始化 容器 颜色 参数。
public BallPress(Component c, Color aColor) {
canvas = c;
color = aColor;
}

// 制定位置,画出小球。
public void draw(Graphics2D g2) {
g2.setColor(color);
g2.fill(new Ellipse2D.Double(x, y, 15, 15)); // ellipse:椭圆形
}

// 移动小球。
// 每次画时,先移动,再判断,再画。
// 该方法每次执行,画小球的起点坐标 (x,y), 每次各自+2, 即斜向右下运动。
public void move() {
x += dx; // x=x+dx; 画小球的起点坐标 (x,y), 每次各自+2, 即斜向右下运动。
y += dy; // y=y+dy;
if (x < 0) { // 小球已到左边框。保证,从左边框开始画。
x = 0;
dx = -dx; // 小球横坐标变化值取反。开始反向运动。
}
if (x + 15 >= canvas.getWidth()) { // 小球右边已经到画板右边。
x = canvas.getWidth() - 15;
dx = -dx; // 开始反向运动。
}
if (y < 0) { // 保证,从顶框开始画。
y = 0;
dy = -dy;
}
if (y + 15 >= canvas.getHeight()) { // 小球已到画板顶。
y = canvas.getHeight() - 15;
dy = -dy;
}
canvas.paint(canvas.getGraphics()); // 画出面板对象canvas----(及其上所有组件)
// //.paint()方法,自动调用
}
}

/*import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;

*//**
* 单线程,小球演示 搜索不到,run()方法/.start()
*//*
public class Bounce {
public static void main(String[] args) {
JFrame frame = new BounceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // similar to
// window
// listener
frame.show();
}
}

不懂的再问啊。。。

Ⅳ 任务协作系统源码和团队协作源码是一样的吗

有点差别,但共同点都是协作软件源码。Workless可量化的团队协作 软件提供源码出售。

Ⅳ 源代码什么意思

源代码(也称源程序),是指一系列人类可读的计算机语言指令。 在现代程序语言中,源代码可以是以书籍或者磁带的形式出现,但最为常用的格式是文本文件,这种典型格式的目的是为了编译出计算机程序。计算机源代码的最终目的是将人类可读的文本翻译成为计算机可以执行的二进制指令,这种过程叫做编译,通过编译器完成。
代码组合
源代码作为软件的特殊部分,可能被包含在一个或多个文件中。一个程序不必用同一种格式的源代码书写。例如,一个程序如果有C语言库的支持,那么就可以用C语言;而另一部分为了达到比较高的运行效率,则可以用汇编语言编写。
较为复杂的软件,一般需要数十种甚至上百种的源代码的参与。为了降低种复杂度,必须引入一种可以描述各个源代码之间联系,并且如何正确编译的系统。在这样的背景下,修订控制系统(RCS)诞生了,并成为研发者对代码修订的必备工具之一。
还有另外一种组合:源代码的编写和编译分别在不同的平台上实现,专业术语叫做软件移植。
质量
对于计算机而言,并不存在真正意义上的“好”的源代码;然而作为一个人,好的书写习惯将决定源代码的好坏。源代码是否具有可读性,成为好坏的重要标准。软件文档则是表明可读性的关键。
源代码主要功用有如下2种作用:
1、生成目标代码,即计算机可以识别的代码。
2、对软件进行说明,即对软件的编写进行说明。为数不少的初学者,甚至少数有经验的程序员都忽视软件说明的编写,因为这部分虽然不会在生成的程序中直接显示,也不参与编译。但是说明对软件的学习、分享、维护和软件复用都有巨大的好处。因此,书写软件说明在业界被认为是能创造优秀程序的良好习惯,一些公司也硬性规定必须书写。
(需要指出的是,源代码的修改不能改变已经生成的目标代码。如果需要目标代码做出相应的修改,必须重新编译。 )
如果按照源代码类型区分软件,通常被分为两类:自由软件和非自由软件。自由软件一般是不仅可以免费得到,而且公开源代码;相对应地,非自由软件则是不公开源代码。所有一切通过非正常手段获得非自由软件源代码的行为都将被视为非法。

Ⅵ XXL admin 源码解析

xxl-job 的 admin 服务是 xxl-job 的调度中心,负责管理和调度注册的 job,关于 xxl-job 的使用,可以阅读 “参考阅读” 中的《XXL-JOB分布式调度框架全面详解》,这里主要是介绍 admin 中的源码。

admin 服务除了管理页面上的一些接口外,还有一些核心功能,比如:

1、根据 job 的配置,自动调度 job;

2、接收 executor 实例的请求,实现注册和下线;

3、监视失败的 job,进行重试;

4、结束一些异常的 job;

5、清理和统计日志;

这些功能都是在 admin 服务启动后,在后台自动运行的,下面将详细介绍 admin 服务这些功能敏罩的实现。

XxlJobAdminConfig 是 admin 服务的配置类,在 admin 服务启动时,它除了配置 admin 服务的一些参数外,还会启动 admin 服务的所有后台线程。

该类的属性主要分为5类:

1、配置文件中的参数,比如 accessToken;

2、DAO 层各个数据表的 mapper;

3、Spring 容器中的一些 Bean,比如 JobAlarmer、DataSource 等;

4、私有变量 XxlJobScheler 对象;

5、私有静态变量 adminConfig,指向实例自身。

该类有两个重要方法,分别实现自接口 InitializingBean、DisposableBean,作用如下:

这两个方法分别调用了 XxlJobScheler 对象的 init 、 destroy 方法,源码如下:

XxlJobAdminConfig 作为 admin 服务的配置类,作用就是在 Spring 容器启动时,调用 XxlJobScheler 的初始化方法,来初始化和启动 admin 服务的功能。

XxlJobScheler 的作用就是调用各个辅助类(xxxHelper)来启动和结束不同的线程和功能,初始化方法 init 的代码如下:

下面我们主要介绍 init 中各个类及其作用,最后再简单一下介绍 destroy 的作用纤拿森。

当 admin 服务向 executor 实例发出一个调度请求来执行 job 时,会调用 XxlJobTrigger.trigger() 方法把要传输的参数(比如 job_id、jobHandler、job_log_id、阻塞策略等,包装成 TriggerParam 对象)传给 ExecutorBiz 对象来执行一次调度。

xxl-job 对调度过程做了两个优化:

JobTriggerPoolHelper 在 toStart 方法中初始化了它的两个线程池属性,代码如下:

每次有调度请求时,就会在这两个线程池中创建线程,创建线程的逻辑在 addTrigger 方法中。

不同 job 存在执行时长的差异,为了避免不同耗时 job 之间相互阻塞,xxl-job 根据 job 的响应时间,对 job 进行了区分,主要体现在:

如果快 job 与调用频繁的慢 job 在同一个线程池中创建线程,慢 job 会占用大量的线程,导致快 job 线程不能及时运行,降低了线程池和线程的利用率。xxl-job 通过快慢隔离,避免了这个问题。

不能,因为慢 job 还是会占用大量线程,抢占了快 job 的线程资源;增加线程池中的线程数不但没有提升利用率,还会导致大量线程看空闲,利用率反而降低了。最好的方法还是用两个线程池把两者隔离,可以合理地使用各自线程池的资源。

为了记录慢 job 的超时次毁亩数,代码中使用一个 map(变量 jobTimeoutCountMap )来记录一分钟内 job 超时次数,key 值是 job_id,value 是超时次数。在调用 XxlJobTrigger.trigger() 方法之前,会先判断 map 中,该 job_id 的超时次数是否大于 10,如果大于10,就是使用 slowTriggerPool,代码如下:

调用 XxlJobTrigger.trigger() 方法后,根据两个值来更新 jobTimeoutCountMap 的值:

和上面的代码相结合,一个 job 在一分钟内有10次调用超过 500 毫秒,就认为该 job 是一个 频繁调度且耗时的 job。

代码如下:

在该类中,属性变量 minTim 和 jobTimeoutCountMap 都使用 volatile 来修饰,保证了并发调用 addTrigger 时数据的一致性和可见性。

admin 服务发起 job 调度请求时,是在静态方法 public static void trigger() 中调用静态变量 private static JobTriggerPoolHelper helper 的 addTrigger 方法来发起请求的。minTim 和 jobTimeoutCountMap 虽然不是 static 修饰的,但可以看做是全局唯一的(因为持有它们的对象是全局唯一的),因此这两个参数维护的是 admin 服务全局的调度时间和超时次数,为了避免记录的数据量过大,需要每分钟清空一次数据的操作。

admin 服务提供了接口给 executor 来注册和下线,另外,当 executor 长时间(90秒)没有发心跳时,要把 executor 自动下线。前一个功能通过暴露一个接口来接收请求,后一个功能需要开启一个线程,定时更新过期 executor 的状态。

xxl-job 为了提升 admin 服务的性能,在前一个功能的接口接收到 executor 的请求时,不是同步执行,而是在线程池中开启一个线程,异步执行 executor 的注册和下线请求。

JobRegistryHelper 类就负责管理这个线程池和定时线程的。

线程池的定义和初始化代码如下:

executor 实例在发起注册和下线请求时,会调用 AdminBizImpl 类的对应方法,该类的方法如下:

可以看到,AdminBizImpl 类的两个方法都是调用了 JobRegistryHelper 方法来实现,其中 JobRegistryHelper.registry 方法代码如下(registryRemove 代码与之相似):

这两个方法是通过在线程池 registryOrRemoveThreadPool 中创建线程来异步执行请求,然后把数据更新或新建到数据表 xxl_job_registry 中。

当 executor 注册到 admin 服务后(数据入库到 xxl_job_registry 表),是不会在页面上显示的,需要要用户手动添加 job_group 数据(添加到 xxl_job_group 表),admin 服务会自动把用户添加的 job_group 数据与 xxl_job_registry 数据关联。这就需要 admin 定时从 xxl_job_group 表读取数据,关联 xxl_job_registry 表和 xxl_job_group 表的数据。

这个功能是与 “executor 自动下线” 功能在同一个线程中实现,该线程的主要逻辑是:

相关代码如下:

从这里可以看出,如果是对外接口(接收请求等)的功能,使用线程池和异步线程来实现;如果是一些自动任务,则是通过一个线程来定时执行。

如果一个 Job 调度后,没有响应返回,需要定时重试。作为一种“自动执行”的任务,很显然可以像前面 JobRegistryHelper 一样,使用一个线程定时重试。

在这个类中,定义了一个监视线程,以每10 秒一次的频率运行,对失败的 job 进行重试。如果 job 剩余的重试次数大于0,就会 job 进行重试,并把发送告警信息。线程的定义如下:

在这个线程中,它利用 “数据库执行 UPDATE 语句时会加上互斥锁” 的特性,使用了 “基于数据库的分布式锁”,代码如下所示:

在这个语句中,会把 jobLog 的状态设置为 -1,这是一个无效状态值,当其他线程通过有效状态值来搜索失败记录时,会略过该记录,这样该记录就不会被其他线程重试,达到的分布式锁的功能(这个锁是一个行锁)。或者说,-1状态类似于 java 中的对象头的锁标志位,表明该记录已经被加锁了,其他线程会“忽略”该记录。

在 try 代码块中加锁和解锁,如果加锁后重试时抛出异常,会导致该记录永远无法解锁。所以,应该在 finnally 块中执行解锁操作,或者使用 redis 给锁加一个过期时间来实现分布式锁。

从失败的日志中取出 jobId,查询出对应的 jobInfo 数据,如果日志中的剩余重试次数大于 0,就执行重试。代码如下:

调度任务使用的就是前面介绍的 JobTriggerPoolHelper.trigger 方法,最后更新 jobLog 的 alarm_status 值,有两个作用:

这个类与 JobRegistryHelper 类似,都有一个线程池、一个线程,通过前面 JobRegistryHelper 的学习,可以大胆猜测:

实际上,该类中线程池和线程的作用就是用来 “完成” 一个 job。

当 executor 接收到 admin 的调度请求后,会异步执行 job,并立刻返回一个回调。

admin 接受到回调后,和前面的 “注册、下线” 一样,在线程池中创建线程来处理回调,主要是更新 job 和日志。

当有回调请求时, public callback 方法(该方法被 AdminBizImpl 调用)会在线程池中创建一个线程,遍历回调请求的参数列表,依次处理回调参数,代码如下:

从代码可以看出,最后调用 XxlJobCompleter.updateHandleInfoAndFinish 方法完成回调逻辑。

如果一个 job 较长时间前被调度,但是一直处于 “运行中” 且它所属的 executor 已经超过 90 秒没有心跳了,那么可以认为该 job 已经丢失了,需要把该 job 结束掉。这个就是线程 monitorThread 的主要功能。

monitorThread 会以 60秒 一次的频率,从 xxl_job_log 表中找出 10分钟前调度、仍处于”运行中“状态、executor 已经下线 的 job,然后调用 XxlJobCompleter.updateHandleInfoAndFinish 来更新 handler 的信息和结束 job,代码如下:

从代码可以看出,上面的两个功能最后都调用了 XxlJobCompleter.updateHandleInfoAndFinish 方法,关于该方法的介绍,可以看后面 XxlJobCompleter 部分的介绍,这里不详细展开。

如果去看 XxlJobTrigger.triger 方法,会发现每次调度 job 时,都会先新增一个 jobLog 记录,这也是为什么 JobFailMonitorHelper 中的线程在重试时,先查询 jobLog 的原因。

JobLog 作为 job 的调度记录,还可以用来统计一段时间内 job 的调度次数、成功数等;另外,会清理超出有效期(配置的参数 logretentiondays )的日志,避免日志数据过大。很显然,这又是一个 ”自动任务“,可以使用一个线程定时完成。

该类持有一个线程变量,线程以 每分钟一次的频率,执行两个操作:

在线程 run 方法的前半部分,线程会统计 3 天内,每天的调度次数、运行次数、成功运行数、失败次数;然后更新或新增 xxl_job_log_report 表的数据。

在线程 run 方法的后半部分,线程按天对日志进行清理,如果当前时间与上次清理的时间相隔超过一天,就会清理日志记录,代码如下:

如果不使用参数 lastCleanLogTime 来记录上次清理的时间,只是清理一天前创建的数据记录。那么该线程每分钟执行一次时,都会删除前天当前时刻的数据,导致前一年的数不完整。

使用参数 lastCleanLogTime 来记录上次清理的时间,并且与当前时间相差超过一天时才清理,能保证前一天的日志是完整的。

不明白为什么清理日志时,不是一次性删除全部的过期日志,而是每次删除 1000条。按理说,这些旧的日志数据应该已经不在 buffer pool 中了,trigger_time 字段又是普通索引,那么 DELETE 操作会先更新到 change buffer 中,之后再合并。现在先查询再删除,相当于多了一次 IO 且没有使用到 change buffer。

admin 服务是用来管理和调度 job 的,用户也可以在它的管理后台新建一个 job,配置 CRON 和 JobHandler,然后 admin 服务就会按照配置的参数来调度 job。很显然,这种“自动化工作”也是由线程定时执行的。

1、如果使用线程调度 Job,存在的第一个问题是:如果某个 Job 在调度时比较耗时,就可能阻塞后续的 Job,导致后续 job 的执行有延迟,怎么解决这个问题?

在前面 JobTriggerPoolHelper 我们已经知道,admin 在调度 job 时是 ”使用线程池、线程“ 异步执行调度任务,避免了主线程的阻塞。

2、使用线程定时调度 job,存在的第二个问题是:怎么保证 job 在指定的时间执行,而不会出现大量延迟?

admin 使用 ”预读“ 的方式,提前读取在未来一段时间内要执行的 job,提前取到内存中,并使用 “时间轮算法” 按时间分组 job,把未来要执行的 job 下一个时间段执行。

3、还隐藏第三个问题:admin 服务是可以多实例部署的,在这种情况下该怎么避免一个 job 被多个实例重复调度?

admin 把一张数据表作为 “分布式锁” 来保证只有一个 admin 实例能执行 job 调度,又通过随机 sleep 线程一段时间,来降低线程之间的竞争。

下面我们就通过代码来了解 xxl-job 是怎么解决上述问题的。

在该类中,定义了一个调度线程,用来调度要执行的 job 和已经过期一段时间的 job,定义代码如下:

该线程会预读出 “下次执行时间 <= now + 5000 毫秒内” 的部分 job,根据它们下一次执行时间划分成三段,执行三种不同的逻辑。

1、下次执行时间在 (- , now - 5000) 范围内

说明过期时间已经大于 5000 毫秒,这时如果过期策略要求调度,就调度一次。代码如下:

2、下次执行时间在 [now - 5000, now) 范围内

说明过期时间小于5000毫秒,只能算是延迟不能算是过期,直接调度一次,代码如下:

如果 job 的下一次执行时间在 5000 毫秒以内,为了省下下次预读的 IO 耗时,这里会记录下 job id,等待后面的调度。

3、下次执行时间在 [now, now + 5000) 范围内

说明还没到执行时间,先记录下 job id, 等待后面的调度 ,代码如下:

上面的3个步骤结束后,会更新 jobInfo 的 trigger_last_time、trigger_next_time、trigger_status 字段:

可以看到,通过预读,一方面会把过期一小段时间的 job 执行一遍,另一方面会把未来一小段时间内要执行的 job 取出,保存进一个 map 对象 ringData 中,等待另一个线程调度。这样就避免了某些 job 到了时间还没执行。

因为 admin 是可以多实例部署的,所以在调度 job 时,需要考虑怎么避免 job 被多次调度。

xxl-job 在前面 JobFailMonitorHelper 中遍历失败的 job 时,会对每个 job 设置一个无效的状态作为 ”分布式行锁“,如果设置失败就跳过。而在这里,如果还使用该方法,有可能出现,一个 job 被设置为无效状态后,线程就崩溃了,导致该 job 永远无法被调度。因此,要尽量避免对 job 状态的修改。

在这里,admin 服务使用一张表 xxl_job_lock 作为分布式锁,每个 admin 实例都要先尝试获取该表的锁,获取成功才能继续执行;同时,为了降低不同实例之间的竞争,会在线程开始执勤随机 sleep 一段时间。

如何获取分布式锁?

在线程中会开启一个事务,设置为手动提交,然后对表 xxl_job_lock 执行 FOR UPDATE 查询。如果该线程执行语句成功,其他实例的线程就会排队等待该表的锁,实现了分布式锁功能。代码如下:

怎么降低锁的竞争?

为了降低锁竞争,在线程开始前会先 sleep 4000 5000 毫秒的随机值(不能大于 5000 毫秒,5000 毫秒是预读的时间范围);在线程结束当前循环时,会根据耗时和是否有预读数据,选择不同的 sleep 策略:

代码如下:

在前面的线程中,对即将要开始的 job,不是立刻调度,而是按照执行的时刻(秒),把 job id 保存进一个 map 中,然后由 ringThread 线程按时刻进行调度,这只典型的“时间轮算法”。代码如下:

每次轮询调度时,只取出当前时刻(秒)、前一秒内的 job,不会去调度与现在相隔太久的 job。

在执行轮询调度前,有一个时间在 0 1000 毫秒范围内的 sleep。如果没有这个 sleep,该线程会一直执行,而 ringData 中当前时刻(秒)的数据可能已经为空,会导致大量无效的操作;增加了这个 sleep 之后,可以避免这种无效的操作。之所以 sleep 时间在 1000 毫秒以内,是因为调度时刻最小精确到秒,一秒的 sleep 可以避免 job 的延迟。

因为在前面的 scheleThread 线程中,最后一个操作是把 job 的 next_trigger_time 值更新为大于 now + 5000 毫秒,其他 admin 实例 scheleThread 线程的查询条件是:next_trigger_time < now + 5000,不会查询出这里调度的 job,所以不需要加分布式锁。

至此,XxlJobScheler-init 方法的作用我们介绍完毕,下面我们简单介绍一下 XxlJobScheler-destroy 方法

destroy 方法很简单,就是销毁前面初始化的线程池和线程,它销毁的顺序与前面启动的顺序相反。

代码如下:

因为各个 toStop 方法都很相似,所以我们只介绍 JobScheleHelper 的 toStop 方法。

该方法的步骤如下:

1、设置停止标志位为 true;

2、sleep 一段时间,让出 CPU 时间片给线程执行任务;

3、如果线程不是终止状态(线程正在 sleep),中断它;

4、线程执行 join 方法,直到线程结束,执行最后一次。

代码如下:

至此,JobScheleHelper 的主要功能就介绍完了,可以看出, admin 服务在启动时,启动了多个线程池和线程,异步执行任务和异步响应 executor 的请求。

下面,我们介绍前面涉及到的 XxlJobTrigger 和 XxlJobCompleter。

XxlJobTrigger 是调度 job 时的封装类,它主要工作就是接受传入的 jobId、调度参数等,查询对应的 jobGroup、jobInfo,然后调用 ExecutorBiz 对象来执行调度(run 方法)。

该类中三个核心方法及其调用关系如下: trigger -> processTrigger -> runExecutor ,

该方法的功能比较简单,就是根据传入的参数查询 jobGroup 和 jobInfo 对象,设置相关的字段值,然后调用 processTrigger 方法。

该方法的主要工作分为以下几步:

1、保存一条调度日志;

2、从 jobInfo、jobGroup 中取出字段值,构造 TriggerParam 对象;

3、根据 jobInfo 的路由策略,从 jobGroup 中取出要调度的 executor 地址;

4、调用 runExecutor 方法执行调度;

5、保存调度参数、设置调度信息、更新日志。

这里不会修改 jobInfo、jobGroup 对象的字段值,只取出字段值来使用,对这两个对象字段的修改,是在前一步 trigger 方法中进行的。

该方法会执行调度,并返回调度结果,它的核心代码如下:

这里使用 XxlJobScheler 类取出 ExecutorBiz 对象,以 “懒加载” 的方式给每个 address 创建一个 ExecutorBiz 对象,代码如下:

可以看出,该类中的三个方法其实可以归类为:pre -> execute -> post,在执行前、执行时、执行后做一些前置和收尾工作。

该类在前面 JobCompleteHelper 中被使用,最终 job 的完成就是在该类中执行的,该类有两个主要方法:

下面主要介绍 finishJob 方法。

finishJob 的主要功能是:如果当前任务执行成功了,就调度它的所有子任务,最后把子任务的调度消息添加到当前 job 的日志中。代码如下:

需要注意的是:

1、这里依赖于 JobTriggerPoolHelper 来调度 job,所以在 JobCompleteHelper 的监视线程开始时,有一个 50 秒的等待,就是等待 JobTriggerPoolHelper 启动完成;

2、在 finishJob 方法中,调度子任务的时候,默认子任务的调度结果是成功,注意,这里是指 “调度” 这个行为是成功的,而不是指子任务执行是成功的。

1、XxlJobAdminConfig 作为 admin 服务的启动入口,要尽可能保持简洁,作用类似于一个仓库,来管理和持有所有的类和对象,并不会去启动具体的线程,它只需要“按下启动器的按钮”就可以了;

2、XxlJobScheler 是 admin 服务的启动器类,它会调用各个辅助类(xxxHelper)来启动对应的线程;

3、对外的接口,比如调度 job、接收注册或下线等,都是使用线程池 + 线程 的异步方式实现,避免 job 对主线程的阻塞;

4、对“自动任务“类的功能,都是使用线程定时执行;

XXL-JOB分布式调度框架全面详解: https://juejin.cn/post/6948397386926391333

时间轮算法:https://spongecaptain.cool/post/widget/timingwheel

一个开源的时间轮算法介绍:https://spongecaptain.cool/post/widget/timingwheel2

热点内容
pm编程卡 发布:2024-11-28 02:39:50 浏览:563
convertsql 发布:2024-11-28 02:39:50 浏览:386
phpwap源码 发布:2024-11-28 02:36:46 浏览:586
狂牛加密视频破解 发布:2024-11-28 02:32:54 浏览:867
腾讯视频上传技巧 发布:2024-11-28 02:28:05 浏览:238
2016数据库系统工程师 发布:2024-11-28 02:22:16 浏览:891
压缩机飞动 发布:2024-11-28 02:00:04 浏览:273
50年脚本 发布:2024-11-28 01:58:38 浏览:220
光遇渠道服初音脚本怎么换号 发布:2024-11-28 01:57:44 浏览:416
平板电脑配置怎么看配置高不高 发布:2024-11-28 01:57:43 浏览:613