基于spring的事件监听机制和redis队列实现按先后顺序处理用户请求

336 阅读3分钟

需求背景

有一个特别消耗CUP的接口,比如批量生成excel、批量生成图片等

如果不控制处理的频率,太多的用户请求打过来可能造成瞬间CPU拉满,影响其他用户的其他操作

思路

参考日常生活中的排队,当用户请求这个接口时,直接放入队列,等候处理,在数据库中记录一条处理日志,状态为处理中,给前端返回处理中,请求结束。

后台需要一个线程,不断的轮询队列中的任务,如果有队列里面有任务,就取出来处理,处理完后更新处理日志,然后继续下一条任务。

具体实现

任务队列

考虑到项目中已接入的中间件有redis,就使用redis队列来实现

使用redisTemplate.opsForList().leftPush(key, value)来向对列添加任务,左边进

使用redisTemplate.opsForList().rightPop(key, timeout, unit)来从队列取任务,右边出

rightPop() 方法用于将列表中最右边的元素弹出并返回。如果列表为空,则该方法会阻塞等待元素加入,直到超时或有新元素加入为止。

消费者(处理线程)

然后就是什么时候创建这个处理线程了,希望它能在项目启动的时候就创建好,然后就静静的等待任务来~

项目是基于spring的,在spring启动的时候有N多的事件发布,我们可以监听这些事件中的某些来实现,前提是仅且运行一次,还要考虑到具体的任务处理逻辑里面依赖了其他的Service,所以要在容器已经处理完所有的 bean之后再去创建。

符合这两个要求的就只有ContextRefreshedEvent了, ContextRefreshedEvent 事件是一个上下文级别的事件,表示容器已经处理完所有的 bean,可以使用了。当 Spring 容器完成其 bean 工厂的初始化后,会发布此事件。通常情况下,ContextRefreshedEvent 是由 WebApplicationContext 自动触发的。

代码(伪)

监听器

@Slf4j
@Component
public class BuildTaskListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {

        if (event.getApplicationContext().getParent() != null
                && event.getApplicationContext().getParent().getParent() != null) {//确保是第一次刷新,以防没内部显示的调用了容器刷新方法
            return;
        }


        new Thread(() -> {
            //创建消费者
            while (true) {
                try {
                 String taskId = (String) RedisUtil.rightPop(key);
                 if(taskId != null){
                      handleTask(taskId);
                 }
                
                } catch (Exception e) {
                    log.error(e.getMessage());
                }
            }

        }, "createConsumer").start();
    }


}

注意:这里是nwe 了一个Thread,然后一直轮询,所以在handleTask中抛出异常要处理,不能往外抛,否则线程会终止!

加入队列

public R doSomeThing(Integer param) {

'''
参数校验等逻辑
'''
    //添加记录
    Integer taskId = addTaskLog(param);

    if (ObjectUtils.isNotNull(taskId)) {

        // 将任务添加到队列中
        Long rightPushResult = redisTemplate.opsForList().leftPush(QUEUE_NAME, taskId);
        if (rightPushResult > 0) {
            result = Boolean.TRUE;
        }

    } else {
        log.error("添加记录失败");
    }

    return result;
}

处理任务

public void handleTask(Integer taskId) {

   ''''
   Boolean result = 处理任务
   ''''
   
   根据result更新处理结果

}

完善

如果任务失败可以在日志中记录失败原因,以便后续处理,还可以提供重试接口