目录

项目背景

整体流程

1.创建队列中任务的接口

2.创建BaseTask类

3.实现队列方法

4.新开线程池持续拿到最新的任务执行

项目背景

        现在有一个需求需要用到阻塞队列,让任务依次执行。我想过使用LBQ,但是有一个问题,我们的项目是分布式+负载均衡,就可能会出现以下几个问题:

  1. 项目处于起步阶段,会有频繁更新的情况,而使用LBQ的话,每次更新之后都会重新运行,导致LBQ未执行的任务不会再执行。
  2. 由于服务会有多个,所以使用LBQ会导致每个服务的LBQ任务都不相通,如果LBQ的任务在执行中出现了预期之外的错误,只能退回重新让该服务继续执行,但这很显然不符合负载均衡的观念。

        所以我选择使用redis中间件来实现任务的互通,这样无论怎么更新,无论服务器出了什么样的问题,都不会影响到队列任务的执行。

整体流程

        1.创建队列中任务的接口

                首先创建任务的接口,编写了一些基本方法

/*队列中每个任务的接口*/
public interface ITask<V>{
    //任务执行方法
    void run();

    //任务执行结束执行
    void finish();

    //任务是否正在被消费
    boolean isUse();

    //设置消费状态
    void setUse(boolean use);

    //任务名称
    String getName();

    void setName(String name);

    //任务附带数据
    Map<String,V> getData();

    //添加附带数据
    void addData(String k,V v);

    //异常处理
    void error(Exception e);
}

        2.创建BaseTask类

//基础任务类
public class BaseTask implements ITask, Serializable {

    //任务名
    private String name;

    //是否被使用
    public boolean isUse;

    //附加数据
    private final Map<String,Object> data;

    public BaseTask(){
        this.data = new HashMap<>();
    }

    @Override
    public void run() {
        this.setUse(true);
    }

    @Override
    public void finish() {
        BaseQueue.getInstance().remove(this);
    }

    @Override
    public boolean isUse() {
        return isUse;
    }

    @Override
    public void setUse(boolean use) {
        BaseQueue.getInstance().setUse(BaseQueue.getInstance().getIndex(getName()),use);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public Map getData() {
        return data;
    }

    @Override
    public void addData(String k, Object o) {
        this.data.put(k,o);
    }

    @Override
    public void error(Exception e) {
        throw new RuntimeException(e);
    }
}

                BaseTask类实现了基础的属性和方法,之后的拓展Task只需要继承BaseTask,覆写其中的run、finish、error方法即可。继承Serializable是因为传入Redis的对象必须继承Serializable ,因为redis需要序列化/反序列化。

        3.实现队列方法                

public class BaseQueue {

    //队列单例
    private static final BaseQueue INSTANCE = new BaseQueue(Integer.MAX_VALUE - 1);

    //redis存储名
    private static final String QUEUE_NAME = "company_queue";

    public static RedisTemplate<String,BaseTask> redisTemplate;

    //通过ReentrantLock来实现同步
    final ReentrantLock lock;
    //有2个条件对象,分别表示队列不为空和找不到下一个的情况
    private final Condition notEmpty;
    private final Condition notNext;

    //队列的最大值
    private final int size;

    public static BaseQueue getInstance(){
        return INSTANCE;
    }

    private BaseQueue(@Range(from = 1,to = Integer.MAX_VALUE) int size){
        //初始化队列长度
        if(size <= 0) throw new IllegalArgumentException();
        this.lock = new ReentrantLock(false);
        this.notEmpty = lock.newCondition();
        this.notNext = lock.newCondition();
        this.size = size;
    }

    //在队列中添加一个数据
    public boolean offer(BaseTask baseTask){
        Objects.requireNonNull(baseTask);
        final ReentrantLock lock = this.lock;
        lock.lock();

        try{
            if(getSize() == size) return false;
            if(exist(baseTask)) return false;
            enqueue(baseTask);
            return true;
        }finally {
            lock.unlock();
        }
    }

    //在队列中取出某一个数据(不会阻塞线程)
    public BaseTask poll(int index){
        final ReentrantLock lock = this.lock;
        lock.lock();
        try{
            return index < 0 ? null : dequeue(index);
        }finally {
            lock.unlock();
        }
    }

    //在队列中取出来某一个数据(会阻塞线程)
    public BaseTask take(int index) throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            if (getSize() == 0)
                notEmpty.await();
            return dequeue(index);
        }finally {
            lock.unlock();
        }
    }

    //删除队列中的某一个数据
    public boolean remove(BaseTask task){
        Objects.requireNonNull(task);
        final ReentrantLock lock = this.lock;
        lock.lock();

        try{
            if(getSize() > 0){
                update(getIndex(task.getName()),task);
                redisTemplate.opsForList().remove(QUEUE_NAME,-1,task);
                return true;
            }
            return false;
        }finally {
            lock.unlock();
        }
    }

    //获取左侧第一个可消费的任务(会阻塞线程)
    public BaseTask next() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            if (getSize() == 0) notEmpty.await();
            BaseTask task = null;
            task: while (task == null){
                for (int i = 0; i < getSize(); i++) {
                    BaseTask index = take(i);
                    if (!index.isUse()) {
                        task = index;
                        break task;
                    }
                }
                notNext.await();
            }
            return task;
        }finally {
            lock.unlock();
        }
    }

    public boolean setUse(int index,boolean use){
        final ReentrantLock lock = this.lock;
        lock.lock();

        try {
            if(index == -1) return false;
            if(getSize() == 0) return false;
            if(index >= getSize()) return false;

            BaseTask baseTask = poll(index);
            if(baseTask == null) return false;

            baseTask.isUse = use;
            update(index,baseTask);
            return true;
        }finally {
            lock.unlock();
        }
    }

    //根据名字获取当前任务所在的索引
    public int getIndex(String name){
        final ReentrantLock lock = this.lock;
        lock.lock();

        try {
            if(getSize() == 0) return -1;
            List<BaseTask> baseTasks = getAllTake();
            for(int i = 0; i < baseTasks.size(); i++){
                if(baseTasks.get(i).getName().equals(name)) return i;
            }
            return -1;
        }finally {
            lock.unlock();
        }
    }

    //判断某个任务是否存在
    private boolean exist(BaseTask task){
        Objects.requireNonNull(task);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if(getSize() == 0) return false;
            for(int i = 0; i < getSize(); i++){
                BaseTask poll = poll(i);
                if(poll == null) continue;
                if(task.getName().equals(poll.getName())) return true;
            }
            return false;
        }finally {
            lock.unlock();
        }
    }

    //修改某条数据
    private void update(int index,BaseTask baseTask){
        redisTemplate.opsForList().set(QUEUE_NAME,index,baseTask);
    }

    //获取队列的个数
    private int getSize(){
        final ReentrantLock lock = this.lock;
        lock.lock();
        try{
            return Math.toIntExact(redisTemplate.opsForList().size(QUEUE_NAME));
        }finally {
            lock.unlock();
        }
    }

    //在队列中取出某一个数据实现
    private BaseTask dequeue(int index){
        return redisTemplate.opsForList().index(QUEUE_NAME,index);
    }

    //添加数据的具体实现
    private void enqueue(BaseTask baseTask){
        //将数据插入到list最右侧
        redisTemplate.opsForList().rightPush(QUEUE_NAME,baseTask);
        //释放所有因为notEmpty阻塞的线程
        notEmpty.signal();
        //释放所有因为notNext阻塞的线程
        notNext.signal();
    }

    //获取队列中的所有任务
    public List<BaseTask> getAllTake(){
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return redisTemplate.opsForList().range(QUEUE_NAME,0,-1);
        }finally {
            lock.unlock();
        }
    }
}

        4.新开线程池持续拿到最新的任务执行

                我这边用的框架是SpringBoot,所以我采用Spring的线程池来实现的,也可以用其他的线程去实现,这个不重要。

@SneakyThrows
    private void queueStare(){
        CountDownLatch countDownLatch = new CountDownLatch(1);
        asyncService.executeAsync(()->{
            while (true){
                try{
                    BaseTask task = BaseQueue.getInstance().next();
                    try{
                        task.run();
                    }catch (Exception e){
                        task.error(e);
                    }finally {
                        task.finish();
                    }
                }catch (Exception e){
                    throw new RuntimeException(e);
                }
            }
        },countDownLatch);
    }

        至此整个流程就是OK了,我们来测试一下

redisson阻塞队列 集群模式 redis阻塞队列的实现_缓存

 我这边开了两个线程池,来模拟多服务的情况

redisson阻塞队列 集群模式 redis阻塞队列的实现_缓存_02

这是我新写的一个TestTask,用来测试

redisson阻塞队列 集群模式 redis阻塞队列的实现_缓存_03

我这边通过访问test来循环添加10条任务,让我们运行一下看看效果

redisson阻塞队列 集群模式 redis阻塞队列的实现_缓存_04

可以注意到,每个任务都独立运行,且互不干涉,至此整个流程算是完成了

该框架特点和注意事项:

  1. 每一个任务都有一个独特的name,这个name是唯一的,如果添加的任务发生name重复问题,则不会添加到队列
  2. 由于lock的存在,所以每一个操作都能保证数据的准确性,不会因为并发问题导致数据不同步,所以请不要用BaseQueue以外的方法去使用和修改redis内的队列信息,这样很容易导致一些预期之外的错误 
  3. BaseQueue中的redisTemplate是我在ContextEvent事件中注入赋值给他的,所以请注意redis的连接和赋值问题。