Kafka+PowerJob实现延时消息、定时消息,动态控制消息发送时间

前言:因为公司需要一个kafka延时消息的组件服务,看了下市面上的实现kafka延时消息的实现,感觉都比较复杂难理解,自己就去研究了下使用其他中间件进行解决,于是有了这篇分享文章

实现技术:SpringBoot+kafka+powerjob(最新的分布式任务调度产品)

思路:powerjob是一款非常不错的java分布式任务调度产品,配合这个产品来定时调度我们的kafka的producer从而实现了延迟消息、定时消息

本项目之后的扩展:

1、甚至能在可视化界面动态控制消息的发送时间

2、可在消息发送后动态指定接下来业务的走向(例如发送消息后需要干某件些其他事情,也可以在这里动态控制每个事情的执行顺序)

前提:kafka环境已经装好

大家可以去官方了解下 地址:PowerJob

项目我自己改了官方的后 也开源了:https://gitee.com/yangjial/powerjob.git

整体结构:

 

第一步:启动调度任务服务 

1.1修改配置文件、

依赖一个MySql5.7以上的数据库,在application-daily.properties配置中改(根据自己的配置文件)随便创建一个新库即可

1.2启动 PowerJobServerApplication

启动成功后访问http://localhost:7700/  会出现如下界面

 1.3 创建执行应用(创建执行者)

应用名称:一般用项目名称

密码:自定义就好

 创建好之后登录进入

以上powerjob服务就启动成功了

第二步:启动任务执行者(kafka服务)

2.1 结构

 2.2 先看配置文件application.yml

kafka的配置我就不讲了,大家根据自己要求配置

主要看powerjob的配置 ,

1、工作端口默认2777

2、接入的应用名称和密码 就是第一步我们在界面上面注册的应用名称 一定要相同

3、调度服务的地址就是刚刚第一步启动服务的地址

2.3 看PowerJobUtil工具类

这里引入了powerjob的openApi  文档: OpenAPI · 语雀

saveJob方法:创建任务或者更改任务,每个任务都有ID如果传入ID就是修改任务,ID为空就是创建任务,具体参数可以看注释

重要参数说明:

1、StartTime:延时最重要的是时间参数,powerjob支持cron表达式,将要延时的时间转换成cron格式后调用API即可创建定时任务

2、params:这是任务参数,也就是业务参数,在本章节就是消息内容

3、processorInfo:执行者全类名,指定执行者类(本项目中也就是KafkaProducer的类路径)

4、jobId:任务id,如果为空就是创建任务

package com.kafka.cloud.util;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.powerjob.client.PowerJobClient;
import tech.powerjob.common.enums.ExecuteType;
import tech.powerjob.common.enums.ProcessorType;
import tech.powerjob.common.enums.TimeExpressionType;
import tech.powerjob.common.request.http.SaveJobInfoRequest;
import tech.powerjob.common.response.ResultDTO;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * powerJob的api封装成一个工具类,供外部通过调用生产任务
 * @author YangBoos
 * @date 2021-11-22
 */
@Configuration
public class PowerJobUtil {

    protected final Logger logger = LoggerFactory.getLogger(PowerJobUtil.class);

    @Autowired
    private PowerJobClient ohMyClient;

    @Bean
    public PowerJobUtil getPowerJobUtil() {
        return new PowerJobUtil();
    }
    
    /**
     * 创建一个单核任务
     *
     * @param StartTime     任务开始时间
     * @param params        任务参数
     * @param processorInfo 回调得类全类名
     * @param jobId         任务id:如果为空就是创建任务
     * @return 返回结果
     * @throws Exception
     */
    public ResultDTO<Long> saveJob(Date StartTime, String params, String processorInfo, String jobId, String jobName, String jobDescription) throws Exception {
        logger.info("saveJob .......{},{},{},{}", StartTime, params, processorInfo, jobId);

        SaveJobInfoRequest request = new SaveJobInfoRequest();
        if (StringUtils.isNotEmpty(jobId)) {
            request.setId(Long.valueOf(jobId));
        }
        //任务名称
        request.setJobName(jobName);
        //任务描述
        request.setJobDescription(jobDescription);
        //任务参数,Processor#process方法入参TaskContext对象的jobParams字段
        request.setJobParams(params);
        //时间表达式类型,枚举值
        request.setTimeExpressionType(TimeExpressionType.CRON);
        //时间表达式,填写类型由timeExpressionType决定,比如CRON需要填写CRON表达式
        request.setTimeExpression(getCron(StartTime));
        //执行类型,枚举值
        request.setExecuteType(ExecuteType.STANDALONE);
        //处理器类型,枚举值
        request.setProcessorType(ProcessorType.BUILT_IN);
        //处理器参数,填写类型由processorType决定,如Java处理器需要填写全限定类名,如:com.github.kfcfans.oms.processors.demo.MapReduceProcessorDemo
        request.setProcessorInfo(processorInfo);
        //最大实例数,该任务同时执行的数量(任务和实例就像是类和对象的关系,任务被调度执行后被称为实例)
        request.setMaxInstanceNum(1);
        //单机线程并发数,表示该实例执行过程中每个Worker使用的线程数量
        request.setConcurrency(1);
        //任务实例运行时间限制,0代表无任何限制,超时会被打断并判定为执行失败
        request.setInstanceTimeLimit(0L);
        //任务实例重试次数,整个任务失败时重试,代价大,不推荐使用
        request.setMaxInstanceNum(0);
        //Task重试次数,每个子Task失败后单独重试,代价小,推荐使用
        request.setTaskRetryNum(2);
        //最小可用CPU核心数,CPU可用核心数小于该值的Worker将不会执行该任务,0代表无任何限制
        request.setMinCpuCores(0);
        //最小内存大小(GB),可用内存小于该值的Worker将不会执行该任务,0代表无任何限制
        request.setMinMemorySpace(0);
        //最小磁盘大小(GB),可用磁盘空间小于该值的Worker将不会执行该任务,0代表无任何限制
        request.setMinDiskSpace(0);
        //指定机器执行,设置该参数后只有列表中的机器允许执行该任务,空代表不指定机器
        request.setDesignatedWorkers(null);
        //最大执行机器数量,限定调动执行的机器数量,0代表无限制
        request.setMaxWorkerCount(1);
        //是否启用该任务,未启用的任务不会被调度
        request.setEnable(true);

        ResultDTO<Long> resultDTO = ohMyClient.saveJob(request);
        return resultDTO;
    }

    /**
     * 禁用某个任务
     *
     * @param jobId
     * @return
     * @throws Exception
     */
    public ResultDTO<Void> disableJob(Long jobId) {
        logger.info("disableJob .......{}", jobId);
        try {
            TimeUnit.MINUTES.sleep(5);
            return ohMyClient.disableJob(jobId);
        } catch (Exception e) {
            logger.error("disableJob  error.......{},{}", e, jobId);
        }
        return null;
    }

    /**
     * 删除某个任务
     *
     * @param jobId
     * @return
     * @throws Exception
     */
    public ResultDTO<Void> deleteJob(Long jobId) throws Exception {
        return ohMyClient.deleteJob(jobId);
    }

    /**
     * 通过输入指定日期时间生成cron表达式
     *
     * @param date
     * @return cron表达式
     */
    public String getCron(Date date) {
        SimpleDateFormat timeSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("消息产生时间:"+timeSdf.format(new Date()));
        System.out.println("预计消费时间:"+timeSdf.format(date));
        String dateFormat = "ss mm HH dd MM ? yyyy-yyyy";
        SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
        String formatTimeStr = null;
        if (date != null) {
            formatTimeStr = sdf.format(date);
        }
        System.out.println("任务时间得CRON:" + formatTimeStr);
        return formatTimeStr;
    }
}

2.4 看具体执行者类KafkaProducer(kafka生产类)

这里是具体执行业务的地方,当定时到点就会执行process方法,其中参数context可获取所有任务体内容 包括之前提到的参数也就是消息,在这个方法中发送kafka消息即可

package com.kafka.cloud.producer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import tech.powerjob.worker.core.processor.ProcessResult;
import tech.powerjob.worker.core.processor.TaskContext;
import tech.powerjob.worker.core.processor.sdk.BasicProcessor;

/**
 *  kafka的生产,也是调度任务的消费者
 * @author YangBoos
 * @date 2021-11-22
 */
@Component
public class KafkaProducer  implements BasicProcessor {


    private final static String TOPIC_NAME = "test01"; //topic的名称

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 任务执行者,具体业务执行方法,也就是我们发送消息的方法
     * @param context 任务体 其中包括非常多的属性,可看官方文档解释
     * @return
     */
    @Override
    public ProcessResult process(TaskContext context) {
        String params = context.getJobParams();
        //发送一个简单的消息
        kafkaTemplate.send(TOPIC_NAME  , params);
        return new ProcessResult(true, context.getJobId() + " process successfully.");
    }

}

2.5 看消费者 KafkaConsumer类

这里就是普通的消费 ,将时间消费消息时间打印

package com.kafka.cloud.consumer;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * kafka消费者
 * @author YangBoos
 * @date 2021-11-22
 */
@Component
public class KafkaConsumer {

    //kafka的监听器,topic为"zhTest",消费者组为"zhTestGroup"
    @KafkaListener(topics = "test01", groupId = "zhTestGroup")
    public void listenZhugeGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        SimpleDateFormat timeSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("消息实际消费时间:"+timeSdf.format(new Date()));
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //手动提交offset
        ack.acknowledge();
    }
}

2.6 看接口调用类 调用任务生成 KafkaController

通过调用找个接口 生成一个1分钟后执行的任务,此任务的执行内容就是发送一个kafka消息

参数:

1、指定任务执行时间

2、执行者具体类路径

3、任务ID  为null表示创建任务,如果修改任务就些具体任务ID,可以动态改变任务执行时间等各种功能

4、任务标题

5、任务说明

package com.kafka.cloud.controller;

import com.kafka.cloud.util.PowerJobUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.powerjob.common.response.ResultDTO;

import java.util.Calendar;
import java.util.Date;


/**
 * 测试接口
 * @author YangBoos
 * @date 2021-11-22
 */
@RestController
public class KafkaController {
    /**
     * 调度任务
     */
    @Autowired
    private PowerJobUtil powerJobUtil;


    @RequestMapping(value = "/send")
    public String send() {
        //获取1分钟后时间(一分钟后才进行消息发送)
        Calendar beforeTime = Calendar.getInstance();
        beforeTime.add(Calendar.MINUTE, +1);
        Date beforeDate = beforeTime.getTime();
        ResultDTO<Long> resultDTO = null;
        try {
            //生成任务
            resultDTO = powerJobUtil.saveJob(beforeDate, "这是一个延迟1分钟的消息",
                    "com.kafka.cloud.producer.KafkaProducer",
                    null,"kafka定时任务",
                    "kafka消息定时-开始任务");
            if (resultDTO.isSuccess()) {
                return "创建任务成功";
            } else {
                return "创建任务失败,请联系管理员";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "创建任务异常:"+e.getMessage();
        }
    }
}

2.7 启动执行者服务(kafka服务)PowerjobKafkaApplication

2.8 测试调用

启动成功后,调用 http://localhost:8080/send 可以看见创建任务成功

 2.9 重新回到第一步的可视化界面刷新

首页可以看见具体连入的应用,可以看见任务也多了一个

看到左侧的任务,可以看见我们刚刚接口创建的任务和具体任务执行时间

 

点击编辑也可以 查看所有详情内容

甚至可以动态去修改这个任务的所有参数,达到动态改变消息的发送时间

, 

也可以查看这个任务的执行记录 日志,操作重试等等

 

2.10 看到控制台

 可以看到任务已经创建 并且打印了一分钟后的时间

 等待一分钟后 ,任务准时执行 并发送消息 实时消费消息

 到此 kafka延时消息就完成了

任务如果已经过了时间可以在界面上看到状态已停止 

此项目可以扩展很多功能,消息发送后 建议调用API将任务删除,不用保留无用资源.

扩展:powerjob还可以可视化的指定工作流,可在消息发送后执行其他顺序任务

 

PowerJob是全新一代分布式调度与计算框架,支持CRON、API、固定频率、固定延迟等调度策略,提供工作流来编排任务解决依赖关系,使用简单,功能强大,文档齐全,能让您轻松完成作业的调度与繁杂任务分布式计算。 PowerJob特点: 使用简单:提供前端Web界面,允许开发者可视化地完成调度任务的管理(增、删、改、查)、任务运行状态监控和运行日志查看等功能。 定时策略完善:支持CRON表达式、固定频率、固定延迟和API四种定时调度策略。 执行模式丰富:支持单机、广播、Map、MapReduce四种执行模式,其中Map/MapReduce处理器能使开发者寥寥数行代码便获得集群分布式计算的能力。 DAG工作流支持:支持在线配置任务依赖关系,可视化得对任务进行编排,同时还支持上下游任务间的数据传递 执行器支持广泛:支持Spring Bean、内置/外置Java类、Shell、Python等处理器,应用范围广。 运维便捷:支持在线日志功能,执行器产生的日志可以在前端控制台页面实时显示,降低debug成本,极大地提高开发效率。 依赖精简:最小仅依赖关系型数据库(MySQL/Oracle/MS SQLServer...),扩展依赖为MongoDB(用于存储庞大的在线日志)。 高可用&高性能:调度服务器经过精心设计,一改其他调度框架基于数据库锁的策略,实现了无锁化调度。部署多个调度服务器可以同时实现高可用和性能的提升(支持无限的水平扩展)。 故障转移与恢复:任务执行失败后,可根据配置的重试策略完成重试,只要执行器集群有足够的计算节点,任务就能顺利完成。 PowerJob适用场景: 有定时执行需求的业务场景:如每天凌晨全量同步数据、生成业务报表等。 有需要全部机器一同执行的业务场景:如使用广播执行模式清理集群日志。 有需要分布式处理的业务场景:比如需要更新一大批数据,单机执行耗时非常长,可以使用Map/MapReduce处理器完成任务的分发,调动整个集群加速计算。 有需要延迟执行某些任务的业务场景:比如订单过期处理等。     PowerJob 更新日志: v4.0.1 Features 支持 PostgreSQL 强化前端控制台,新增 tag、上次在线时间等 worker 信息,便于排查无法连接的问题。 BugFix 修复 server 集群选主问题 修复当没有 worker 连接到 server 时出现的 NPE 问题 修复前端控制台错误显示 worker 列表的问题
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yang疯狂打码中

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值