06编写处理课程发布消息/任务的sdk并集成到内容管理模块

消息处理SDK

SDK工具包的特点

对于当前基于本地数据库+消息表()的分布式事务控制的方案在其他服务中如果存在分布式事务问题也可以采用

在每个服务中都实现一套针对消息表的新增/定时扫描/更新/删除操作是比较繁琐的,所以我们可以将消息处理相关的业务做成一个通用的东西

  • 通用的服务:服务可以提供独立的网络接口,比如项目中的文件系统服务,提供文件的分布式存储服务,缺点是需要要提供与其他微服务通信的网络接口且可能要连接数据库,开发成本高
  • 通用的代码组件SDK(降低成本):SDK工具包通常会提供API的方式供外部系统使用,比如fastjson、Apache commons工具包等

SDK工具包的特点

  • 任务逻辑:对于课程发布任务是要向redis、索引库等同步数据,但其它任务的执行逻辑是不同的,所以执行任务在sdk中不用实现任务逻辑,只需要提供一个抽象方法由具体的执行任务方去实现

  • 任务的幂等性:一个任务只需要被处理一次,即使重复执行了相同的任务资源最终也只会处理一次,课程发布任务执行完成后会从消息表删除任务,如果消息表中任务的状态是完成或不存在则不用执行

  • 设计任务阶段状态(根据需求设置阶段数):如果一个任务有好几个小任务也不应该去重复执行这些小任务

    • 比如课程发布任务完成需要执行三个同步操作,将发布课程信息存储到Redis、将发布课程信息存储到索引库,将发布课程页面存储到文件系统
  • 任务不重复: 任务调度采用分片广播,根据分片参数去获取任务,配置调度过期策略为忽略,阻塞调度策略为丢弃后续调度,这里不再设置乐观锁保证任务绝对的不重复执行,因为对于信息同步类任务(低消耗),即使任务重复执行也没有关系

数据模型

xc_project项目工程根目录下创建xuecheng-plus-message-sdkSDK工程,在内容管理数据库创建消息表和消息历史表

java 复制代码
@Data
@ToString
@TableName("mq_message")
public class MqMessage implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 消息id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 消息类型代码: course_publish ,  media_test,
     */
    private String messageType;

    /**
     * 关联业务信息
     */
    private String businessKey1;

    /**
     * 关联业务信息
     */
    private String businessKey2;

    /**
     * 关联业务信息
     */
    private String businessKey3;

    /**
     * 执行次数
     */
    private Integer executeNum;

    /**
     * 处理状态,0:初始,1:成功
     */
    private String state;

    /**
     * 回复失败时间
     */
    private LocalDateTime returnfailureDate;

    /**
     * 回复成功时间
     */
    private LocalDateTime returnsuccessDate;

    /**
     * 回复失败内容
     */
    private String returnfailureMsg;

    /**
     * 最近执行时间
     */
    private LocalDateTime executeDate;

    /**
     * 阶段1处理状态, 0:初始,1:成功
     */
    private String stageState1;

    /**
     * 阶段2处理状态, 0:初始,1:成功
     */
    private String stageState2;

    /**
     * 阶段3处理状态, 0:初始,1:成功
     */
    private String stageState3;

    /**
     * 阶段4处理状态, 0:初始,1:成功
     */
    private String stageState4;
}

消息表的增删改查

MqMessageService接口中定义新增/定时扫描/任务完成后更新任务结果/删除/查询阶段任务执行状态/更新阶段任务执行结果的功能

java 复制代码
public interface MqMessageService extends IService<MqMessage> {
    /**
     * @description 扫描消息表记录,采用与扫描视频处理表相同的思路
     * @param shardIndex 分片序号
     * @param shardTotal 分片总数
     * @param messageType  消息类型
     * @param count 扫描记录数
     * @return java.util.List 消息记录
     */
    public List<MqMessage> getMessageList(int shardIndex, int shardTotal, String messageType,int count);
    
     /**
     * 添加任务消息
     * @param businessKey1 业务id,任务处理时关联的业务字段,如处理课程信息就需要指定要处理的课程Id
     * @param businessKey2 业务id
     * @param businessKey3 业务id
     * @return com.xuecheng.messagesdk.model.po.MqMessage 消息内容
     */
    MqMessage addMessage(String messageType, String businessKey1, String businessKey2, String businessKey3);
    
    /**
     * @description 更新任务的执行结果
     * @param id 消息id
     * @return int 更新成功:1
     */
    public int completed(long id);

    /**
     * @description 更新阶段任务的执行结果
     * @param id 消息id
     * @return int 更新成功:1
     */
    public int completedStageOne(long id);
    public int completedStageTwo(long id);
    public int completedStageThree(long id);
    public int completedStageFour(long id);

    /**
     * @description 查询阶段任务的处理状态
    */
    public int getStageOne(long id);
    public int getStageTwo(long id);
    public int getStageThree(long id);
    public int getStageFour(long id);
}

定义实现类MqMessageServiceImpl实现消息处理的业务

java 复制代码
@Slf4j
@Service
public class MqMessageServiceImpl extends ServiceImpl<MqMessageMapper, MqMessage> implements MqMessageService {
    @Autowired
    MqMessageMapper mqMessageMapper;

    @Autowired
    MqMessageHistoryMapper mqMessageHistoryMapper;
    
    @Override
    public List<MqMessage> getMessageList(int shardIndex, int shardTotal, String messageType, int count) {
        return mqMessageMapper.selectListByShardIndex(shardTotal, shardIndex, messageType, count);
    }
}

新增任务

java 复制代码
@Override
public MqMessage addMessage(String messageType, String businessKey1, String businessKey2, String businessKey3) {
    MqMessage mqMessage = new MqMessage();
    mqMessage.setMessageType(messageType);
    mqMessage.setBusinessKey1(businessKey1);
    mqMessage.setBusinessKey2(businessKey2);
    mqMessage.setBusinessKey3(businessKey3);
    int insert = mqMessageMapper.insert(mqMessage);
    if (insert > 0) {
        return mqMessage;
    } else {
        return null;
    }
}

更新大任务结果

任务执行成功后我们需要根据任务Id更新任务的处理状态,然后删除执行成功的任务(提高下次查询任务速度),最后将其添加到任务历史表

java 复制代码
@Transactional
@Override
public int completed(long id) {
    MqMessage mqMessage = new MqMessage();
	// 更新发布任务的处理状态,初始默认为0,1表示成功
    mqMessage.setState("1");
    // 根据where条件进行更新,实体类存放修改的条件
    int update = mqMessageMapper.update(mqMessage, new LambdaQueryWrapper<MqMessage>().eq(MqMessage::getId, id));
    if (update > 0) {
        mqMessage = mqMessageMapper.selectById(id);
        // 任务执行成功后删除任务,将任务添加到任务历史表,便于下次查询任务
        MqMessageHistory mqMessageHistory = new MqMessageHistory();
        BeanUtils.copyProperties(mqMessage, mqMessageHistory);
        mqMessageHistoryMapper.insert(mqMessageHistory);
        mqMessageMapper.deleteById(id);
        return 1;
    }
    return 0;
}

更新阶段任务结果

阶段任务执行成功后我们需要根据任务Id更新任务中对应的阶段任务的处理状态

java 复制代码
@Override
public int completedStageOne(long id) {
    MqMessage mqMessage = new MqMessage();
    // 更新阶段1任务的处理状态,初始默认为0,1表示成功
    mqMessage.setStageState1("1");
    // 根据where条件进行更新
    return mqMessageMapper.update(mqMessage, new LambdaQueryWrapper<MqMessage>().eq(MqMessage::getId, id));
}
@Override
public int completedStageTwo(long id) {
    MqMessage mqMessage = new MqMessage();
    // 更新阶段2任务的处理状态,初始默认为0,1表示成功
    mqMessage.setStageState2("1");
    return mqMessageMapper.update(mqMessage, new LambdaQueryWrapper<MqMessage>().eq(MqMessage::getId, id));
}

@Override
public int completedStageThree(long id) {
    MqMessage mqMessage = new MqMessage();
    // 更新阶段3任务的处理状态,初始默认为0,1表示成功
    mqMessage.setStageState3("1");
    return mqMessageMapper.update(mqMessage, new LambdaQueryWrapper<MqMessage>().eq(MqMessage::getId, id));
}

@Override
public int completedStageFour(long id) {
    MqMessage mqMessage = new MqMessage();
    // 更新阶段4任务的处理状态,初始默认为0,1表示成功
    mqMessage.setStageState4("1");
    return mqMessageMapper.update(mqMessage, new LambdaQueryWrapper<MqMessage>().eq(MqMessage::getId, id));
}

查询阶段任务的处理状态

根据任务Id查询任务中对应的阶段任务的处理状态

java 复制代码
@Override
public int getStageOne(long id) {
    // 查询阶段1状态
    return Integer.parseInt(mqMessageMapper.selectById(id).getStageState1());
}

@Override
public int getStageTwo(long id) {
    // 查询阶段2状态
    return Integer.parseInt(mqMessageMapper.selectById(id).getStageState2());
}

@Override
public int getStageThree(long id) {
    // 查询阶段3状态
    return Integer.parseInt(mqMessageMapper.selectById(id).getStageState3());
}

@Override
public int getStageFour(long id) {
    // 查询阶段4状态
    return Integer.parseInt(mqMessageMapper.selectById(id).getStageState4());
}

利用多线程执行任务内容

消息SDK提供消息处理的抽象类,此抽象类供使用方去继承使用,该抽象类已经提供了扫描消息表和执行任务的逻辑,但是任务具体的处理内容需要我们自己实现

java 复制代码
@Slf4j
@Data
public abstract class MessageProcessAbstract {
    @Autowired
    MqMessageService mqMessageService;
    /**
     * @param mqMessage 执行任务内容
     * @return boolean true:处理成功,false处理失败
     * @description 任务处理
     */
    public abstract boolean execute(MqMessage mqMessage);
    
    /**
     * @description 扫描消息表多线程执行任务
     * @param shardIndex 分片序号
     * @param shardTotal 分片总数
     * @param messageType  消息类型
     * @param count  一次取出任务总数
     * @param timeout 预估任务执行时间,到此时间如果任务还没有结束则强制结束 单位秒
    */
    public void process(int shardIndex, int shardTotal, String messageType,int count,long timeout) {

        try {
            // 扫描消息表获取任务清单
            List<MqMessage> messageList = mqMessageService.getMessageList(shardIndex, shardTotal,messageType, count);
            // 任务个数
            int size = messageList.size();
            log.debug("取出待处理消息"+size+"条");
            if(size<=0){
                return ;
            }

            // 创建线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(size);
            // 计数器
            CountDownLatch countDownLatch = new CountDownLatch(size);
            messageList.forEach(message -> {
                threadPool.execute(() -> {
                    log.debug("开始任务:{}",message);
                    // 处理任务
                    try {
                        // 任务具体的执行内容
                        boolean result = execute(message);
                        if(result){
                            log.debug("任务执行成功:{})",message);
                            // 更新任务状态,删除消息表记录,添加到历史表
                            int completed = mqMessageService.completed(message.getId());
                            if (completed>0){
                                log.debug("任务执行成功:{}",message);
                            }else{
                                log.debug("任务执行失败:{}",message);
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        log.debug("任务出现异常:{},任务:{}",e.getMessage(),message);
                    }
                    // 计数
                    countDownLatch.countDown();
                    log.debug("结束任务:{}",message);

                });
            });

            // 等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
            countDownLatch.await(timeout,TimeUnit.SECONDS);
            System.out.println("结束....");
        } catch (InterruptedException e) {
           e.printStackTrace();
        }
    }
}

测试SDK工具包

第一步:在test目录下的bootstrap.yml文件中编写数据库的连接配置

yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xc_content?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: A10ne,tillde@th.

# 日志文件配置路径
logging:
  config: classpath:log4j2-dev.xml
  level:
    org.springframework.cloud.gateway: trace

第二步:MessageProcessClass继承MessageProcessAbstract抽象类的execute方法,编写任务具体的处理内容*

java 复制代码
@Slf4j
@Component
public class MessageProcessClass extends MessageProcessAbstract {
    @Autowired
    MqMessageService mqMessageService;
    // 执行任务
    @Override
    public boolean execute(MqMessage mqMessage) {
        Long id = mqMessage.getId();
        log.debug("开始执行任务:{}",id);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 取出阶段状态
        int stageOne = mqMessageService.getStageOne(id);
        if(stageOne<1){
            log.debug("开始执行第一阶段任务");
            System.out.println();
            int i = mqMessageService.completedStageOne(id);
            if(i>0){
                log.debug("完成第一阶段任务");
            }

        }else{
            log.debug("无需执行第一阶段任务");
        }

        return true;
    }
}

在消息表添加消息类型为test的消息,执行MessageProcessClassTest类中的test()方法执行任务

java 复制代码
@SpringBootTest
public class MessageProcessClassTest {
    @Autowired
    MessageProcessClass messageProcessClass;

    @Test
    public void test() {
        System.out.println("开始执行-----》" + LocalDateTime.now());
        messageProcessClass.process(0, 1, "test", 5, 30);
        System.out.println("结束执行-----》" + LocalDateTime.now());
        Thread.sleep(9000000);
    }
}

内容管理模块集成消息SDK

添加课程发布任务

第一步:在内容管理模块的service工程中添加xuecheng-plus-message-sdk依赖

xml 复制代码
<dependency>
    <groupId>com.xuecheng</groupId>
    <artifactId>xuecheng-plus-message-sdk</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

第二步:在数据库中手动修改提交课程的审核状态为审核通过,然后执行课程发布操作,使用本地事务控制添加/更新课程发布信息和添加消息表的操作同时成功

java 复制代码
@Transactional
@Override
public void publish(Long companyId, Long courseId) {
    // 获取课程预发布表数据
    CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
    // 提交课程审核通过方可发布
    if(coursePublishPre == null){
        XueChengPlusException.cast("请先提交课程审核,审核通过才可以发布");
    }
    //  预发布课程审核通过方可发布
    if(!"202004".equals(coursePublishPre.getStatus()){
        XueChengPlusException.cast("操作失败,课程审核通过方可发布");
    }
    // 本机构只允许提交本机构的课程
    if(!coursePublishPre.getCompanyId().equals(companyId)){
        XueChengPlusException.cast("不允许提交其它机构的课程");
    }   
    // 向课程发布表插入数据,如果存在则更新
    saveCoursePublish(courseId);

    // 向消息表插入数据即课程发布的任务
    saveCoursePublishMessage(courseId);

    // 课程发布后,可以删除课程预发布表对应的课程审核记录
    coursePublishPreMapper.deleteById(courseId);

}

第三步:使用SDk工具包中MqMessageService提供的addMessage方法实现向消息表中添加一条课程发布任务记录

java 复制代码
public class CoursePublishServiceImpl implements CoursePublishService {
    	@Autowired
        MqMessageService mqMessageService
        /**
  		* @description 保存消息表记录
  		* @param courseId  课程id
  		*/
        private void saveCoursePublishMessage(Long courseId){
        	// course_publish表示消息类型,任务处理时关联的业务字段,如处理课程信息就需要指定要处理的课程Id
        	MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
        	if(mqMessage==null){
            XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
        }
    }
}

课程发布任务调度入口

第一步:在内容管理模块的service工程中添加xxl-job的依赖`

xml 复制代码
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
</dependency>

第二步:在Nacos的dev环境下content-service-dev.yaml配置文件中配置执行器,同时在内容管理的service工程中创建XxlJobConfig配置类将执行器注册到容器中

yaml 复制代码
xxl:
  job:
    admin: 
      addresses: http://127.0.0.1:8088/xxl-job-admin
    executor:
      appname: coursepublish-job
      address: 
      ip: 
      port: 8999
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: 30
    accessToken: default_token

第三步:编写任务调度入口,方法执行顺序调度入口-->process-->execute-->添加课程静态化-->添加课程索引-->添加课程缓存

  • content-service工程的jobhandler中编写CoursePublishTask类,继承MessageProcessAbstract类,重写其中的execute方法指定任务的具体执行内容
java 复制代码
@Slf4j
@Component
public class CoursePublishTask extends MessageProcessAbstract {
    //任务调度入口
    @XxlJob("CoursePublishJobHandler")
    public void coursePublishJobHandler() throws Exception {
        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        log.debug("shardIndex="+shardIndex+",shardTotal="+shardTotal);
        //参数:分片序号、分片总数、消息类型、一次最多取到的任务数量、一次任务调度执行的超时时间
        process(shardIndex,shardTotal,"course_publish",30,60);
    }
    ....
}

课程发布后需要执行的任务内容

java 复制代码
@Override
public boolean execute(MqMessage mqMessage) {
    log.debug("开始执行课程发布任务,课程id:{}", mqMessage.getBusinessKey1());
    // 获取消息相关的业务信息即课程Id
    String businessKey1 = mqMessage.getBusinessKey1();
    long courseId = Integer.parseInt(businessKey1);
    // 课程静态化
    generateCourseHtml(mqMessage,courseId);
    // 课程索引
    saveCourseIndex(mqMessage,courseId);
    // 课程缓存
    saveCourseCache(mqMessage,courseId);
    return true;
}      

生成课程静态化页面并上传至文件系统

java 复制代码
public void generateCourseHtml(MqMessage mqMessage,long courseId){
    log.debug("开始进行课程静态化,课程id:{}",courseId);
    // 消息id
    Long id = mqMessage.getId();
    // 消息处理的service
    MqMessageService mqMessageService = this.getMqMessageService();
    // 消息幂等性处理,取出该阶段的执行状态
    int stageOne = mqMessageService.getStageOne(id);
    if(stageOne >0){
        log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
        return ;
    }
    try {
        TimeUnit.SECONDS.sleep(10);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    //保存第一阶段状态
    mqMessageService.completedStageOne(id);

}

将课程信息缓存至redis

java 复制代码
public void saveCourseCache(MqMessage mqMessage,long courseId){
    log.debug("将课程信息缓存至redis,课程id:{}",courseId);
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

保存课程索引信息

java 复制代码
public void saveCourseIndex(MqMessage mqMessage,long courseId){
    log.debug("保存课程索引信息,课程id:{}",courseId);
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

xxl-job调度中心

xxl-job-admin控制台中添加执行器

添加任务及其配置

测试

在消息表添加课程发布的消息,消息类型为course_publish,business_key1为发布课程的ID

1、测试是否可以正常调度执行。

2、测试任务幂等性

在 saveCourseCache(mqMessage,courseId);处打断点,待执行到这里观察数据库第一阶段完成的标记预期标记为1

结束进程,再重新启动,观察第一阶段的任务预期不再执行

3、任务执行完成删除消息表记录,插入历史表,state状态字段为1

相关推荐
尚学教辅学习资料3 分钟前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
雷神乐乐20 分钟前
File.separator与File.separatorChar的区别
java·路径分隔符
小刘|24 分钟前
《Java 实现希尔排序:原理剖析与代码详解》
java·算法·排序算法
逊嘘43 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
morris1311 小时前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员1 小时前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU1 小时前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie61 小时前
在IDEA中使用Git
java·git
Elaine2023912 小时前
06 网络编程基础
java·网络