RocketMQ应用-实现周期性自动任务

应用背景

提供配置功能,用于固定周期的执行某个动作;如在基金交易的每个交易日结束时,需要根据当天交易量计算基金的收益,可以提供定时任务,在每天晚上固定的时间计算收益数据。

功能设计

提供任务数据表task_info和任务执行记录表task_log_info;通过扫描task_info表中所有的任务配置数据,将任务发送的RocketMQ,当消费者消费任务后按照指定的周期时间向RocketMQ发送延时执行任务,同时在task_log_info记录当此任务的消费记录。

数据表

task_info

CREATE TABLE `task_info`  (
  `id` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `version` int NOT NULL,
  `create_time` datetime NOT NULL,
  `content` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '消息内容,可传递自定义参数',
  `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息标题',
  `topic` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息主题',
  `tag` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息tag',
  `execute_time` datetime NOT NULL COMMENT '待执行时间',
  `interval_time` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '周期间隔时间,单位:秒',
  `send_status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '发送状态 0-待发送 1-发送成功 2-发送失败',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `topic`(`topic`, `tag`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

task_log_info

CREATE TABLE `task_log_info`  (
  `id` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `version` int NOT NULL,
  `create_time` datetime NOT NULL,
  `key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '消息唯一键',
  `topic` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息主题',
  `tag` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息tag',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

功能逻辑

自动任务核心功能

  1. 在task_info表中插入自动任务功能的基本任务-任务表扫描,并将这个任务初始化发送到RocketMQ(仅需要初始化一次),设置间隔执行时间为5分钟。
  2. 提供“任务表扫描”的消费者,消费者查询任务表中当天和当天之前所有待发送到RocketMQ的任务,并根据配置的任务执行时间向RocketMQ发送定时消息
  3. “任务表扫描”消息每次消费结束时,需要向RocketMQ重新发送延时的“任务表扫描”消息,实现周期性执行的效果。

自动任务业务功能

在核心功能“任务表扫描”周期执行完成的基础上,按照如下方式添加业务类任务:

  1. 在task_info表中插入业务任务
  2. 一对一提供业务任务的消费监听代码
  3. 消费时按照校验重复消费、业务处理、消息重新延时发送到RocketMQ三个步骤进行

功能实现

核心功能部分

  1. 基本抽象类提供
    将任务的消费分成消费前幂等性校验(通过task_log_info判断消息是否已消费)、业务处理、消费后向RocketMQ重新发送延时消息三个部分
/**
 * 自动任务基本实现
 */
public abstract class ATaskAutoHandle implements RocketMQListener<MessageExt> {

    @Override
    public void onMessage(MessageExt messageExt) {
        //前置任务,避免重复消费
        if (!before(messageExt)) {
            return;
        }
        //处理业务
        handle(messageExt);
        //后置任务
        after(messageExt);
    }

    public boolean before(MessageExt messageExt) {
        //判断当前消息是否已经消费
        TaskLogInfoEntity entity = getTaskCenterService().queryTaskLogByKey(messageExt.getKeys());
        return entity == null;
    }

    abstract void handle(MessageExt messageExt);

    public void after(MessageExt msgExt) {
        //添加消息的消费记录
        getTaskCenterService().addTaskSendLog(new TaskLogInfoEntity(msgExt.getTopic(), msgExt.getTags(), msgExt.getKeys()));
        //重新发送消息
        // 获取间隔时间
        long intervalTime = Long.parseLong(msgExt.getProperty(TaskCenterConstants.INTERVAL_TIME_KEY));
        SendResult sendResult = getRocketMQTemplate().syncSendDelayTimeSeconds(String.format("%s:%s", msgExt.getTopic(), msgExt.getTags()), MessageBuilder
                .withPayload(msgExt.getBody())
                .setHeader(RocketMQHeaders.KEYS, msgExt.getTopic() + UUID.randomUUID())
                .setHeader(TaskCenterConstants.INTERVAL_TIME_KEY, intervalTime)
                .build(), intervalTime);
        if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)){
            throw new RuntimeException("消息重新发送失败,key: " + msgExt.getKeys());
        }
    }

    abstract RocketMQTemplate getRocketMQTemplate();

    abstract TaskCenterService getTaskCenterService();
}
  1. 提供“任务表扫描”消息的消费实现
    继承“基本抽象类”,实现业务部分-查询当日及当日之前未发送的任务、执行时间小于当前时间的任务立即发送到RocketMQ、执行时间晚于当前时间的任务发送定时消息到RocketMQ
@Component
@RocketMQMessageListener(
    topic = "taskAutoTopic",
    consumerGroup = "taskAutoConsumerGroup"
)
public class TaskAutoListener extends ATaskAutoHandle {

    private static final Logger logger = LoggerFactory.getLogger(TaskAutoListener.class);

    @Autowired
    private TaskCenterService taskCenterService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Override
    public void handle(MessageExt messageExt) {
        Date now = new Date();
        logger.info("待执行任务扫描开始:{}", DateFormatUtils.format(now, DateFormatUtils.ISO_DATETIME_FORMAT.getPattern()));
        //查询当天需要执行的所有任务
        List<TaskInfoDTO> taskInfoDTOS = taskCenterService.queryNeedSendTask();
        if (CollectionUtils.isEmpty(taskInfoDTOS)) {
            logger.info("待执行任务扫描结束");
            return;
        }
        //执行时间在当前时间之前的立即发送
        taskInfoDTOS.stream().filter(task -> task.getExecuteTime().before(now) || task.getExecuteTime().equals(now)).forEach(task -> {
            SendResult sendResult = rocketMQTemplate.syncSend(String.format("%s:%s", task.getTopic(), task.getTag()), MessageBuilder
                    .withPayload(task.getContent())
                    .setHeader(RocketMQHeaders.KEYS, task.getTopic() + UUID.randomUUID())
                    .setHeader(TaskCenterConstants.INTERVAL_TIME_KEY, task.getIntervalTime())
                    .build());
            task.setSendStatus(sendResult.getSendStatus().equals(SendStatus.SEND_OK) ? SendStatusEnum.SEND_SUCCESS.getCode() : SendStatusEnum.SEND_FAIL.getCode());
            //更新任务执行状态
            taskCenterService.modifyTaskSendStatus(task);
        });

        //执行时间在当前时间之后的定时发送
        taskInfoDTOS.stream().filter(task -> task.getExecuteTime().after(now)).forEach(task -> {
            SendResult sendResult = rocketMQTemplate.syncSendDeliverTimeMills(String.format("%s:%s", task.getTopic(), task.getTag()), MessageBuilder
                    .withPayload(task.getContent())
                    .setHeader(RocketMQHeaders.KEYS, task.getTopic() + UUID.randomUUID())
                    .setHeader(TaskCenterConstants.INTERVAL_TIME_KEY, task.getIntervalTime())
                    .build(), task.getExecuteTime().getTime());
            task.setSendStatus(sendResult.getSendStatus().equals(SendStatus.SEND_OK) ? SendStatusEnum.SEND_SUCCESS.getCode() : SendStatusEnum.SEND_FAIL.getCode());
            //更新任务执行状态
            taskCenterService.modifyTaskSendStatus(task);
        });

        logger.info("待执行任务扫描结束");
    }

    @Override
    public RocketMQTemplate getRocketMQTemplate() {
        return this.rocketMQTemplate;
    }

    @Override
    public TaskCenterService getTaskCenterService() {
        return this.taskCenterService;
    }
}

自定义业务任务

  1. 向task_info插入自定义业务任务配置
  2. 继承“基本抽象类”,实现业务处理部分即可
@Component
@RocketMQMessageListener(
    topic = "fundIncomeCalTopic",
    consumerGroup = "fundIncomeCalConsumerGroup"
)
public class FundIncomeCalTaskListener extends ATaskAutoHandle {

    private static final Logger logger = LoggerFactory.getLogger(FundIncomeCalTaskListener.class);

    @Autowired
    private TaskCenterService taskCenterService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Override
    public void handle(MessageExt messageExt) {
        Date now = new Date();
        logger.info("基金收益计算开始:{}", DateFormatUtils.format(now, DateFormatUtils.ISO_DATETIME_FORMAT.getPattern()));
        logger.info("基金收益计算结束");
    }

    @Override
    public RocketMQTemplate getRocketMQTemplate() {
        return rocketMQTemplate;
    }

    @Override
    public TaskCenterService getTaskCenterService() {
        return taskCenterService;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值