课程发布
数据模型
教学机构人员在课程审核通过后即可发布课程,课程发布后会公开展示在网站上供学生查看、选课和学习。
在网站上展示课程信息需要解决课程信息显示的性能问题,如果速度慢(排除网速)会影响用户的体验性。
如何去快速搜索课程?
打开课程详情页面仍然去查询数据库可行吗?
为了提高网站的速度需要将课程信息进行缓存,并且要将课程信息加入索引库方便搜索,下图显示了课程发布后课程信息的流转情况:
1、向内容管理数据库的课程发布表存储课程发布信息,更新课程基本信息表中发布状态为已发布。
2、向Redis存储课程缓存信息。
3、向Elasticsearch存储课程索引信息。
4、请求分布文件系统存储课程静态化页面(即html页面),实现快速浏览课程详情页面。
课程发布表的数据来源于课程预发布表,它们的结构基本一样,只是课程发布表中的状态是课程发布状态,如下图:
- redis中的课程缓存信息是将课程发布表中的数据转为json进行存储。
- elasticsearch中的课程索引信息是根据搜索需要将课程名称、课程介绍等信息进行索引存储。
- MinIO中存储了课程的静态化页面文件(html网页),查看课程详情是通过文件系统去浏览课程详情页面。
分布式事务技术方案
一次课程发布操作需要向数据库、redis、elasticsearch、MinIO写四份数据,这里存在分布式事务问题。
什么是分布式事务?
首先理解什么是本地事务?
平常我们在程序中通过spring去控制事务是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,此数据库只属于该应用,所以基于本应用自己的关系型数据库的事务又被称为本地事务。
本地事务具有ACID四大特性,数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。
理解了本地事务,什么是分布式事务?
现在的需求是课程发布操作后将数据写入数据库、redis、elasticsearch、MinIO四个地方,这四个地方已经不限制在一个数据库内,是由四个分散的服务去提供,与这四个服务去通信需要网络通信,而网络存在不可到达性,这种分布式系统环境下,通过与不同的服务进行网络通信去完成事务称之为分布式事务。
在分布式系统中分布式事务的场景很多:
例如用户注册送积分,银行转账,创建订单减库存,这些都是分布式事务。
拿转账举例:
我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
但是在分布式环境下,会变成下边这样:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;
可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
下边的场景都会产生分布式事务:
微服务架构下:
单服务多数据库:
多服务单数据库:
CAP理论
控制分布式事务首先需要理解CAP理论
什么是CAP理论?
CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。
使用下边的分布式系统结构进行说明:
- 客户端经过网关访问用户服务的两个结点,一致性是指用户不管访问哪一个结点拿到的数据都是最新的,比如查询小明的信息,不能出现在数据没有改变的情况下两次查询结果不一样。
- 可用性是指任何时候查询用户信息都可以查询到结果,但不保证查询到最新的数据。
- 分区容忍性也叫分区容错性,当系统采用分布式架构时由于网络通信异常导致请求中断、消息丢失,但系统依然对外提供服务。
CAP理论要强调的是在分布式系统中这三点不可能全部满足,由于是分布式系统就要满足分区容忍性,因为服务之间难免出现网络异常,不能因为局部网络异常导致整个系统不可用。
满足P那么C和A不能同时满足:
比如我们添加一个用户小明的信息,该信息先添加到结点1中,再同步到结点2中,如下图:
如果要满足C一致性,必须等待小明的信息同步完成系统才可用(否则会出现请求到结点2时查询不到数据,违反了一致性),在信息同步过程中系统是不可用的,所以满足C的同时无法满足A。
如果要满足A可用性,要时刻保证系统可用就不用等待信息同步完成,此时系统的一致性无法满足。
所以在分布式系统中进行分布式事务控制,要么保证CP、要么保证AP。
分布式事务控制方案
学习了CAP理论该如何控制分布式事务呢?
学习了CAP理论我们知道进行分布式事务控制要在C和A中作出取舍,保证一致性就不要保证可用性,保证可用性就不要保证一致,首先你确认是要CP还是AP,具体要根据应用场景进行判断。
- CP的场景:满足C舍弃A,强调一致性。
- 跨行转账:一次转账请求要等待双方银行系统都完成整个事务才算完成,只要其中一个失败另一方执行回滚操作。
- 开户操作:在业务系统开户同时要在运营商开户,任何一方开户失败该用户都不可使用,所以要满足CP。
- AP的场景:满足A舍弃C,强调可用性。
- 订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。
- 注册送积分,注册成功积分在24分到账。
- 支付短信通信,支付成功发短信,短信发送可以有延迟,甚至没有发送成功。
在实际应用中符合AP的场景较多,其实虽然AP舍弃C一致性,实际上最终数据还是达到了一致,也就满足了最终一致性,所以业界定义了BASE理论。
什么是BASE理论?
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。
- 基本可用:当系统无法满足全部可用时保证核心服务可用即可,比如一个外卖系统,每到中午12点左右系统并发量很高,此时要保证下单流程涉及的服务可用,其它服务暂时不可用。
- 软状态:是指可以存在中间状态,比如:打印自己的社保统计情况,该操作不会立即出现结果,而是提示你打印中,请在XXX时间后查收。虽然出现了中间状态,但最终状态是正确的。
- 最终一致性:退款操作后没有及时到账,经过一定的时间后账户到账,舍弃强一致性,满足最终一致性。
分布式事务控制有哪些常用的技术方案?
- 实现CP就是要实现强一致性:
- 使用Seata框架基于AT模式实现
- 使用Seata框架基于TCC模式实现。
- 实现AP则要保证最终数据一致性:
- 使用消息队列通知的方式去实现,通知失败自动重试,达到最大失败次数需要人工处理;
- 使用任务调度的方案,启动任务调度将课程信息由数据库同步到elasticsearch、MinIO、redis中。
课程发布的事务控制方案
学习了这么多的理论,回到课程发布,执行课程发布操作后要向数据库、redis、elasticsearch、MinIO写四份数据,这个场景用哪种方案?
满足CP?
如果要满足CP就表示课程发布操作后向数据库、redis、elasticsearch、MinIO写四份数据,只要有一份写失败其它的全部回滚。
满足AP?
课程发布操作后,先更新数据库中的课程发布状态,更新后向redis、elasticsearch、MinIO写课程信息,只要在一定时间内最终向redis、elasticsearch、MinIO写数据成功即可。
目前我们已经有了任务调度的技术积累,这里选用任务调度的方案去实现分布式事务控制,课程发布满足AP即可。
下图是具体的技术方案:
1、在内容管理服务的数据库中添加一个消息表,消息表和课程发布表在同一个数据库。
2、点击课程发布通过本地事务向课程发布表写入课程发布信息,同时向消息表写课程发布的消息。通过数据库进行控制,只要课程发布表插入成功消息表也插入成功,消息表的数据就记录了某门课程发布的任务。
3、启动任务调度系统定时调度内容管理服务去定时扫描消息表的记录。
4、当扫描到课程发布的消息时即开始完成向redis、elasticsearch、MinIO同步数据的操作。
5、同步数据的任务完成后删除消息表记录。
时序图如下:
下图是课程发布操作的流程:
1、执行发布操作,内容管理服务存储课程发布表的同时向消息表添加一条“课程发布任务”。这里使用本地事务保证课程发布信息保存成功,同时消息表也保存成功。
2、任务调度服务定时调度内容管理服务扫描消息表,由于课程发布操作后向消息表插入一条课程发布任务,此时扫描到一条任务。
3、拿到任务开始执行任务,分别向redis、elasticsearch及文件系统存储数据。
4、任务完成后删除消息表记录。
课程发布接口
根据课程发布的分布式事务控制方案,课程发布操作首先通过本地事务向课程发布表写入课程发布信息并向消息表插入一条消息,这里定义的课程发布接口要实现该功能。
在内容管理接口工程中定义课程发布接口。
package com.xuecheng.content.api;
/**
* @author Mr.M
* @version 1.0
* @description 课程预览,发布
* @date 2022/9/16 14:48
*/
@Controller()
public class CoursePublishController {
@ApiOperation("课程发布")
@ResponseBody
@PostMapping("/coursepublish/{courseId}")
public void coursepublish(@PathVariable("courseId") Long courseId) {
}
}
课程发布操作对数据库操作如下:
1、向课程发布表course_publish插入一条记录,记录来源于课程预发布表,如果存在则更新,发布状态为:已发布。
2、更新course_base表的课程发布状态为:已发布
3、删除课程预发布表的对应记录。
4、向mq_message消息表插入一条消息,消息类型为:course_publish
约束:
1、课程审核通过方可发布。
2、本机构只允许发布本机构的课程。
以上功能使用自动生成的mapper接口即可完成。
1、在内容管理数据库创建mq_message消息表及消息历史消息表(历史表存储已经完成的消息)。
消息表结构如下:
2、生成mq_message消息表、course_publish课程发布表的po和mapper接口
3、稍后会开发一个通用的消息处理组件,这里先不生成代码。
定义Service接口:
package com.xuecheng.content.service;
/**
* @author Mr.M
* @version 1.0
* @description 课程预览、发布接口
* @date 2022/9/16 14:59
*/
public interface CoursePublishService {
/**
* @param companyId 机构id
* @param courseId 课程id
* @return void
* @description 课程发布接口
* @author Mr.M
* @date 2022/9/20 16:23
*/
public void publish(Long companyId, Long courseId);
}
编写课程发布的Service方法:
package com.xuecheng.content.service.impl;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/16 15:37
*/
@Service
public class CoursePublishServiceImpl implements CoursePublishService {
@Autowired
CourseBaseInfoService courseBaseInfoService;
@Autowired
TeachplanService teachplanService;
@Autowired
CourseBaseMapper courseBaseMapper;
@Autowired
CourseMarketMapper courseMarketMapper;
@Autowired
CoursePublishPreMapper coursePublishPreMapper;
@Autowired
CoursePublishMapper coursePublishMapper;
@Transactional
@Override
public void publish(Long companyId, Long courseId) {
//约束校验
//查询课程预发布表
CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
if (coursePublishPre == null) {
XueChengPlusException.cast("请先提交课程审核,审核通过才可以发布");
}
//本机构只允许提交本机构的课程
if (!coursePublishPre.getCompanyId().equals(companyId)) {
XueChengPlusException.cast("不允许提交其它机构的课程。");
}
//课程审核状态
String auditStatus = coursePublishPre.getStatus();
//审核通过方可发布
if (!"202004".equals(auditStatus)) {
XueChengPlusException.cast("操作失败,课程审核通过方可发布。");
}
// 保存课程发布信息
saveCoursePublish(courseId);
//保存消息表
saveCoursePublishMessage(courseId);
//删除课程预发布表对应记录
coursePublishPreMapper.deleteById(courseId);
}
/**
* @description 保存课程发布信息
* @param courseId 课程id
* @return void
* @author Mr.M
* @date 2022/9/20 16:32
*/
private void saveCoursePublish(Long courseId){
//整合课程发布信息
//查询课程预发布表
CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
if(coursePublishPre == null){
XueChengPlusException.cast("课程预发布数据为空");
}
CoursePublish coursePublish = new CoursePublish();
//拷贝到课程发布对象
BeanUtils.copyProperties(coursePublishPre,coursePublish);
coursePublish.setStatus("203002");
CoursePublish coursePublishUpdate = coursePublishMapper.selectById(courseId);
if(coursePublishUpdate == null){
coursePublishMapper.insert(coursePublish);
}else{
coursePublishMapper.updateById(coursePublish);
}
//更新课程基本表的发布状态
CourseBase courseBase = courseBaseMapper.selectById(courseId);
courseBase.setStatus("203002");
courseBaseMapper.updateById(courseBase);
}
/**
* @param courseId 课程id
* @return void
* @description 保存消息表记录,
* @author Mr.M
* @date 2022/9/20 16:32
*/
private void saveCoursePublishMessage(Long courseId) {
// 稍后实现
}
}
完善接口层代码
package com.xuecheng.content.api;
/**
* @author Mr.M
* @version 1.0
* @description 课程预览,发布
* @date 2022/9/16 14:48
*/
@Controller()
public class CoursePublishController {
@Autowired
CoursePublishService coursePublishService;
@ApiOperation("课程发布")
@ResponseBody
@PostMapping("/coursepublish/{courseId}")
public void coursepublish(@PathVariable("courseId") Long courseId) {
Long companyId = 1232141425L;
coursePublishService.publish(companyId, courseId);
}
}
消息处理SDK
消息模块技术方案
课程发布操作执行后需要扫描消息表的记录,有关消息表处理的有哪些?
上图中红色框内的都是与消息处理相关的操作:
1、新增消息表
2、扫描消息表。
3、更新消息表。
4、删除消息表。
使用消息表这种方式实现最终事务一致性的地方除了课程发布还有其它业务场景。
如果在每个地方都实现一套针对消息表定时扫描、处理的逻辑基本上都是重复的,软件的可复用性太低,成本太高。
如何解决这个问题?
针对这个问题可以想到将消息处理相关的逻辑做成一个通用的东西。
是做成通用的服务,还是做成通用的代码组件呢?
- 通用的服务是完成一个通用的独立功能,并提供独立的网络接口,比如:项目中的文件系统服务,提供文件的分布式存储服务。
- 代码组件也是完成一个通用的独立功能,通常会提供API的方式供外部系统使用,比如:fastjson、Apache commons工具包等。
如果将消息处理做成一个通用的服务,该服务需要连接多个数据库,因为它要扫描微服务数据库下的消息表,并且要提供与微服务通信的网络接口,单就针对当前需求而言开发成本有点高。
如果将消息处理做一个SDK工具包相比通用服务不仅可以解决将消息处理通用化的需求,还可以降低成本。
所以,本项目确定将对消息表相关的处理做成一个SDK组件供各微服务使用, 如下图所示:
下边对消息SDK的设计内容进行说明:
sdk需要提供执行任务的逻辑吗?
拿课程发布任务举例,执行课程发布任务是要向redis、索引库等同步数据,其它任务的执行逻辑是不同的,所以执行任务在sdk中不用实现任务逻辑,只需要提供一个抽象方法由具体的执行任务方去实现。
如何保证任务的幂等性?
在视频处理章节介绍的视频处理的幂等性方案,这里可以采用类似方案,任务执行完成后会从消息表删除,如果消息的状态是完成或不存在消息表中则不用执行。
如何保证任务不重复执行?
采用和视频处理章节一致方案,除了保证任务的幂等性外,任务调度采用分片广播,根据分片参数去获取任务,另外阻塞调度策略为丢弃任务。
注意:这里是信息同步类任务,即使任务重复执行也没有关系,不再使用抢占任务的方式保证任务不重复执行。
还有一个问题,根据消息表记录是否存在或消息表中的任务状态去保证任务的幂等性,如果一个任务有好几个小任务,比如:课程发布任务需要执行三个同步操作:存储课程到redis、存储课程到索引库,存储课程页面到文件系统。如果其中一个小任务已经完成也不应该去重复执行。这里该如何设计?
将小任务作为任务的不同的阶段,在消息表中设计阶段状态。
每完成一个阶段在相应的阶段状态字段打上完成标记,即使这个大任务没有完成再重新执行时,如果小阶段任务完成了也不会重复执行某个小阶段的任务。
综上所述,除了消息表的基本的增、删、改、查的接口外,消息SDK还具有如下接口功能:
package com.xuecheng.messagesdk.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xuecheng.messagesdk.model.po.MqMessage;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author Mr.M
* @since 2022-09-21
*/
public interface MqMessageService extends IService<MqMessage> {
/**
* @description 扫描消息表记录,采用与扫描视频处理表相同的思路
* @param shardIndex 分片序号
* @param shardTotal 分片总数
* @param count 扫描记录数
* @return java.util.List 消息记录
* @author Mr.M
* @date 2022/9/21 18:55
*/
public List<MqMessage> getMessageList(int shardIndex, int shardTotal, String messageType,int count);
/**
* @description 完成任务
* @param id 消息id
* @return int 更新成功:1
* @author Mr.M
* @date 2022/9/21 20:49
*/
public int completed(long id);
/**
* @description 完成阶段任务
* @param id 消息id
* @return int 更新成功:1
* @author Mr.M
* @date 2022/9/21 20:49
*/
public int completedStageOne(long id);
public int completedStageTwo(long id);
public int completedStageThree(long id);
public int completedStageFour(long id);
/**
* @description 查询阶段状态
* @param id
* @return int
* @author Mr.M
* @date 2022/9/21 20:54
*/
public int getStageOne(long id);
public int getStageTwo(long id);
public int getStageThree(long id);
public int getStageFour(long id);
}
消息SDK提供消息处理抽象类,此抽象类供使用方去继承使用,如下:
package com.xuecheng.messagesdk.service;
import com.xuecheng.messagesdk.model.po.MqMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.concurrent.*;
/**
* @author Mr.M
* @version 1.0
* @description 消息处理抽象类
* @date 2022/9/21 19:44
*/
@Slf4j
@Data
public abstract class MessageProcessAbstract {
@Autowired
MqMessageService mqMessageService;
/**
* @param mqMessage 执行任务内容
* @return boolean true:处理成功,false处理失败
* @description 任务处理
* @author Mr.M
* @date 2022/9/21 19:47
*/
public abstract boolean execute(MqMessage mqMessage);
/**
* @description 扫描消息表多线程执行任务
* @param shardIndex 分片序号
* @param shardTotal 分片总数
* @param messageType 消息类型
* @param count 一次取出任务总数
* @param timeout 预估任务执行时间,到此时间如果任务还没有结束则强制结束 单位秒
* @return void
* @author Mr.M
* @date 2022/9/21 20:35
*/
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
1、在内容管理数据库创建消息表和消息历史表
2、拷贝课程资料中的xuecheng-plus-message-sdk到工程目录,如下图:
3、在内容管理service工程中添加sdk依赖
<!--操作消息表SDK -->
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-message-sdk</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
4、课程发布操作使用本地事务保存课程发布信息、添加消息表。
回到当初编写课程发布时的代码,填充的saveCoursePublishMessage(courseId)方法
package com.xuecheng.content.service.impl;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/16 15:37
*/
@Service
public class CoursePublishServiceImpl implements CoursePublishService {
@Autowired
MqMessageService mqMessageService;
@Transactional
@Override
public void publish(Long companyId, Long courseId) {
... ...
//保存消息表
saveCoursePublishMessage(courseId);
}
/**
* @param courseId 课程id
* @return void
* @description 保存消息表记录,
* @author Mr.M
* @date 2022/9/20 16:32
*/
private void saveCoursePublishMessage(Long courseId) {
MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
if(mqMessage==null){
XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
}
}
}
4、下边进行测试:
- 启动前端, 启动后端, 启动nginx
- 发布一门课程,观察消息表是否正常添加消息。
- 需要手动修改课程审核状态为审核通过执行发布操作,发布后可以修改发布状态为下架重新发布测试。
课程发布任务处理
在内容管理服务添加消息处理sdk的依赖即可使用它,实现sdk中的MessageProcessAbstract类,重写execte方法。
实现sdk中的MessageProcessAbstract类:
package com.xuecheng.content.jobhandler;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/22 10:16
*/
@Slf4j
@Component
public class CoursePublishTask extends MessageProcessAbstract {
//课程发布任务处理
@Override
public boolean execute(MqMessage mqMessage) {
//获取消息相关的业务信息
String businessKey1 = mqMessage.getBusinessKey1();
long courseId = Integer.parseInt(businessKey1);
//课程静态化
generateCourseHtml(mqMessage, courseId);
//课程索引
saveCourseIndex(mqMessage, courseId);
//课程缓存
saveCourseCache(mqMessage, courseId);
//返回true表示任务执行完成
return true;
}
//生成课程静态化页面并上传至文件系统
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
public void saveCourseCache(MqMessage mqMessage, long courseId) {
log.debug("将课程信息缓存至redis,课程id:{}", courseId);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//保存课程索引信息
public void saveCourseIndex(MqMessage mqMessage, long courseId) {
log.debug("保存课程索引信息,课程id:{}", courseId);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
开启任务调度
1、首先在内容管理service工程中添加xxl-job依赖
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
2、配置执行器: 在nacos中在content-service-dev.yaml中配置
xxl:
job:
admin:
addresses: http://192.168.101.65:8088/xxl-job-admin
executor:
appname: coursepublish-job
address:
ip:
port: 8999
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
accessToken: default_token
3、从媒资管理服务层工程中拷贝一个XxlJobConfig配置类到内容管理service工程中。
在xxl-job-admin控制台中添加执行器
4、编写任务调度入口
package com.xuecheng.content.jobhandler;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/22 10:16
*/
@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);
}
......
}
5、在xxl-job添加任务
到此SDK开发、集成完成,下一步添加课程发布后页面静态化、课程缓存、课程索引等任务。
6、在消息表添加课程发布的消息,消息类型为course_publish, business_key1为发布课程的ID
- 启动内容管理服务
- 启动课程发布任务, 测试是否可以正常调度执行。
- 测试任务幂等性: 在generateCourseHtml方法中模拟异常
- 任务执行失败后, 阶段处理状态还是0, 符合预期