任务日志记录
前情提要:我参与的智能调度系统,任务涉及到算法调度,重新预排人员,在人员任务甘特图维度会出现任务调整,所以有一个任务日志表记录任务冲突跟任务调整

业务流程图:

日志表:表中包含初始分配信息、调整分配信息

具体代码实现(主要体现 事件结合redis队列使用,不过多涉及具体业务代码)
事件实体
@ToString
@Getter
@Setter
public class FlightJobAdjustEvent extends ApplicationEvent {
@JsonProperty("schedulerJobId")
private String schedulerJobId;
@JsonProperty("dataSource")
private String dataSource;
@JsonProperty("oldTaskInfos")
private List<TaskScheduleInfo> oldTaskInfos = new ArrayList<>();
@JsonProperty("newTaskInfos")
private List<TaskScheduleInfo> newTaskInfos = new ArrayList<>();
@JsonProperty("noticeType")
private TaskChangeNoticeTypeEnum noticeType;
@JsonProperty("assignMode")
private AssignModeEnum assignMode;
@JsonProperty("isChange")
private Boolean isChange;
public FlightJobAdjustEvent() {
super(new Object());
}
public FlightJobAdjustEvent(Object source) {
super(source);
}
public FlightJobAdjustEvent(Object source, String schedulerJobId, TaskChangeNoticeTypeEnum noticeType,
AssignModeEnum assignMode,
List<TaskScheduleInfo> oldTaskInfos, List<TaskScheduleInfo> newTaskInfos) {
super(source);
this.schedulerJobId = schedulerJobId;
this.assignMode = assignMode;
this.noticeType = noticeType;
this.oldTaskInfos = oldTaskInfos;
this.newTaskInfos = newTaskInfos;
}
public FlightJobAdjustEvent(Object source, TaskChangeNoticeTypeEnum noticeType) {
super(source);
this.noticeType = noticeType;
}
public FlightJobAdjustEvent(Object source, String schedulerJobId, TaskChangeNoticeTypeEnum noticeType,
AssignModeEnum assignMode,
List<TaskScheduleInfo> oldTaskInfos, List<TaskScheduleInfo> newTaskInfos, Boolean isChange, String dataSource) {
super(source);
this.schedulerJobId = schedulerJobId;
this.assignMode = assignMode;
this.noticeType = noticeType;
this.oldTaskInfos = oldTaskInfos;
this.newTaskInfos = newTaskInfos;
this.isChange = isChange;
this.dataSource = dataSource;
}
}
事件监听(将事件实体存到有序队列)
@EventListener(FlightJobAdjustEvent.class)
@Transactional(rollbackFor = Exception.class)
public void eventAdjustListener(FlightJobAdjustEvent event) {
log.info("日志填装进有序队列------------------{}", event);
redisUtil.leftPush(RedisConstant.REDIS_TASK_LOG_QUEUE, JSONUtil.toJsonStr(event));
}
我使用的是封装的redisUtil,具体的方法可以根据使用的redis工具进行调整

发送事件
applicationContext.publishEvent(new FlightJobAdjustEvent(this, id,
TaskChangeNoticeTypeEnum.AUTO, AssignModeEnum.AUTO, oldList, newList, isChange, data.getDataSource()));
Redis监听器(取队列中最左边的值出来执行业务)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public void listenLog() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
executor.execute(() -> {
while (true) {
Object eventDataStr = null;
try {
String eventDataObj = (String) redisUtil.rightPop(RedisConstant.REDIS_TASK_LOG_QUEUE);
if (ObjUtil.isNotNull(eventDataObj)) {
TaskHelper.setTaskLogQueueBak(eventDataObj);
FlightJobAdjustEvent eventData = objectMapper.readValue(eventDataObj.toString(), FlightJobAdjustEvent.class);
handleEventData(eventData);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
if (isRedisConnectionException(e)) {
break;
}
}
}
});
}
redis连接超时异常,跳出循环
private boolean isRedisConnectionException(Exception e) {
return e.getMessage().contains("Unable to connect to Redis") || e.getCause() instanceof RedisConnectionFailureException || e.getMessage().contains("Connection closed");
}
给事件执行加锁
@Async()
public void handleEventData(FlightJobAdjustEvent eventData) {
String lockName = "wsbJobConflictLog_" + DateUtil.currentSeconds();
String unlockKey = UUID.randomUUID().toString();
try {
if (redisUtil.getLock(lockName, unlockKey, 60L)) {
try {
log.debug("开始处理日志业务");
wsbJobConflictLogService.logBusiness(eventData);
} finally {
redisUtil.releaseLock(lockName, unlockKey);
}
} else {
log.warn("获取分布式锁失败:" + lockName);
}
} catch (Exception e) {
log.error("处理日志业务出错:" + e.getMessage(), e);
throw e;
}
}
业务(不贴代码,只贴业务流程)
@Transactional(rollbackFor = Exception.class)
public void logBusiness(FlightJobAdjustEvent event) {
Date now = new Date();
long beginTime = DateUtil.currentSeconds();
log.info("校验调整");
adjustBusiness(event, now);
log.info("校验冲突");
conflictBusiness(event);
long endTime = DateUtil.currentSeconds();
log.info("校验耗时:{},ScheduleId:{}", endTime - beginTime, event.getSchedulerJobId());
}


调整留痕
根据任务id查询日志中最新的一条 类型是调整的,或者类型是冲突,且状态是已解决的数据。
结语: 如果有疑问,或者更好的方案,欢迎评论区讨论。
---------------------------------------------------------
更新 (2024/06/18)
优化:采用redisson的阻塞式获取元素,避免无限循环获取数据。
@Autowired
private RedissonClient redissonClient;
public void listenLog() {
executor.execute(() -> {
RBlockingQueue<String> queue = redissonClient.getBlockingQueue(RedisConstant.REDIS_TASK_LOG_QUEUE, JsonJacksonCodec.INSTANCE);
while (true) {
try {
log.info("检测任务日志队列是否阻塞");
String eventDataObj = queue.take();
if (eventDataObj != null) {
TaskHelper.setTaskLogQueueBak(eventDataObj);
FlightJobAdjustEvent eventData = objectMapper.readValue(eventDataObj, FlightJobAdjustEvent.class);
handleEventData(eventData);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
if (isRedisConnectionException(e)) {
break;
}
}
}
});
}
redisson存储数据
@Autowired
private RedissonClient redissonClient;
private RQueue<String> taskLogQueue;
@PostConstruct
public void init() {
this.taskLogQueue = redissonClient.getQueue(RedisConstant.REDIS_TASK_LOG_QUEUE, JsonJacksonCodec.INSTANCE);
}
taskLogQueue.add(JSONUtil.toJsonStr(event));
更新(6月28号)
优化 handleEventData的加解锁、锁续租(可以加长锁的时间,不使用续租)
@Async()
public void handleEventData(FlightJobAdjustEvent eventData) throws InterruptedException {
String lockName = "wsbJobConflictLog_" + IdUtil.getSnowflakeNextIdStr();
String lockOwner;
final int maxRenewals = 3;
final AtomicInteger renewalCount = new AtomicInteger(0);
try {
lockOwner = redisUtil.getLockOwner(lockName, 60L);
if (ObjUtil.isNotEmpty(lockOwner)) {
try {
Future<?> future = Executors.newVirtualThreadPerTaskExecutor().submit(() -> {
try {
if (!Thread.currentThread().isInterrupted()) {
log.warn("任务被中断,停止续租:" + lockName);
return;
}
while (Thread.currentThread().isInterrupted() && renewalCount.incrementAndGet() <= maxRenewals) {
Thread.sleep(30000);
redisUtil.renewLock(lockName, lockOwner, 60L);
}
log.warn("达到最大续租次数,停止续租:" + lockName);
} catch (Exception e) {
log.error("续租锁失败:" + e.getMessage(), e);
}
});
log.debug("开始处理日志业务");
wsbJobConflictLogService.logBusiness(eventData);
if (ObjUtil.isNotEmpty(future)) {
log.debug("关闭续约");
future.cancel(true);
}
} finally {
redisUtil.releaseLock(lockName, lockOwner);
}
} else {
log.warn("正在加锁中:" + lockName);
}
} catch (Exception e) {
log.error("处理日志业务出错:" + e.getMessage(), e);
throw e;
}
}