文章目录
一、背景
很多业务使用任务调度(xxl-job)+消息表的方式来处理一些特定的业务逻辑来实现最终一致性,这种业务一般满足AP即可。这里以课程发布作为例子来讲述如何设计消息模块。这种模式可能在项目中很多地方都会用到,如果每个设计的业务单独去做一套消息表明显是不合适的,所以这里可以考虑把它设计成SDK供业务模块来使用。
二、消息模块技术方案
课程发布操作执行后需要扫描消息表的记录,有关消息表处理的有哪些?
上图中红色框内的都是与消息处理相关的操作:
1、新增消息表
2、扫描消息表。
3、更新消息表。
4、删除消息表。
使用消息表这种方式实现最终事务一致性的地方除了课程发布还有其它业务场景。
如果在每个地方都实现一套针对消息表定时扫描、处理的逻辑基本上都是重复的,软件的可复用性太低,成本太高。
- 如何解决这个问题?
针对这个问题可以想到将消息处理相关的逻辑做成一个通用的东西。是做成通用的服务,还是做成通用的代码组件呢?通用的服务是完成一个通用的独立功能,并提供独立的网络接口,比如:项目中的文件系统服务,提供文件的分布式存储服务。代码组件也是完成一个通用的独立功能,通常会提供API的方式供外部系统使用,比如:fastjson、Apache commons工具包等。如果将消息处理做成一个通用的服务,该服务需要连接多个数据库,因为它要扫描微服务数据库下的消息表,并且要提供与微服务通信的网络接口,单就针对当前需求而言开发成本有点高。如果将消息处理做一个SDK工具包相比通用服务不仅可以解决将消息处理通用化的需求,还可以降低成本。所以,对消息表相关的处理做成一个SDK组件供各微服务使用,如下图所示: - SDK需要提供执行任务的逻辑吗?
拿课程发布任务举例,执行课程发布任务是要向redis、索引库等同步数据,其它任务的执行逻辑是不同的,所以执行任务在sdk中不用实现任务逻辑,只需要提供一个抽象方法由具体的执行任务方去实现。 - 如何保证任务的幂等性?
任务执行完成后会从消息表删除,如果消息的状态是完成或不存在消息表中则不用执行。 - 如何保证任务不重复执行?
除了保证任务的幂等性外,任务调度采用分片广播,根据分片参数去获取任务,另外阻塞调度策略为丢弃任务。
注意:这里是信息同步类任务,即使任务重复执行也没有关系,不再使用抢占任务的方式保证任务不重复执行。 - 还有一个问题,根据消息表记录是否存在或消息表中的任务状态去保证任务的幂等性,如果一个任务有好几个小任务,比如:课程发布任务需要执行三个同步操作:存储课程到redis、存储课程到索引库,存储课程页面到文件系统。如果其中一个小任务已经完成也不应该去重复执行。这里该如何设计?
将小任务作为任务的不同的阶段,在消息表中设计阶段状态。
每完成一个阶段在相应的阶段状态字段打上完成标记,即使这个大任务没有完成再重新执行时,如果小阶段任务完成了也不会重复执行某个小阶段的任务。
综上所述,除了消息表的基本的增、删、改、查的接口外,消息SDK还具有如下接口功能:
public interface MqMessageService extends IService<MqMessage> {
/**
* @description 扫描消息表记录
* @param shardIndex 分片序号
* @param shardTotal 分片总数
* @param count 扫描记录数
* @return java.util.List 消息记录
*/
public List<MqMessage> getMessageList(int shardIndex, int shardTotal, String messageType,int count);
/**
* @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 查询阶段状态
* @param id
* @return int
*/
public int getStageOne(long id);
public int getStageTwo(long id);
public int getStageThree(long id);
public int getStageFour(long id);
}
消息SDK提供消息处理抽象类,此抽象类供使用方去继承使用,如下:
@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 预估任务执行时间,到此时间如果任务还没有结束则强制结束 单位秒
* @return void
*/
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();
}
}
}
三、测试使用
1.数据库创建消息表和消息历史表
2.继承MessageProcessAbstract 抽象类编写任务执行方法
/**
1. @description 消息处理测试类,继承MessageProcessAbstract
*/
@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;
}
}
3.编写测试类
@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);
}
}
4.准备测试数据,在消息表添加消息类型为"test"的消息
5.执行MessageProcessClassTest 类中的test()方法,观察控制台任务执行的日志信息。