结合Redis实现自定义线程池拒绝策略

453 阅读3分钟

前段时间在面试过程中被问到了这样一个场景问题:有一个业务场景,需要向多个用户发送手机短信,如果需要同时发送大量的短信你会如何高效实现?对于这个问题当时首先想到的就是使用MQ或者线程池。当提到线程池时,面试官又问道:线程池处理的任务过多,会触发拒绝策略对吧(此处点了点头,心想这又要出什么幺蛾子),如果使用CallerRunsPolicy策略会由提交任务的线程执行,对于这点你有什么优化手段吗?对于这个问题,好在当时再看TL课堂的并发课时,里面提了一嘴说可以自定义拒绝策略并将任务序列化到缓存中

虽然提到了这个优化点,但是在实际项目中并没有实践过,所以在空闲时间来手动实现这个场景。首先确认分别有哪几个步骤:

  1. 自定义一个Task任务类,便于序列化。
  2. 自定义线程池的拒绝策略,将任务缓存到redis,这样可以避免主线程执行任务
  3. 线程池中使用自定义的拒绝策略
  4. 定时或循环从redis中拉取任务执行

自定义任务类

自定义一个RejectTask类并包含手机号和短信内容字段,该类需要实现Runnable并重写run方法。

@Slf4j
@Data
public class RejectTask implements Runnable {

    //手机号
    private String mobile;

    // 短信内容
    private String msg;


    public RejectTask(String mobile, String msg) {
        this.mobile = mobile;
        this.msg = msg;
    }

    /**
     * 最终发送短信的方法
     */
    @Override
    public void run() {
        log.info("====发送短信内容:{},至{}", this.msg, this.mobile);
    }
}

自定义拒绝策略

定义一个MyThreadRejectedExecutionHandler类,该类需要实现RejectedExecutionHandler接口并重写rejectedExecution方法。在rejectedExecution方法中,将被拒绝的任务由主线程序列化到redis中,所以主线程不会再执行该任务了,相对来说效率会好那么一些。

/**
 * 自定义线程池拒绝策略
 */
@Slf4j
@Component
public class MyThreadRejectedExecutionHandler implements RejectedExecutionHandler {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 线程池是否被关闭
        if (executor.isShutdown()) {
            return;
        }
        log.info("==========>1. 执行自定义线程池拒绝策略<==========");
        //获取被拒绝的任务
        RejectTask task = (RejectTask)r;
        String str = JSONUtil.toJsonStr(task);
        //将任务缓存到redis
        stringRedisTemplate.opsForList().leftPush("thread::rejected::list", str);
        log.info("==========>2. 任务序列化到redis <========== {}",task.getMobile());
    }
}

线程池中使用自定义的拒绝策略

@Configuration
public class ThreadConfig {

    @Resource
    private MyThreadRejectedExecutionHandler myThreadRejectedExecutionHandler;

    @Bean(name = "defExecutor")
    public ThreadPoolExecutor executor() {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                5,
                10,
                20L,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(5),
                //自定义的拒绝策略
                myThreadRejectedExecutionHandler);
        return poolExecutor;
    }
}

获取任务

前面拒绝的任务存入的缓存,那么这个时候就需要从缓存中读取任务并执行。一般情况下可以使用定时任务来定期执行这些任务,这里为了方便就采用的while循环的方式。主要的实现方式就是读取缓存中的任务,如果线程池的任务队列还是满的情况下,那么这时就直接执行该任务,否则还是把这个任务交给原线程池执行(个人理解该策略只是一个辅助作用,如果线程池有空闲的情况下还是由线程池本身来执行比较合适,如果觉得不合理也可以不需要)。

@Slf4j
@Order(value = Integer.MAX_VALUE)
@Component
public class RejectedThreadLister {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource(name = "defExecutor")
    private ThreadPoolExecutor executor;

    private int maxNumber = 20;//最大空循环次数

    @PostConstruct
    public void rejectedThreadLister() {
        new Thread(() -> {
            int count = 0;//当前空循环次数
            while (!executor.isShutdown()) {
               // 线程池的任务队列
                ArrayBlockingQueue<Runnable> queue = (ArrayBlockingQueue<Runnable>) executor.getQueue();
                // 从缓存中获取被拒绝的任务
                String taskJson = stringRedisTemplate.opsForList().leftPop("thread::rejected::list");
                if (StrUtil.isNotEmpty(taskJson)) {
                    count = 0;
                    RejectTask task = JSONUtil.toBean(taskJson, RejectTask.class);
                    log.info("==========> redis中获取到任务 <========== {}",task.getMobile());
                    // 如果任务队列满载,那么直接执行该任务
                    if (queue.size() == 5) {
                        task.run();
                    } else {
                        try {
                           //如果任务队列有空闲,那么继续把任务交给线程池执行(也可以不需要)
                            queue.put(task);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } else {
                    count++;
                    // 如果多次循环后并没有拒绝的任务,说明任务此时数据量并不大,那么就可以减少循环次数
                    if (count >= maxNumber) {
                        try {
                            Thread.sleep(10000L);
                            count = 0;
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }

            }
        }, "rejected-lister").start();
    }
}

测试

测试代码相对比较随意,方式不固定,只要任务数能够触发拒绝策略即可。

@Slf4j
@RestController
public class TestController {

    @Resource(name = "defExecutor")
    private ThreadPoolExecutor executor;

    @GetMapping("/test")
    public void test(@RequestParam("taskCount") int taskCount){
        for (int i = 0; i < taskCount; i++) {
            executor.execute(new RejectTask(RandomUtil.randomNumbers(11),"测试测试"));
        }
    }

}

通过日志输出结果可以看到,这个策略确实是有效的。如果你有其他方法,也可以进行讨论。 image.png

image.png