第四节 List作为消息中间件

一、基本依据

        Redis的List是一个队列。对于队列这种数据结构来说,有两个非常重要的特性。可左进右出,亦可右进左出。那么,基于这种特点,就能够通过Redis的List实现基本的消息中间件。

        从具体的代码上来看,可以先把一些数据使用 leftPush方法加入Redis的队列中。再通过同一key从Redis中指定队列中取出来,这个方法叫做rightPop

        下面的代码展示了如何将消息加入Redis的指定的List中。重点关注leftPush方法的调用。

   //核心:左进消息
   public void addNotice(Notice notice) {
        if (notice != null) {
            getListOperations().leftPush(Constant.NOTICE_KEY, notice);
        }
    }

   private ListOperations<String, Notice> getListOperations() {
        return redisTemplate.opsForList();
    }

        另一方面,使用定时任务一直监听这个List。类似与RabbitMQ里面的监听Queue。重点关注rightPop方法的调用。

    @Scheduled(cron = "*/10 * * * * ?")
    @Async
    public void listen() {
        LOGGER.info("监听消息ing");

        Notice notice = getListOperations().rightPop(Constant.NOTICE_KEY);
        while (notice != null) {

            sendMail(notice);
            //在队列中从右边弹出第一个数据
            notice = getListOperations().rightPop(Constant.NOTICE_KEY);
        }
    }

二、定时任务阻塞问题思考

        这个是比较重要的问题。如果使用定时任务,如何保证多个定时任务之间使用不同线程。在实际工作中,曾经就出现过这种情况。我同事的定时任务耗时过长,导致我的定时任务一直得不到执行。测试以为我的定时任务出了问题,并且日志还没有打印出任何错误。实际上我的定时任务压根就没得到执行的机会。其问题根源在于多个定时任务使用了同一个线程。

        解决方案:解决SpringBoot多定时任务使用同一线程问题

        第一步:在系统中配置大名鼎鼎的线程池ThreadPoolTaskExecutor。

package com.tyzhou;

import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.util.concurrent.Executor;

@Component
public class ThreadPoolConfig {

    @Bean(value = "threadPoolTaskExecutor")
    public Executor getExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
        executor.setCorePoolSize(5);
        //线程池维护线程的最大数量
        executor.setMaxPoolSize(10);
        //允许的空闲时间,当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        executor.setKeepAliveSeconds(60);
        //缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
        executor.setQueueCapacity(10);
        /**
         * 拒绝task的处理策略
         * CallerRunsPolicy使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
         * AbortPolicy丢掉这个任务并且抛出
         * DiscardPolicy线程池队列满了,会直接丢掉这个任务并且不会有任何异常
         * DiscardOldestPolicy队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
         */
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

}

        第二步:在定时任务类上使用@EnableAsyn注解,并在具体定时任务方法上使用@Async注解。那么,这两个定时任务在同一时刻触发,也不会使用同一个线程。

@Component
@EnableScheduling
@EnableAsync
public class NoticeListener ...

    @Scheduled(cron = "*/10 * * * * ?")
    @Async
    public void listen() {
        LOGGER.info("监听消息ing");
    }

    @Scheduled(cron = "*/10 * * * * ?")
    @Async
    public void test() {
        LOGGER.info("另外一个定时任务在执行ing....");
    }

三、定时任务业务复杂情况

         这里还有一个十分重要的问题没有解决。如果只是单单的解决了多个定时任务使用同一个线程的问题,这还不够。另外一个令人头大的问题是。如果定时任务的业务很复杂,如何再优化。我在实际项目中遇到的问题就是,仅仅在测试环境一千个学生,定时任务执行的时间就超过一小时。这严重影响当前系统的内存,测试小姐姐苦不堪言。

         经过仔细排查,最耗时的操作太平常,代码类似于下面这种。每个学生都占一次循环,且处理每个学生的时间都要花费很多。

for(User user : userList){
   service.handleStu(user, otherArg);
}

        下面是优化后的代码。使用了大名顶顶的ExecutorService。可参考:线程池ThreadPoolExecutor的使用

        优化后的代码思路

         第一步:将以前的处理业务,封装到一个线程中,实现Callable接口即可。

package com.tyzhou.mail.listener;

import com.tyzhou.mail.MailService;
import com.tyzhou.mail.modol.Notice;
import com.tyzhou.redis.User;
import lombok.AllArgsConstructor;

import java.util.concurrent.Callable;

@AllArgsConstructor
public class MailThread implements Callable<Boolean> {

    private MailService mailService;
    private Notice notice;
    private User user;

    @Override
    public Boolean call() throws Exception {
        mailService.sendMail(notice, user);
        return true;
    }
}

         第二步:封装任务并使用使用ExecutorService来执行任务,以此减少定时任务执行时间。

  public void sendMail(Notice notice) {
       try {
            //构造线程池
            ExecutorService pool = Executors.newFixedThreadPool(30);
            //封装要被处理的任务到集合里面
            Collection<MailThread> taskList = new ArrayList<>();
            for (User user : users) {
                //构造任务,MailThread实现Callable接口
                taskList.add(new MailThread(mailService, notice, user));
            }
            //使用线程池来执行任务
            pool.invokeAll(taskList);

        } catch (Exception e) {
            LOGGER.error("发送邮件出现异常:", e.fillInStackTrace());
        }
    }

          大致执行效果如下。红色框表明不同定时任务实现了异步。蓝色框表明,使用ExecutorService实现了多个任务使用了多个线程来处理。

阅读更多 

        跟着大宇学Redis--------目录帖

 

posted @ 2022-07-17 12:14  小大宇  阅读(36)  评论(0编辑  收藏  举报