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会降低很多.