Redis做消息队列使用

背景:

  很多场景需要实效性不是很高,可以异步操作,java自带的Async异步操作或者新创建线程池进行异步会额外占用内存,或者使用RabbitMQ和RocketMQ也会带来额外操作,在同一系统使用这些MQ也会站占用消耗其他资源,所以选择使用Redis队列来实现

场景:

  比如发布评论消息,需要进行内容审核校验,需要修改主贴的回复数量,需要添加被回复人的消息通知,如果是除了一级回复添加二三级回复需要更新一级回复的回复数量,等等,一系列的操作全部同步,客户端等待响应,耗费的时间也是相对比较长的,所以将添加回复之后其他操作放入队列完成。

Redis的list(列表)数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用lpop/rpop操作来出队列

封装代码如下:

public interface IQueue<E> {
    /**
     * 入队操作
     * @param e 入队数据
     * @return
     */
    boolean rightPush(E e);

    /**
     * 出队
     * @return 一条队列数据
     */
    E leftPop();

    /**
     * 队列大小
     * @return
     */
    long size();
}

 

  

public class RedisQueue implements IQueue<String> {

    /**
     * redis 模板
     */
    private RedisTemplate<String, String> redisQueueTemplate;

    /**
     * redis key
     */
    private String key;

    public RedisQueue(String key, RedisTemplate<String, String> redisQueueTemplate) {
        this.key = key;
        this.redisQueueTemplate = redisQueueTemplate;
    }

    @Override
    public boolean rightPush(String s) {
        return redisQueueTemplate.opsForList().rightPush(key, s) > 0;
    }

    @Override
    public String leftPop() {
        return redisQueueTemplate.opsForList().leftPop(key);
    }

    @Override
    public long size() {
        return redisQueueTemplate.opsForList().size(key);
    }
}

在添加回复完之后进行放入队列操作,

实例话接口IQueue,这里使用redisTemplate的list进行右侧入队操作调用rightPush()

public abstract class  AbstractQueueWorkerService implements InitializingBean {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractQueueWorkerService.class);

    protected volatile boolean monitorStarted = false;

    protected volatile boolean monitorShutDowned = false;
    //默认休眠时间 500毫秒
    private static final int DEFAULT_SLEEP_TIME = 500;

    private ExecutorService executorService;

    private static final int DEFAULT_THREAD_NUM = 1;
    /**
     * 线程数量(默认1)
     */
    private int threadNum = DEFAULT_THREAD_NUM;

    private int threadSheepTime = DEFAULT_SLEEP_TIME;

    /**
     * 线程名称
     */
    protected String threadName;

    /**
     * 需要监控的队列
     */
    protected IQueue<String> monitorQueue;

    public void setQueueTaskConf(String threadName , int threadNum, IQueue<String> monitorQueue,int threadSheepTime ){
        this.threadName = threadName;
        this.threadNum = threadNum;
        this.monitorQueue = monitorQueue;
        this.threadSheepTime = threadSheepTime;
    }

    public void setThreadNum(int threadNum) {
        this.threadNum = threadNum;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }

    public void setThreadSheepTime(int threadSheepTime) {
        this.threadSheepTime = threadSheepTime;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        executorService = Executors.newFixedThreadPool(threadNum);

        for (int i = 0; i < threadNum; i++) {
            final int num = i;
            executorService.execute(() -> {
                Thread.currentThread().setName(threadName +"["+num+"]");
                while (!monitorShutDowned) {
                    String value = null;
                    try {
                        value = beforeExecute();
                        if (StringUtils.isNotEmpty(value)) {
                            if(LOG.isDebugEnabled()){
                                LOG.debug("Monitor Thread[" + Thread.currentThread().getName() + "], pop from queue,value = {}", value);
                            }
                            boolean success = execute(value);
                            // 失败重试
                            if (!success) {
                                success = retry(value);
                                if (!success) {
                                    LOG.warn("Monitor Thread[" + Thread.currentThread().getName() + "] execute Failed,value = {}", value);
                                    failProcess(value);
                                }
                            } else {
                                if(LOG.isDebugEnabled()){
                                    LOG.debug("Monitor Thread[" + Thread.currentThread().getName() + "]:execute successfully!values = {}", value);
                                }
                            }
                        } else {
                            if(LOG.isDebugEnabled()){
                                LOG.debug("Monitor Thread[" + Thread.currentThread().getName() + "]:monitorThreadRunning = {}", monitorStarted);
                            }
                            Thread.sleep(threadSheepTime);
                        }
                    } catch (Exception e) {
                        LOG.error("Monitor Thread[" + Thread.currentThread().getName() + "] execute Failed,value = " + value, e);
                    }
                }
                LOG.info("Monitor Thread[" + Thread.currentThread().getName() + "] Completed...");
            });
        }
        LOG.info("thread pool is started...");
    }

    /**
     * 操作队列取数据
     *
     * @return 队列数据
     */
    public String beforeExecute() {
        return monitorQueue.leftPop();
    }

    /**
     * 执行业务逻辑
     */
    public abstract boolean execute(String value);

    /**
     * 重试
     *
     * @param value 队列内容
     * @return true:成功,false:失败
     */
    protected boolean retry(String value) {
        LOG.info("job retry, value: {}",value);
        return execute(value);
    }

    /**
     * 失败处理
     * @param value
     */
    protected void failProcess(String value){

    }

    protected void shutdown() {
        executorService.shutdown();
        monitorShutDowned = true;
        LOG.info("thread pool is shutdown...");
    }
}

封装类继承InitializingBean,或者实现ApplicationContext,或者使用方法注解@PostConstruct待上下文加载完成进行消息的消费,出队列操作

实现execute方法可使用redisTemplate的list.leftPop()进行左侧取出队列

@Slf4j
public class MonitorReplyQueueService extends AbstractQueueWorkerService {

    @Autowired
    private ReplyService replyService;

    @Override
    public boolean execute(String value) {
        log.info("MonitorReplyQueueService execute value: {}", value);
        replyService.handleReplyQueue(value);
        return true;
    }

    public MonitorReplyQueueService(IQueue<String> redisQueue, String threadName, int threadNum,
                                      int threadSleepTime) {
        setQueueTaskConf(threadName, threadNum, redisQueue, threadSleepTime);
    }

}

这里可以自己取出队列数据大小来进行线程池的动态扩容,可以保证消息快速消费完。

> 队列的数据大多数时间是空的怎么办

 

客户端一直通过pop来取队列的数据,然后进行处理,处理完了接着再获取消息,再进行处理,如此循环往复,这便是作为队列消费者的客户端的生命周期

可是如果队列空了,客户端就会陷入pop的死循环,不停的pop,没有数据,再pop,还是没有数据,这就是浪费生命的空轮循,空轮循不但提高了客户端的CPU,也拉高了Redis的QPS,空轮循的客户端连接有几十个,Redis的满查询可能会明显增多。面对这种情况我们需要合理的设置线程数,设置sleep来解决这个问题,让线程休眠一会,Thread.sleep(1),这样客户端的cpu和Redis的QPS会降低很多.

 

posted @ 2022-06-11 21:09  木马不是马  阅读(163)  评论(0编辑  收藏  举报