目录
项目背景
现在有一个需求需要用到阻塞队列,让任务依次执行。我想过使用LBQ,但是有一个问题,我们的项目是分布式+负载均衡,就可能会出现以下几个问题:
- 项目处于起步阶段,会有频繁更新的情况,而使用LBQ的话,每次更新之后都会重新运行,导致LBQ未执行的任务不会再执行。
- 由于服务会有多个,所以使用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了,我们来测试一下
我这边开了两个线程池,来模拟多服务的情况
这是我新写的一个TestTask,用来测试
我这边通过访问test来循环添加10条任务,让我们运行一下看看效果
可以注意到,每个任务都独立运行,且互不干涉,至此整个流程算是完成了
该框架特点和注意事项:
- 每一个任务都有一个独特的name,这个name是唯一的,如果添加的任务发生name重复问题,则不会添加到队列
- 由于lock的存在,所以每一个操作都能保证数据的准确性,不会因为并发问题导致数据不同步,所以请不要用BaseQueue以外的方法去使用和修改redis内的队列信息,这样很容易导致一些预期之外的错误
- BaseQueue中的redisTemplate是我在ContextEvent事件中注入赋值给他的,所以请注意redis的连接和赋值问题。