Redis有序队列+事件 实现异步执行业务

526 阅读4分钟

任务日志记录

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

image-20240529135454572.png

业务流程图:

image-20240529112110091.png

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

image-20240529114347784.png

具体代码实现(主要体现 事件结合redis队列使用,不过多涉及具体业务代码)

事件实体

@ToString
@Getter
@Setter
public class FlightJobAdjustEvent extends ApplicationEvent {
​
    /**
     * 算法调度的结果id
     */
    @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工具进行调整

redis消息队列.png

发送事件

        // 发送算法变更信息
        applicationContext.publishEvent(new FlightJobAdjustEvent(this, id,
                TaskChangeNoticeTypeEnum.AUTO, AssignModeEnum.AUTO, oldList, newList, isChange, data.getDataSource()));

Redis监听器(取队列中最左边的值出来执行业务)

   // jdk21的虚拟线程
   ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
​
    public void listenLog() {
        ObjectMapper objectMapper = new ObjectMapper();
        
        // 忽略json中出现的非实体的数据
        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)) {
​
                        // 备份日志事件,队列长度为10
                        TaskHelper.setTaskLogQueueBak(eventDataObj);
​
                        FlightJobAdjustEvent eventData = objectMapper.readValue(eventDataObj.toString(), FlightJobAdjustEvent.class);
                        
                        // 加锁执行业务
                        handleEventData(eventData);
​
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                    if (isRedisConnectionException(e)) {
​
                        // redis连接超时,跳出循环
                        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());
    }

image-20240529115121803.png

image-20240529115704412.png

调整留痕

根据任务id查询日志中最新的一条 类型是调整的,或者类型是冲突,且状态是已解决的数据。

结语: 如果有疑问,或者更好的方案,欢迎评论区讨论。

---------------------------------------------------------

更新 (2024/06/18)

优化:采用redisson的阻塞式获取元素,避免无限循环获取数据。


@Autowired
private RedissonClient redissonClient;

public void listenLog() {

    executor.execute(() -> {
    
        // 获取一个阻塞式的 Redis 队列。
        RBlockingQueue<String> queue = redissonClient.getBlockingQueue(RedisConstant.REDIS_TASK_LOG_QUEUE, JsonJacksonCodec.INSTANCE);

        while (true) {
            try {

                log.info("检测任务日志队列是否阻塞");

                // 阻塞式获取队列中的第一个元素,队列为空会阻塞在这里
                String eventDataObj = queue.take();

                if (eventDataObj != null) {

                    // 备份日志事件,长度为10
                    TaskHelper.setTaskLogQueueBak(eventDataObj);

                    FlightJobAdjustEvent eventData = objectMapper.readValue(eventDataObj, FlightJobAdjustEvent.class);

                    handleEventData(eventData);
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                if (isRedisConnectionException(e)) {

                    // redis连接超时,跳出循环
                    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的加解锁、锁续租(可以加长锁的时间,不使用续租)

/**
 * 处理事件数据
 * @param eventData
 * @throws InterruptedException
 */
@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); // 每30秒续租一次锁
                            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;
    }
}