消息处理SDK
SDK工具包的特点
对于当前基于本地数据库+消息表()
的分布式事务控制的方案在其他服务中如果存在分布式事务问题也可以采用
在每个服务中都实现一套针对消息表的新增/定时扫描/更新/删除
操作是比较繁琐的,所以我们可以将消息处理相关的业务做成一个通用的东西
通用的服务
:服务可以提供独立的网络接口,比如项目中的文件系统服务,提供文件的分布式存储服务,缺点是需要要提供与其他微服务通信的网络接口且可能要连接数据库,开发成本高通用的代码组件SDK(降低成本)
:SDK工具包通常会提供API的方式供外部系统使用,比如fastjson、Apache commons工具包等
SDK工具包的特点
-
任务逻辑
:对于课程发布任务是要向redis、索引库等同步数据,但其它任务的执行逻辑是不同的,所以执行任务在sdk中不用实现任务逻辑,只需要提供一个抽象方法由具体的执行任务方去实现 -
任务的幂等性
:一个任务只需要被处理一次,即使重复执行了相同的任务资源最终也只会处理一次,课程发布任务执行完成后会从消息表删除任务,如果消息表中任务的状态是完成或不存在
则不用执行 -
设计任务阶段状态(根据需求设置阶段数)
:如果一个任务有好几个小任务也不应该去重复执行这些小任务- 比如课程发布任务完成需要执行三个同步操作,将发布课程信息存储到Redis、将发布课程信息存储到索引库,将发布课程页面存储到文件系统
-
任务不重复
: 任务调度采用分片广播,根据分片参数去获取任务,配置调度过期策略为忽略,阻塞调度策略为丢弃后续调度,这里不再设置乐观锁保证任务绝对的不重复执行,因为对于信息同步类任务(低消耗),即使任务重复执行也没有关系
数据模型
在xc_project
项目工程根目录下创建xuecheng-plus-message-sdk
SDK工程,在内容管理数据库创建消息表和消息历史表
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