转载声明:
先我们要知道使用队列的目的是什么?一般情况下,如果是一些及时消息的处理,并且处理时间很短的情况下是不需要使用队列的,直接阻塞式的方法调用就可以了。但是,如果在消息处理的时候特别费时间,这个时候如果有新的消息来了,就只能处于阻塞状态,造成用户等待。这个时候在项目中引入队列是十分有必要的。当我们接受到消息后,先把消息放到队列中,然后再用新的线程进行处理,这个时候就不会有消息的阻塞了。下面就跟大家介绍两种队列的使用,一种是基于内存的,一种是基于数据库的。
首先,我们来看看基于内存的队列。在Java的并发包中已经提供了BlockingQueue的实现,比较常用的有ArrayBlockingQueue和LinkedBlockingQueue,前者是以数组的形式存储,后者是以Node节点的链表形式存储。至于数组和链表的区别这里就不多说了。
BlockingQueue 队列常用的操作方法:
1.往队列中添加元素: add(), put(), offer()
2.从队列中取出或者删除元素: remove() element() peek() poll() take()
每个方法的说明如下:
offer()方法往队列添加元素如果队列已满直接返回false,队列未满则直接插入并返回true;
add()方法是对offer()方法的简单封装.如果队列已满,抛出异常new IllegalStateException("Queue full");
put()方法往队列里插入元素,如果队列已经满,则会一直等待直到队列为空插入新元素,或者线程被中断抛出异常.
remove()方法直接删除队头的元素:
peek()方法直接取出队头的元素,并不删除.
element()方法对peek方法进行简单封装,如果队头元素存在则取出并不删除,如果不存在抛出异常NoSuchElementException()
poll()方法取出并删除队头的元素,当队列为空,返回null;
take()方法取出并删除队头的元素,当队列为空,则会一直等待直到队列有新元素可以取出,或者线程被中断抛出异常
offer()方法一般跟pool()方法相对应, put()方法一般跟take()方法相对应.日常开发过程中offer()与pool()方法用的相对比较频繁.
下面用一个例子来看看是怎么使用的。
import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class UserTask { //队列大小 private final int QUEUE_LENGTH = 10000*10; //基于内存的阻塞队列 private BlockingQueuequeue = new LinkedBlockingQueue (QUEUE_LENGTH); //创建计划任务执行器 private ScheduledExecutorService es = Executors.newScheduledThreadPool(1); /** * 构造函数,执行execute方法 */ public UserTask() { execute(); } /** * 添加信息至队列中 * @param content */ public void addQueue(String content) { queue.add(content); } /** * 初始化执行 */ public void execute() { //每一分钟执行一次 es.scheduleWithFixedDelay(new Runnable(){ public void run() { try { String content = queue.take(); //处理队列中的信息。。。。。 System.out.println(content); } catch (InterruptedException e) { e.printStackTrace(); } } }, 0, 1, TimeUnit.MINUTES); } }
以上呢,就是基于内存的队列的介绍,基于内存的队列,队列的大小依赖于JVM内存的大小,一般如果是内存占用不大且处理相对较为及时的都可以采用此种方法。如果你在队列处理的时候需要有失败重试机制,那么用此种队列就不是特别合适了。下面就说说基于数据库的队列。
基于数据库的队列,很好理解,就是接收到消息之后,把消息存入数据库中,设置消费时间、重试次数等,再用新的线程从数据库中读取信息,进行处理。首先来看看数据库的设计。
代码示例如下:
/** * 批量获取 可以消费的消息 * 先使用一个时间戳将被消费的消息锁定,然后再使用这个时间戳去查询锁定的数据。 * @param count * @return */ public ListfindActiveQueueNew(int count) { //先去更新数据 String locker = String.valueOf(System.currentTimeMillis())+random.nextInt(10000); int lockCount = 0; try { //将status为1的更新为3,设置locker,先锁定消息 lockCount = queueDAO.updateActiveQueue(PayConstants.QUEUE_STATUS_LOCKED, PayConstants.QUEUE_STATUS_ACTIVE, count, locker); } catch (Exception e) { logger.error( "QueueDomainRepository.findActiveQueueNew error occured!" + e.getMessage(), e); throw new TuanRuntimeException( PayConstants.SERVICE_DATABASE_FALIURE, "QueueDomainRepository.findActiveQueue error occured!", e); } //如果锁定的数量为0,则无需再去查询 if(lockCount == 0){ return null; } //休息一会在再询,防止数据已经被更改 try { Thread.sleep(1); } catch (Exception e) { logger.error("QueueDomainRepository.findActiveQueue error sleep occured!" + e.getMessage(), e); } List activeList = null; try { activeList = queueDAO.getByLocker(locker); } catch (Exception e) { logger.error("QueueDomainRepository.findActiveQueue error occured!" + e.getMessage(), e); throw new TuanRuntimeException( PayConstants.SERVICE_DATABASE_FALIURE, "QueueDomainRepository.findActiveQueue error occured!",e); } return activeList; }
获取到消息之后,还需要再判断消息是否合法,如是否达到最大消费次数,消息是否已被成功消费,等,判断代码如下:
/** * 验证队列modle 的合法性 * * @param model * @return boolean true,消息还可以消费。false,消息不允许消费。 */ public boolean validateQueue(final QueueModel model){ int consumeCount = model.getConsumeCount(); if (consumeCount >= PayConstants.QUEUE_MAX_CONSUME_COUNT) { //消费次数超过了最大次数 return false; } int consumeStatus = model.getConsumeStatus(); if(consumeStatus == PayConstants.QUEUE_STATUS_CONSUMER_SUCCESS){ //消息已经被成功消费 return false; } QueueStatusEnum queueStatusEnum = model.getQueueStatusEnum(); if(queueStatusEnum == null || queueStatusEnum != QueueStatusEnum.LOCKED){ //消息状态不正确 return false; } String jsonData = model.getJsonData(); if(StringUtils.isEmpty(jsonData)){ //消息体为空 return false; } return true; }
消息处理完毕之后,根据消费结果修改数据库中的状态。
public void consume(boolean isDelete, Long consumeMinTime, String tradeNo,int consumeCount) { QueueDO queueDO = new QueueDO(); if (!isDelete) { //已经到了做大消费次数,消息作废 不再处理 if (consumeCount >= PayConstants.QUEUE_MAX_CONSUME_COUNT) { //达到最大消费次数的也设置为消费成功 queueDO.setConsumeStatus(PayConstants.QUEUE_STATUS_CONSUMER_SUCCESS); queueDO.setStatus(PayConstants.QUEUE_STATUS_CANCEL); } else { queueDO.setConsumeStatus(PayConstants.QUEUE_STATUS_CONSUMER_FAILED); //设置为可用状态等待下次继续发送 queueDO.setStatus(PayConstants.QUEUE_STATUS_ACTIVE); } } else { //第三方消费成功 queueDO.setConsumeStatus(PayConstants.QUEUE_STATUS_CONSUMER_SUCCESS); queueDO.setStatus(PayConstants.QUEUE_STATUS_DELETED); } queueDO.setNextConsumeTime(consumeMinTime == null ? QueueRuleUtil .getNextConsumeTime(consumeCount) : consumeMinTime); if (StringUtils.isNotBlank(tradeNo)) { queueDO.setTradeNo(tradeNo); } long now = System.currentTimeMillis(); queueDO.setUpdateTime(now); queueDO.setLastConsumeTime(now); queueDO.setConsumeCount(consumeCount); queueDO.setQueueID(id); setQueueDOUpdate(queueDO); }
下次消费时间的计算如下:根据消费次数计算,每次消费存在递增的时间间隔。
/** * 队列消费 开始时间 控制 */ public class QueueRuleUtil { public static long getNextConsumeTime(int consumeCount) { return getNextConsumeTime(consumeCount, 0); } public static long getNextConsumeSecond(int consumeCount) { return getNextConsumeTime(consumeCount, 0); } public static long getNextConsumeTime(int cousumeCount, int addInteval) { int secends = getNextConsumeSecond(cousumeCount,addInteval); return System.currentTimeMillis()+secends*1000; } public static int getNextConsumeSecond(int cousumeCount, int addInteval) { if (cousumeCount == 1) { return addInteval + 10; } else if (cousumeCount == 2) { return addInteval + 60; } else if (cousumeCount == 3) { return addInteval + 60 * 5; } else if (cousumeCount == 4) { return addInteval + 60 * 15; } else if (cousumeCount == 5) { return addInteval + 60 * 60; } else if (cousumeCount == 6){ return addInteval + 60 * 60 *2; } else if(cousumeCount == 7){ return addInteval + 60 * 60 *5; } else { return addInteval + 60 * 60 * 10; } }
除此之外,对于消费完成,等待删除的消息,可以将消息直接删除或者是进行备份。最好不要在该表中保留太多需要删除的消息,以免影响数据库的查询效率。
我们在处理消息的时候,首先对消息进行了锁定,设置了locker,如果系统出现异常的时候,也会产生消息一直处于被锁定的状态,此时可能还需要定期去修复被锁定的消息。
/** * 批量获取 可以消费的消息 * * @param count * @return */ public void repairQueueByStatus(int status) { ListactiveList = null; try { Map params = new HashMap (); params.put("status", status); //下次消费时间在当前时间3小时以内的消息 params.put("next_consume_time", System.currentTimeMillis()+3*60*1000); activeList = queueDAO.findQueueByParams(params); } catch (Exception e) { logger.error("QueueDomainRepository.repairQueueByStatus find error occured!" + e.getMessage(), e); throw new TuanRuntimeException( PayConstants.SERVICE_DATABASE_FALIURE, "QueueDomainRepository.findQueueByStatus error occured!",e); } if (activeList == null || activeList.size() == 0) { return ; } for (QueueDO temp : activeList) { try { //status=1,可被消费 queueDAO.update(temp.getQueueID(), PayConstants.QUEUE_STATUS_ACTIVE); } catch (Exception e) { logger.error("QueueDomainRepository.repairQueueByStatus update error occured!" + e.getMessage(), e); throw new TuanRuntimeException( PayConstants.SERVICE_DATABASE_FALIURE, "QueueDomainRepository.repairQueueByStatus update error occured!",e); } } }