在实际的开发中,定时执行某任务是非常常见的需求。首先我们先回顾一下,spring框架内置的定时任务框架(SpringTask),代码如下:
@Slf4j
@Component //使spring管理
@EnableScheduling //定时任务注解
public class Timer {
@Scheduled(cron = "0/10 * * * * ?")
public void notifyOrder(){
log.info("timer notifyOrder ....");
//todo: some logic
}
}
Spring定时任务存在的问题:
因为Spring定时任务存在这些“硬伤”,所以我们需要一个更专业的分布式任务调度平台来解决开发中的问题。
分布式定时任务框架的实现思路:
分布式任务调度比较常见的有elastic-Job(当当网开源) 、XXL-job(美团点评)、TBSchedule(阿里早期开源项目)。
XXL-JOB是美团点评中间件研发工程师许雪里开发的一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。目前已有多家公司接入xxl-job,包括比较知名的大众点评,京东,优信二手车,北京尚德,360金融 (360),联想集团 (联想),易信 (网易)等等…
官网地址:https://github.com/xuxueli/xxl-job
XXL-JOB架构图如下:
我们提供的镜像已经安装好了xxl-job。
如果你想在自己的服务器上安装xxl-job。可以按照以下步骤按照xxl-job
(1)连接mysql,执行xxl-job的建库建表脚本。
https://github.com/xuxueli/xxl-job/blob/2.2.0/doc/db/tables_xxl_job.sql
执行后数据库表
(2)在服务器建立docker-compose.yml,内容如下
version: "3"
services:
minio:
image: xuxueli/xxl-job-admin:2.2.0
container_name: xxl-job-admin
privileged: true
volumes:
- /opt/middleware/xxljob/applogs:/data/applogs
ports:
- "8080:8080"
environment:
TZ: Asia/Shanghai
PARAMS: '--spring.datasource.url=jdbc:mysql://192.168.200.128:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 --spring.datasource.username=root --spring.datasource.password=root123
(3)执行命令:
docker-compose up
即可启动xxl-job
访问管理台:http://192.168.200.128:8080/xxl-job-admin/ 默认帐号/密码:admin/123456
在资料第4章提供了XXL-job2.2.0的源码,同学们也可以到 https://github.com/xuxueli/xxl-job/tree/2.2.0 下载
(1)打开XXL-job源码包中xxl-job-executor-samples下xxl-job-executor-sample-springboot
(2)修改application.properties,修改调度中心的Ip地址。
### xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
xxl.job.admin.addresses=http://192.128.200.128:8080/xxl-job-admin
要点:
配置文件xxl.job.executor.appname执行的名字和调度中心-执行器管理列表中的AppName相同
如果没有,需要在执行器管理中新增。如果注册方式为自动注册,online地址无需添加,执行器启动后,会自动注册。
在我们在任务管理中新增任务窗口,会看到一个较阻塞处理策略的设置
阻塞处理策略:调度过于密集执行器来不及处理时的处理策略。
阻塞测试:
将cron表达式设置为0/5 * * * * ?
即每5秒执行一次,而任务的执行时间要大于10秒
(1)在lkd_user_service项目中添加xxl-job的引用
<dependency>
<groupId>com.xuxueligroupId>
<artifactId>xxl-job-coreartifactId>
<version>2.2.0version>
dependency>
(2)更新核心配置文件,添加xxl-job的相关配置如下:
xxl:
job:
accessToken:
admin:
addresses: http://192.168.200.128:8080/xxl-job-admin
executor:
appname: user-service
address:
ip:
port: 9981
logretentiondays: 30
(3)在lkd_user_service创建com.lkd.job包,添加xxl-job的配置类:
package com.lkd.job;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
@Slf4j
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
//@Value("${xxl.job.executor.logpath}")
//private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
//xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
以上配置类,可以从xxl-job提供的源码中拷贝,稍作修改即可。
(4)新建一个job
package com.lkd.job;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class UserJob {
@XxlJob("testHandler")
public ReturnT<String> testHandler(String param) throws Exception {
log.info("立可得集成xxl-job");
return ReturnT.SUCCESS;
}
}
(5)在xxl-job-admin里新建执行器user-service
测试步骤:
(1)启动用户微服务
(2)启动任务,观察控制台和日志输出
当售货机上报设备故障状态信息时,工单服务要根据上报上来的设备编号自动从售货机所在区域内找到当天该区域内分配工单最少的维修员,为该维修员创建一个待处理的自动维修工单,并且当设备没有待处理或进行中的维修工单时才能创建成功,这样可以方式设备在未维修之前重复上报故障状态从而创建过多重复工单。
(1)工单服务通过负载均衡的策略订阅EMQ中的售货机状态消息,当设备上报故障状态时自动为该设备创建一个维修工单。
(2)创建工单的过程中需要根据售货机所在区域内的所有维修员当日分配最少的规则为维修员来创建。如果得到所在区域内的所有维修员当日分配最少的那个人呢? 之前有个程序员是这样实现的:
/**
* 获取同一天内分配的工单最少的人
* @param innerCode
* @param isRepair 是否是维修工单
* @return
*/
@Override
public Integer getLeastUser(String innerCode,Boolean isRepair){
//1.根据售货机编号查询所在区域的用户列表
List<UserViewModel> userList = null;
if(isRepair){
userList = userService.getRepairerListByInnerCode(innerCode);
}else {
userList = userService.getOperatorListByInnerCode(innerCode);
}
if(userList == null) return null;
//2.按人分组统计工单表,取工作量
QueryWrapper<TaskEntity> qw = new QueryWrapper<>();
qw.select("assignor_id,count(1) as user_id");
if(isRepair){//如果是运维
qw.lambda().ne(TaskEntity::getProductTypeId, VMSystem.TASK_TYPE_SUPPLY);
}else {//如果是运营
qw.lambda().eq(TaskEntity::getProductTypeId,VMSystem.TASK_TYPE_SUPPLY);
}
qw
.lambda()
.ne(TaskEntity::getTaskStatus,VMSystem.TASK_STATUS_CANCEL) //根据所有未被取消的工单做统计
.ge(TaskEntity::getCreateTime,LocalDate.now())
.in(TaskEntity::getAssignorId,userList.stream().map(UserViewModel::getUserId).collect(Collectors.toList()))
.groupBy(TaskEntity::getAssignorId)
.orderByAsc(TaskEntity::getUserId);
List<TaskEntity> result = this.list(qw);
//3.循环用户列表,找出没有工单的用户
List<TaskEntity> taskList = Lists.newArrayList();
Integer userId = 0;
for (UserViewModel user:userList) {
Optional<TaskEntity> taskEntity = result.stream().filter(r->r.getAssignorId() == user.getUserId()).findFirst();
//当前人员今日没有分配工单
if(taskEntity.isEmpty()){
return user.getUserId();
}
TaskEntity item = new TaskEntity();
item.setAssignorId(user.getUserId());
item.setUserId(taskEntity.get().getUserId());
taskList.add(item);
}
//4.对列表排序取最少工单的人
taskList.stream().sorted(Comparator.comparing(TaskEntity::getUserId));
return taskList.get(0).getAssignorId();
}
以上代码虽然能够实现对每日最低工单人员的查询,但是运行效率及其低下,查询逻辑复杂(至少三次数据库查询)。我们这里有一个更优的方案:
(1)使用redis来保存每个人的工单数据,每次创建工单在redis里使用原子增,不用考虑锁。这样得到每日最低工单人员的查询可以做到对数据库零查询。
(2)使用redis的ZSet可以实现数据的自动排序,无需手动排序,再次提升了程序的运行效率,降低了代码的复杂度。
(3)key的规则,以固定字符串(前缀)+时间+区域+工单类别(运营/运维)为大key,以人员id做小key,过期时间为2天。
(4)redis工单数列表初始化,由xxl-job负责处理,每日下午生成第2天的工单数列表。
(1)VMSystem类中定义常量(区域工单统计前缀)
/**
* 区域工单统计前缀
*/
public static final String REGION_TASK_KEY_PREF = "region.task.";
(2)在lkd_user_service的UserJob类中新建方法,用于初始化每日工单量列表
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 每日工单量列表初始化
* @param param
* @return
* @throws Exception
*/
@XxlJob("workCountInitJobHandler")
public ReturnT<String> workCountInitJobHandler(String param) throws Exception{
//查询用户列表
List<UserEntity> userList = userService.list();
//构建数据(zset)
userList.forEach(user -> {
if(user.getRoleId().intValue()!=0){ //只考虑非管理员
String key= VMSystem.REGION_TASK_KEY_PREF
+ LocalDate.now().plusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
+"."+ user.getRegionId()+"."+user.getRoleCode();
redisTemplate.opsForZSet().add(key,user.getId(),0 ) ;
redisTemplate.expire(key, Duration.ofDays(2));
}
});
return ReturnT.SUCCESS;
}
(3)XXL-JOB 中配置,每日执行一次,用于生成第2天的数据
(1)工单微服务 TaskServiceImpl类新增私有方法,用于更新工单量列表
/**
* 更新工单量列表
* @param taskEntity
* @param score
*/
private void updateTaskZSet(TaskEntity taskEntity,int score){
String roleCode="1003";//运维员
if(taskEntity.getProductTypeId().intValue()==2){//如果是补货工单
roleCode="1002";//运营员
}
String key= VMSystem.REGION_TASK_KEY_PREF
+ LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
+"."+ taskEntity.getRegionId()+"."+roleCode;
redisTemplate.opsForZSet().incrementScore(key,taskEntity.getAssignorId(),score);
}
incrementScore方法属于原子增,不需要考虑锁。
(2)createTask(创建工单)方法添加以下代码:
updateTaskZSet(taskEntity,1);
(3)cancelTask(取消工单)方法添加以下代码:
updateTaskZSet(taskEntity,-1);
产品需求:获取同一天内分配的工单最少的人
TaskService新增方法定义
/**
* 获取同一天内分配的工单最少的人
* @param innerCode
* @param isRepair 是否是维修工单
* @return
*/
Integer getLeastUser(String innerCode,Boolean isRepair);
TaskServiceImpl实现该方法
@Override
public Integer getLeastUser(Integer regionId, Boolean isRepair) {
String roleCode="1002";
if(isRepair){ //如果是维修工单
roleCode="1003";
}
String key= VMSystem.REGION_TASK_KEY_PREF
+ LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
+"."+ regionId+"."+roleCode;
Set<Object> set = redisTemplate.opsForZSet().range(key, 0, 1);
if(set==null || set.isEmpty()){
throw new LogicException("该区域暂时没有相关人员");
}
return (Integer) set.stream().collect( Collectors.toList()).get(0);
}
售货机部件发生变化之后状态主动上报:
//传输方向:client->server
{
"msgType":"vmStatus",
"sn":342424343,
"vmId":"01012121",
"statusInfo":
[{
"statusCode":"1001",//状态码
"status":true,//是否正常
}...]
}
上报的诸多状态中,只要有一个是false就表示设备出现故障。
协议封装类
/**
* 状态信息
*/
@Data
public class StatusInfo implements Serializable{
/**
* 状态码
*/
private String statusCode;
/**
* 是否正常
*/
private boolean status;
}
/**
* 售货机状态
*/
@Data
public class VmStatusContract extends BaseContract{
private List<StatusInfo> statusInfo;
public VmStatusContract() {
this.setMsgType("vmStatus");
}
}
设备发生故障,会上报到主题server/status。
在工单服务的配置项里EMQ相关配置,用来接收售货机上报的状态消息的配置如下:
consumer:
consumerTopics: $share/task/server/status ##售货机状态消息上报主题
这里通过$share
带群组的方式来实现共享订阅,工单服务都在群组task
下通过负载均衡的方式来接收售货机上报的状态消息。那么什么是带群组方式的共享订阅呢?看下面这张图
我们向server/status主题发送消息,三个群组都会收到消息,每个群组只有一个节点会收到消息。
这样做的目的是:
在工单服务项目中的com.lkd.business
包下创建售货机状态消息的接收处理类,来处理设备消息:
@Component
@ProcessType(value = "vmStatus")
@Slf4j
public class VMStatusHandler implements MsgHandler {
@Autowired
private TaskService taskService;
@Autowired
private VMService vmService;
@Override
public void process(String jsonMsg) throws IOException {
VmStatusContract vmStatusContract = JsonUtil.getByJson(jsonMsg, VmStatusContract.class);
if(vmStatusContract == null || vmStatusContract.getStatusInfo() == null || vmStatusContract.getStatusInfo().size() <= 0) return;
//如果为非正常状态,则创建维修工单
if(vmStatusContract.getStatusInfo().stream().allMatch(s->s.isStatus() == false)){
try {
//根据售货机编号,查询售货机
VendingMachineViewModel vmInfo = vmService.getVMInfo(vmStatusContract.getInnerCode());
//查询最少工单量用户
Integer userId = taskService.getLeastUser(vmInfo.getRegionId().intValue(),true);
//创建工单对象
var task = new TaskViewModel();
task.setAssignorId(userId);//执行
task.setDesc("自动维修工单");
task.setInnerCode(vmStatusContract.getInnerCode());//售货机编码
task.setProductType(VMSystem.TASK_TYPE_REPAIR );//维修工单
task.setCreateType(0);//自动工单
taskService.createTask(task);
}catch (Exception ex){
log.error("创建自动维修工单失败,msg is:"+jsonMsg);
}
}
}
}
(1)初始化工单量列表数据,为了我们能马上有初始化的数据列表,我们可以暂时修改UserJob的workCountInitJobHandler方法中plusDays传入的值由1改为0。
(2)启动用户微服务、售货机微服务、工单微服务
(3)然后在xxl-job中运行该任务。运行后再改回1。
(4)在EMQ控制台Websocket工具中发送消息到主题 server/status,消息内容
{"msgType":"vmStatus","sn":342424343,"innerCode":"01000005","statusInfo":[{"statusCode":"1001","status":true},{"statusCode":"1002","status":false}]}
(5)在数据库中查询工单数据,看是否有新的自动维修工单。
如果每天都由管理员去创建补货工单,必定是一项耗时耗力的工作,所以立可得提供了自动补货工单的功能。每天定时执行任务,对售货机进行扫描,发现缺货自动创建补货工单。
根据实际运营过程,需要在系统中设置一个缺货预警值。在运营工单中有工单配置按钮, 补货警戒线的值为最低的存货百分比。
系统通过每天的定时任务扫描所有在运营的售货机,只要某售货机任何一个货道商品数量小于等于这个比例,就会为这个售货机自动创建补货工单,把所有的货道全部补满。
(1)编写业务逻辑,根据售货机编号统计等于或小于预警库存的货道数量,如果结果大于0,将构建补货协议数据(所有货道补满),通过emq发送到工单微服务。
(2)在xxl-job中配置定时任务。编写定时任务类,查询所有正在运营状态的售货机,循环调用第1步的业务逻辑。
(3)工单微服务接收消息,通过补货协议数据,自动创建补货工单,补货人员的选择与自动维修工单一致。
(1)lkd_vms_service的pom.xml添加xxl-job依赖
<dependency>
<groupId>com.xuxueligroupId>
<artifactId>xxl-job-coreartifactId>
<version>2.2.0version>
dependency>
(2)售货机微服务有关xxl-job的配置
xxl:
job:
accessToken:
admin:
addresses: http://192.168.200.128:8080/xxl-job-admin
executor:
appname: vm-service
address:
ip:
port: 9980
logretentiondays: 30
(3)创建com.lkd.job包,包下创建类XxlJobConfig(从用户微服务拷贝)
(4)在售货机微服务项目lkd_vms_service
中实现XXL-JOB的任务执行器,查询所有运营状态的售货机,执行扫描货道的逻辑:
/**
* 自动补货任务生成Job
*/
@Component
@Slf4j
public class SupplyJob {
@Autowired
private VendingMachineService vmService;
/**
* 售货机扫描任务
*/
@XxlJob("supplyJobHandler")
public ReturnT<String> supplyJobHandler(String param) throws Exception {
QueryWrapper<VendingMachineEntity> qw = new QueryWrapper<>();
qw
.lambda()
.eq(VendingMachineEntity::getVmStatus, VMSystem.VM_STATUS_RUNNING);
List<VendingMachineEntity> vmList =vmService.list(qw);
vmList.forEach(v->{
//todo: 扫描售货机货道
});
return ReturnT.SUCCESS;
}
}
(5)XXL-JOB中配置执行器和任务
编写业务逻辑,根据售货机编号统计等于或小于预警库存的货道数量,返回统计结果。
在售货机微服务项目lkd_vms_service
中售货机服务业务中实现售货机缺货检测判断的逻辑:
(1)在VendingMachineService接口中添加检测缺货方法的定义:
/**
* 货道扫描与补货消息
* @param percent
*/
int inventory(int percent,VendingMachineEntity vmEntity);
在实现类VendingMachineServiceImpl
中实现该方法:
@Override
public int inventory(int percent, VendingMachineEntity vmEntity) {
//警戒线的值计算
int maxCapacity = vmEntity.getType().getChannelMaxCapacity();//售货机货道最大容量
int alertValue = (int) (maxCapacity* (float)percent/100); //得到该售货机警戒容量
//统计当前库存小于等于警戒线的货道数量
QueryWrapper<ChannelEntity> channelQueryWrapper=new QueryWrapper<>();
channelQueryWrapper.lambda()
.eq( ChannelEntity::getVmId,vmEntity.getId() )
.le( ChannelEntity::getCurrentCapacity,alertValue )
.ne( ChannelEntity::getSkuId ,0L );
return channelService.count(channelQueryWrapper);
}
(2)修改SupplyJob
@Component
@Slf4j
public class SupplyJob {
@Autowired
private VendingMachineService vmService;
@Autowired
private TaskService taskService;
/**
* 扫描所有运营状态的售货机
* @param param
* @return
* @throws Exception
*/
@XxlJob("supplyJobHandler")
public ReturnT<String> supplyJobHandler(String param) throws Exception{
Integer percent = taskService.getSupplyAlertValue();//获取警戒线百分比
//查询所有运营状态的售货机
QueryWrapper<VendingMachineEntity> qw=new QueryWrapper<>();
qw.lambda()
.eq(VendingMachineEntity::getVmStatus, VMSystem.VM_STATUS_RUNNING);
List<VendingMachineEntity> vmList = vmService.list(qw);
vmList.forEach( vm->{
XxlJobLogger.log("扫描售货机"+vm.getInnerCode());
//扫描售货机货道的逻辑
int count = vmService.inventory(percent, vm);//售货机缺货货道数量
if(count>0){
XxlJobLogger.log("售货机"+vm.getInnerCode()+"缺货");
}
});
return ReturnT.SUCCESS;
}
}
(1)VendingMachineService新增方法定义
/**
* 发送补货工单
* @param vmEntity
*/
void sendSupplyTask( VendingMachineEntity vmEntity );
VendingMachineServiceImpl实现方法
@Override
public void sendSupplyTask(VendingMachineEntity vmEntity) {
//查询售货机的货道列表( skuId!=0 )
QueryWrapper<ChannelEntity> channelQueryWrapper=new QueryWrapper<>();
channelQueryWrapper.lambda()
.eq(ChannelEntity::getVmId,vmEntity.getId() )//售货机Id
.ne(ChannelEntity::getSkuId, 0L );
//货道列表
List<ChannelEntity> channelList = channelService.list(channelQueryWrapper);
//补货列表
List<SupplyChannel> supplyChannelList = channelList.stream().map(c -> {
SupplyChannel supplyChannel = new SupplyChannel();
supplyChannel.setChannelId(c.getChannelCode());//货道编号
supplyChannel.setCapacity(c.getMaxCapacity() - c.getCurrentCapacity());//补货量
supplyChannel.setSkuId(c.getSkuId());
supplyChannel.setSkuName(c.getSku().getSkuName());
supplyChannel.setSkuImage(c.getSku().getSkuImage());
return supplyChannel;
}).collect(Collectors.toList());
//构建补货协议数据
SupplyCfg supplyCfg=new SupplyCfg();
supplyCfg.setInnerCode(vmEntity.getInnerCode());
supplyCfg.setSupplyData(supplyChannelList);
supplyCfg.setMsgType("supplyTask");
try {
mqttProducer.send(TopicConfig.SUPPLY_TOPIC,2,supplyCfg);
XxlJobLogger.log("发送补货数据:"+ JsonUtil.serialize(supplyCfg));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
(2)修改SupplyJob的supplyJobHandler方法,调用sendSupplyTask方法
//扫描售货机
vmList.forEach( vm->{
XxlJobLogger.log("扫描售货机"+vm.getInnerCode());
int count = vmService.inventory(percent, vm); //缺货货道数量
if(count>0){
XxlJobLogger.log("售货机"+vm.getInnerCode()+"缺货");
//发送补货消息
vmService.sendSupplyTask(vm);//增加这句!!!!!!!!!!!!!!!!!
}
});
工单服务的配置,订阅server/task/supply主题
consumer:
consumerTopics: $queue/server/task/supply,$share/task/server/status
在工单服务项目lkd_production_service
中订阅消息并将处理该消息创建补货工单:
@Component
@ProcessType("supplyTask")
@Slf4j
public class SupplyTaskHandler implements MsgHandler {
@Autowired
private VMService vmService;
@Autowired
private TaskService taskService;
@Override
public void process(String jsonMsg) throws IOException {
try {
//1.解析协议内容
SupplyCfg supplyCfg = JsonUtil.getByJson(jsonMsg, SupplyCfg.class);
if(supplyCfg==null) return;
//2.找出被指派人
VendingMachineViewModel vm = vmService.getVMInfo(supplyCfg.getInnerCode());
Integer userId = taskService.getLeastUser(vm.getRegionId().intValue(), false);
//3.创建补货工单
TaskViewModel taskViewModel=new TaskViewModel();
taskViewModel.setAssignorId(userId);
taskViewModel.setCreateType(0);//创建类型
taskViewModel.setProductType(VMSystem.TASK_TYPE_SUPPLY);
taskViewModel.setInnerCode(supplyCfg.getInnerCode());
taskViewModel.setDesc("自动补货工单");
taskViewModel.setDetails( supplyCfg.getSupplyData().stream().map(c->{
TaskDetailsViewModel taskDetailsViewModel=new TaskDetailsViewModel();
taskDetailsViewModel.setChannelCode( c.getChannelId() );
taskDetailsViewModel.setExpectCapacity( c.getCapacity() );
taskDetailsViewModel.setSkuId(c.getSkuId());
taskDetailsViewModel.setSkuName(c.getSkuName());
taskDetailsViewModel.setSkuImage(c.getSkuImage());
return taskDetailsViewModel;
} ).collect(Collectors.toList()) ); //补货详情
taskService.createTask(taskViewModel);
} catch (Exception e) {
e.printStackTrace();
log.error( "创建自动补货工单出错"+e.getMessage() );
}
}
}
(1)启动售货机微服务和工单微服务
(2)在调度中心执行扫描售货机任务
上一小节我们已经实现了自动补货工单,但是在实际运营中,在系统中可能会有大量的售货机,如果将所有的售货机全部轮询一遍,并且为每一台需要补货的机器创建补货工单,可能是一个比较耗时的过程,因为这是一个串行的运算逻辑。为了缩短这个自动创建补货工单的总时间,就需要我们并行的去做这件事情,我们可以部署多份扫描售货机的程序,每一个节点只扫描一部分,大家加起来就是全部,这样就能充分利用起服务器集群的能力,把庞大的工作由原来的一个人拆解之后交给多个人去做了。我们可以使用XXL-JOB的集群分片广播来实现上述逻辑。
**分片广播任务:**执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务
(1)修改调度中心扫描售货机任务的配置,路由策略选择"分片广播"
(2)在售货机微服务中实现XXL-JOB中的任务的执行器,根据分片参数中的当前分片总数和当前执行器的分片索引从售货机表中进行分段切分扫描。如何将售货机表数据进行切分呢?可以有两种方法:
(1)修改调度中心扫描售货机任务的配置,路由策略选择"分片广播"
(2)修改SupplyJob的supplyJobHandler方法
/**
* 扫描所有运营状态的售货机
* @param param
* @return
* @throws Exception
*/
@XxlJob("supplyJobHandler")
public ReturnT<String> supplyJobHandler(String param) throws Exception{
//获取分片总数和当前分片索引
ShardingUtil.ShardingVO shardingVo = ShardingUtil.getShardingVo();
int numbers = shardingVo.getTotal(); //分片总数
int index = shardingVo.getIndex(); //当前分片索引
log.info("分片参数 当前分片索引{} 总分片数{}",index,numbers);
XxlJobLogger.log( "分片参数 当前分片索引"+index+" 总分片数"+numbers);
Integer percent = taskService.getSupplyAlertValue();
//查询所有运营状态的售货机
QueryWrapper<VendingMachineEntity> qw=new QueryWrapper<>();
qw.lambda()
.eq(VendingMachineEntity::getVmStatus, VMSystem.VM_STATUS_RUNNING)
.apply("mod(id,"+numbers+" ) = "+index); //对id取模
List<VendingMachineEntity> vmList = vmService.list(qw);
//扫描售货机
vmList.forEach( vm->{
XxlJobLogger.log("扫描售货机"+vm.getInnerCode());
int count = vmService.inventory(percent, vm); //缺货货道数量
if(count>0){
XxlJobLogger.log("售货机"+vm.getInnerCode()+"缺货");
//发送补货消息
vmService.sendSupplyTask(vm);
}
});
return ReturnT.SUCCESS;
}
(1)删除之前产生的补货工单(如果设备存在未处理的补货工单,将不会重新生成)
(2)启动三个售货机微服务、一个工单微服务
(3)在调度中心运行扫描售货机任务,观察日志输出。