四、队列的使用(基于内存 和 基于数据库)

今天跟大家来看看如何在项目中使用队列。首先我们要知道使用队列的目的是什么?一般情况下,如果是一些及时消息的处理,并且处理时间很短的情况下是不需要使用队列的,直接阻塞式的方法调用就可以了。但是,如果在消息处理的时候特别费时间,这个时候如果有新的消息来了,就只能处于阻塞状态,造成用户等待。这个时候在项目中引入队列是十分有必要的。当我们接受到消息后,先把消息放到队列中,然后再用新的线程进行处理,这个时候就不会有消息的阻塞了。下面就跟大家介绍两种队列的使用,一种是基于内存的,一种是基于数据库的。

首先,我们来看看基于内存的队列。在Java的并发包中已经提供了BlockingQueue的实现,比较常用的有ArrayBlockingQueue和LinkedBlockingQueue,前者是以数组的形式存储,后者是以Node节点的链表形式存储。至于数组和链表的区别这里就不多说了。

BlockingQueue 队列常用的操作方法,前面的文章有介绍这里不在重复。

下面用一个例子来看看是怎么使用的。

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 BlockingQueue<String> queue = new LinkedBlockingQueue<String>(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内存的大小,一般如果是内存占用不大且处理相对较为及时的都可以采用此种方法。如果你在队列处理的时候需要有失败重试机制,那么用此种队列就不是特别合适了。下面就说说基于数据库的队列。

基于数据库的队列,很好理解,就是接收到消息之后,把消息存入数据库中,设置消费时间、重试次数等,再用新的线程从数据库中读取信息,进行处理。首先来看看数据库的设计。

java内存表 sql_java

代码示例如下:

/** 
     * 批量获取 可以消费的消息 
     * 先使用一个时间戳将被消费的消息锁定,然后再使用这个时间戳去查询锁定的数据。 
     * @param count 
     * @return 
     */  
    public List<Queue> findActiveQueueNew(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<Queue> 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) {  
        List<QueueDO> activeList = null;  
        try {  
            Map<String,Object> params = new HashMap<String,Object>();  
            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);  
            }  
              
        }  
         }

以上就是对两种队列的简单说明。