中国风代码(1)——XXL-JOB

任务队列,延迟任务,这些往往是生产者消费者模型中不可缺少的一种模式

主流的开源消息消费框架,Kafka,RocketMQ里都有延迟消息的应用场景,RocketMQ开源版实现了18个等级的延迟队列,但是并没有任意延时时间的实现,Kafka是基于时间轮实现了延迟消息的功能。

除了消息队列框架,任务框架本身也是延迟任务的应用,开源的xxljob就是这么一个基于数据库的任务调度框架

至于为什么从xxljob上入手,还是从源码复杂度上而言,xxljob已经是最简单的时间轮实现了。

直接进入主题

1、时间轮的模型

在这里插入图片描述

private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();

基本结构:一张图,一行代码,Map的key是时间,val List<Integer>是任务链,Integer是任务的唯一ID

轮的体现:round轮次,如果当前在0位置,那么2S后的任务应该在第0轮次的2号槽位上,10S后的任务应该在第1轮次的2号槽位上,18S后的任务应该在第3轮次的2号槽位上,依次类推

1、抽象任务和时间轮模型

相信有了以上的讲解,你大致可以写几行代码至少,那么我们直接以xxljob里源码起步,看一个青春且完整的时间轮任务版本。

我们先讲述目标场景,我们希望以任意延迟,完成任务调度和执行功能,至于任务的内容,可以是分发消息,可以是业务代码无所谓,我们只关注任务调度就可以。 

那么我们先要抽象一个任务,我们不需要子任务和一些乱起八糟的东西,先从青春版开始:

XxlJobInfo:

public class XxlJobInfo {
	
	private int id;				// 主键ID
	
	private int jobGroup;		// 执行器主键ID
	private String jobDesc;
	
	private Date addTime;
	private Date updateTime;
	
	private String scheduleType;			// 调度类型
	private String scheduleConf;			// 调度配置,值含义取决于调度类型
	private String misfireStrategy;			// 调度过期策略

	private String executorRouteStrategy;	// 执行器路由策略
	private String executorHandler;		    // 执行器,任务Handler名称
	private String executorParam;		    // 执行器,任务参数
	private String executorBlockStrategy;	// 阻塞处理策略
	private int executorTimeout;     		// 任务执行超时时间,单位秒
	private int executorFailRetryCount;		// 失败重试次数
	
	private int triggerStatus;		// 调度状态:0-停止,1-运行
	private long triggerLastTime;	// 上次调度时间
	private long triggerNextTime;	// 下次调度时间
}

时间轮:

private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();

给时间轮添加任务:

    private void pushTimeRing(int ringSecond, int jobId){
        // push async ring
        // 获取槽位任务链
        List<Integer> ringItemData = ringData.get(ringSecond);
        if (ringItemData == null) {
            ringItemData = new ArrayList<Integer>();
            ringData.put(ringSecond, ringItemData);
        }
        ringItemData.add(jobId);
    }

那么任务存在哪呢,xxljob是基于数据库的,这里直接读了数据库拿到人物列表,我们掠过这部分代码,还是假设我们能不断拿到任务,注意此时我们处在从任务生产者上拿到任务的阶段里,我们接下来需要做的事情的思路是:执行要执行的任务,延迟的任务要放入时间轮

那么会有以下几个问题:

(1)那到底应该在什么时刻拿任务

(2)是么时刻的任务应该执行

(3)什么时刻的任务又应该放入时间轮

(4)时间轮里的任务又是什么时候被执行的呢?

解决以上四个问题,基本就能完成一个青春版了

当然我们也先把奇奇怪怪的异常情况提出来放到这,不影响主流程但是有助于思考:任务真的不会超时么,任务失败了怎么办?

2、提交任务

带着以上问题,我们先实现把数据库变为可执行的过程提交给任务线程池

继续,这里我总结一下xxl的做法:

先关注任务的几个参数:

	private long triggerLastTime;	// 上次调度时间
	private long triggerNextTime;	// 下次调度时间

(1)如果当前时间已经超过triggerNextTime 5S以上,说明我们任务已经过期,这里xxljob模仿了Quartz框架里miss任务的解决方式提供了两种策略:

do nothing 什么都不做
fire once now 马上开火,立刻补偿执行一次

默认fire once now,发现没有执行的超时任务立马执行一次,如果任务不止触发一次,就更新tiggerNextTime

(2)如果当前时间在triggerNextTime和triggerNextTime+5S以内,我都放入执行线程池立即执行

(3)如果当前时间在triggerNextTime之前,就根据下次任务执行时间,放入时间轮里

这就是整个Schedule线程管理时间轮的过程,Schedule这个概念和Yarn里Schedule的概念很像,就是专门负责调度

调度线程的线程方法源代码:

        scheduleThread = new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>> init xxl-job admin scheduler success.");

                // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

                while (!scheduleThreadToStop) {

                    // Scan Job
                    long start = System.currentTimeMillis();

                    Connection conn = null;
                    Boolean connAutoCommit = null;
                    PreparedStatement preparedStatement = null;

                    boolean preReadSuc = true;
                    try {

                            conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                            connAutoCommit = conn.getAutoCommit();
                            conn.setAutoCommit(false);

                            preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                            preparedStatement.execute();

                        // tx start

                        // 1、pre read
                        long nowTime = System.currentTimeMillis();
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList!=null && scheduleList.size()>0) {
                            // 2、push time-ring
                            for (XxlJobInfo jobInfo: scheduleList) {

                                // time-ring jump
                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                                    // 2.1、trigger-expire > 5s:pass && make next-trigger-time
                                    logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());

                                    // 1、misfire match
                                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                                        // FIRE_ONCE_NOW 》 trigger
                                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    }

                                    // 2、fresh next
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 1、trigger
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、fresh next
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、make ring second
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、push time ring
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            }

                            // 3、update trigger info
                            for (XxlJobInfo jobInfo: scheduleList) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                            }

                        } else {
                            preReadSuc = false;
                        }

                        // tx stop


                    } catch (Exception e) {
                        if (!scheduleThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {

                        // commit
                        if (conn != null) {
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }

                        // close PreparedStatement
                        if (null != preparedStatement) {
                            try {
                                preparedStatement.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                    }
                    long cost = System.currentTimeMillis()-start;


                    // Wait seconds, align second
                    if (cost < 1000) {  // scan-overtime, not wait
                        try {
                            // pre-read period: success > scan each second; fail > skip this period;
                            TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
                        } catch (InterruptedException e) {
                            if (!scheduleThreadToStop) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                }

                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
            }
        });
        scheduleThread.setDaemon(true);
        scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
        scheduleThread.start();

3、时间轮的执行

至此我们大致实现了怎么把生产者任务进行执行,但是时间轮好像和我们当前任务执行没有什么关系,时间轮到底运行了么?又是怎么运行的呢?

xxl的做法是维护了一个时间轮线程维护时间轮的运行

(1)如果当前时间不是整数秒,那么就sleep到整数秒(可能为了更准确的调度吧)

(2)睡完之后取当前秒和下一秒两个槽位上的任务列表,向工作队列提交任务

时间轮线程的代码:

        ringThread = new Thread(new Runnable() {
            @Override
            public void run() {

                while (!ringThreadToStop) {

                    // align second
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
                    } catch (InterruptedException e) {
                        if (!ringThreadToStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }

                    try {
                        // second data
                        List<Integer> ringItemData = new ArrayList<>();
                        //获取当前时间
                        int nowSecond = Calendar.getInstance().get(Calendar.SECOND);
                        // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                        for (int i = 0; i < 2; i++) {
                            //获取任务队列,从槽上拿当前秒和下一秒的两个任务队列
                            List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                            if (tmpData != null) {
                                ringItemData.addAll(tmpData);
                            }
                        }

                        // ring trigger
                        logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                        if (ringItemData.size() > 0) {
                            // do trigger
                            for (int jobId: ringItemData) {
                                // do trigger
                                JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                            }
                            // clear
                            ringItemData.clear();
                        }
                    } catch (Exception e) {
                        if (!ringThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        ringThread.start();
    }

回过头来看这几个问题:

(1)那到底应该在什么时刻拿任务

答:注意schedule源码里有代码:

	try {
		TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
	} catch (InterruptedException e) {
		if (!scheduleThreadToStop) {
			logger.error(e.getMessage(), e);
		}
	}

那么你应该知道他是每五秒去拿一次,那拿多少呢到底?

 int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

快慢线程池的快慢任务池最大值*20,默认(200+100)*20 = 6000,从当前任务开始到下个5S之内取6000个任务。你也许会说5S内执行6000个任务或许有点夸张,注意这里xxljob只负责了任务分发,调度和执行分开真正的执行器是注册到xxljob调度中心的各个执行器。

(2)什么时刻的任务应该执行

答:这里我们也可以看出,xxljob最好的使用方式是上线之后一段时间开始执行任务,比如晚上12点上线,第二天早上8点开始执行任务

(3)什么时刻的任务又应该放入时间轮

答:下一个5S内待执行的任务,一次取6000,部分执行,部分不执行

(4)时间轮里的任务又是什么时候被执行的呢?

时间轮线程单独负责任务分发,每秒做一次分发

4、xxljob和netty

其实延迟任务这块就已经说完了,往下的部分是xxljob维护各个执行器连接的部分,这块涉及netty的部分,可以拿出来看看

还是先明确需求场景:我们作为调度中心,要和执行器建立连接,确定我们能发往具体哪个执行器执行当前任务,这个场景基本就是典型的netty场景,你去看RocketMQ,Kafka,绝壁都长得一样,非常合理。

xxljob也一样,只要你理解NIO,使用netty应该不难,自定义可能你需要多做两次难度也不大,直接看看代码:

    public void start(final String address, final int port, final String appname, final String accessToken) {
        //伴随xxljob启动而启动
        executorBiz = new ExecutorBizImpl();
        thread = new Thread(new Runnable() {

            @Override
            public void run() {
                // 父子工作组
                EventLoopGroup bossGroup = new NioEventLoopGroup();
                EventLoopGroup workerGroup = new NioEventLoopGroup();
                ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
                        0,
                        200,
                        60L,
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<Runnable>(2000),
                        new ThreadFactory() {
                            @Override
                            public Thread newThread(Runnable r) {
                                return new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode());
                            }
                        },
                        new RejectedExecutionHandler() {
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
                            }
                        });


                try {
                    // start server
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    bootstrap.group(bossGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)
                            .childHandler(new ChannelInitializer<SocketChannel>() {
                                @Override
                                public void initChannel(SocketChannel channel) throws Exception {
                                    channel.pipeline()
                                            //开启心跳检测
                                            //第一个参数如果channelRead()方法超过readerIdleTime时间未被调用则会触发超时事件调用
                                            //第二个参数如果write()方法超过writerIdleTime时间未被调用则会触发超时事件调用userEventTrigger()方法;
                                            .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                                            //http编解码
                                            .addLast(new HttpServerCodec())
                                            //因为HttpServerCodec只能获取uri中参数,所以需要加上HttpObjectAggregator,从而可以获取post请求参数
                                            .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                                            //继承自SimpleChannelInboundHandler入栈处理器,对url进行处理,然后触发事件,包括心跳,执行,杀死任务等等
                                            .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
                                }
                            })
                            //TCP长连接
                            .childOption(ChannelOption.SO_KEEPALIVE, true);

                    // bind
                    ChannelFuture future = bootstrap.bind(port).sync();

                    logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

                    // start registry
                    startRegistry(appname, address);

                    // wait util stop
                    future.channel().closeFuture().sync();

                } catch (InterruptedException e) {
                    if (e instanceof InterruptedException) {
                        logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
                    } else {
                        logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
                    }
                } finally {
                    // stop
                    try {
                        workerGroup.shutdownGracefully();
                        bossGroup.shutdownGracefully();
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }

            }

        });
        thread.setDaemon(true);	// daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
        thread.start();
    }

注册的过程就是聊天室模型,维护组的概念,这块可以下次单独再开一篇讲讲聊天室模型。

以上,往下准备开一个xxl的另一个框架,xxl-rpc,为什么不开dubbo还是因为dubbo太庞大了,其实基于netty的rpc是很好实现的,但是dubbo封装了太多东西

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值