前言
在我们项目中在处理一些耗时比较长,而且不太关心处理结果的任务时,我们经常会使用到消息中间件。我们把任务扔进消息队列里,然后由消费者慢慢的处理一条一条的任务。我们最常见的消息中间件有:activeMQ,rabbitMQ,kafka等等。
上面的这些消息中间件都是比较典型的生产者/消费者模型,在项目中根据自己业务规模和能力选择不同的中间件来处理。
这里我给大家介绍一个利用BlockingQueue手写实现一个先进先出的生产者/消费者模型。
一、优缺点
1、优点
- 使用简单,无需引入第三方jar包或额外中间件
- 进程内生产和消费,无需额外网络传输
- 根据需要很方便调整队列大小和消费者数量
2、缺点
- 只是在进程内的生成和消费,无法与其他进程进行关联
- 未实现数据持久化,重启服务后,如果队列里有未消费的数据会丢失数据
- 未实现复杂的路由key绑定功能
- 未实现pub/sub功能
终上所列的优缺点,然后结合我们项目实际的需求,可以选择适合自己项目的生产者/消费者模型。
简单的先进先出的生产者/消费者模型
下面我们就用代码来实现这样一个生产者/消费者模型。
二、代码实现
1.工厂类
首先我们定义一个工厂类,来保存所有的队列,全局变量,还有队列初始化,获取队列等方法。为了多线程安全,我们把这个工厂类做成单例模式(使用饿汉模式),并且提供一个getInstance方法来获取这个工厂类的对象。看代码实现
package com.hp.springboot.queue;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hp.springboot.queue.exception.QueueNotFindException;
import com.hp.springboot.queue.model.QueueExecutorBO;
/**
* 描述:队列的工厂类。保存一些全局变量
* 作者:黄平
* 时间:2017年3月30日
*/
public class QueueFactory {
private static Logger log = LoggerFactory.getLogger(QueueFactory.class);
//存放所有的队列
private Map<String, QueueExecutorBO> queueMap = new ConcurrentHashMap<>();
// 饿汉模式,保证单例
private static QueueFactory instance = new QueueFactory();
/**
* 为了单例,私有化构造方法
*/
private QueueFactory() {}
/**
* 获取实例
* @return
*/
public static QueueFactory getInstance() {
return instance;
}
/**
* @Title: getQueue
* @Description: 获取队列
* @param queueName
* @return
*/
public QueueExecutorBO getQueue(String queueName) {
//获取该队列
QueueExecutorBO queue = queueMap.get(queueName);
if (queue == null) {
log.error("the queue is not find. please create this queue. with queueName={}", queueName);
throw new QueueNotFindException(queueName);
}
return queue;
}
public Map<String, QueueExecutorBO> getQueueMap() {
return queueMap;
}
}
这里有几个注意点:
1、这里的queueMap对象就是保存所有对象的对象,使用ConcurrentHashMap来保证线程安全。key就是queueName,value就是对应的队列对象
2、这里使用了饿汉模式来保证该工厂对象是个单例
3、getQueue方法就是根据队列名称,获取队列
2.消费者抽象类
消费者抽象类里面队列的名称,队列容量,初始化队列等方法。该抽象类让子类去继承,具体的消费方法需要子类去实现。看代码:
package com.hp.springboot.queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hp.springboot.queue.exception.QueueNameExistsException;
import com.hp.springboot.queue.model.QueueExecutorBO;
/**
* 描述:队列的消费者,抽象类,子类继承
* 作者:黄平
* 时间:2017年3月30日
*/
public abstract class AbstractQueueConsumer {
private static Logger log = LoggerFactory.getLogger(AbstractQueueConsumer.class);
/**
* @Title: getQueueName
* @Description: 获取队列的名称
* @return
*/
public abstract String getQueueName();
/**
* @Title: execute
* @Description: 具体处理方法
* @param message
*/
protected abstract void execute(Object message);
/**
* @Title: getConsumerSize
* @Description: 获取消费者数量(默认一个)
* @return
*/
public int getConsumerSize() {
return 1;
}
/**
* @Title: getQueueMaxSize
* @Description: 获取该队列的最大容量
* @return
*/
public int getQueueMaxSize() {
return 5000;
}
/**
* @Title: getRejectedExecutionHandler
* @Description: 获取线程池拒绝策略
* 默认直接拒绝,抛出异常
* @return
*/
public RejectedExecutionHandler getRejectedExecutionHandler() {
return new AbortPolicy();
}
/**
* @Title: onException
* @Description: 当处理失败时,执行
* @param message
* @param e
*/
public void onException(Object message, Exception e) {
// do notiong
}
/**
* 消费
*/
@PostConstruct
protected void init() {
String queueName = getQueueName();
log.info("init HPQueueConsumer start. with queueName={}", queueName);
if (StringUtils.isEmpty(queueName)) {
log.warn("init error. with queueName is empty.");
return;
}
// 创建线程池处理消费者
ThreadPoolExecutor exe = new ThreadPoolExecutor(getConsumerSize(),
getConsumerSize(),
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
getRejectedExecutionHandler());
// 创建队列,然后返回初始化后的队列
QueueExecutorBO queue = createQueue(queueName, getQueueMaxSize());
// 循环,创建多线程执行任务
for (int i = 0; i < getConsumerSize(); i++) {
exe.execute(new Runnable() {
@Override
public void run() {
while (true) {
Object message = null;
try {
// 从队列里面获取一条消息,如果为空,则阻塞
message = queue.getQueue().take();
// 执行任务
execute(message);
} catch (Exception e) {
log.error("execute message from {} error", queueName, e);
onException(message, e);
}
}
}
});
}
log.info("init HPQueueConsumer success. with queueName={}", queueName);
}
/**
* @Title: initQueue
* @Description: 创建新的队里
* 返回初始化后的队列
* @param queueName
* @param queueSize
* @return
*/
private QueueExecutorBO createQueue(String queueName, int queueSize) {
QueueFactory factory = QueueFactory.getInstance();
QueueExecutorBO queue = factory.getQueueMap().get(queueName);
if (queue != null) {
// 创建时,如果队列已经存在,则抛异常
throw new QueueNameExistsException(queueName);
}
// 新生成一个队列对象
queue = new QueueExecutorBO();
queue.setMaxSize(queueSize);
queue.setQueue(new LinkedBlockingQueue<>(queueSize));
queue.setQueueName(queueName);
queue.setConsumerSize(getConsumerSize());
// 放入工厂类
factory.getQueueMap().put(queueName, queue);
return queue;
}
}
这需要子类实现的方法
- getQueueName()(获取该队列的名称。队列名称不能重复)
- execute(Object message)(真实的消费实现方法,传入的message就是生产者生产的消息对象)
这里使用了BlockQueue的take方法来从队列中获取数据。队列如果为空,会一直阻塞在这里,直到队列里有新的对象放入。
3.生产者
生产者很简单,就根据queueName把所需要的消息对象放入队列即可,看代码:
package com.hp.springboot.queue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hp.springboot.queue.model.QueueExecutorBO;
/**
* 描述:生产者
* 作者:黄平
* 时间:2017年3月30日
*/
public class QueueProducer {
private static Logger log = LoggerFactory.getLogger(QueueProducer.class);
/**
* @Title: send
* @Description: 发送消息
* @param queueName
* @param message
*/
public void send(String queueName, Object message) {
QueueExecutorBO queue = QueueFactory.getInstance().getQueue(queueName);
try {
//如果队列有空间,则正常放入;如果队列满了,就阻塞在这里
queue.getQueue().put(message);
} catch (InterruptedException e) {
log.error("put message into queue error. with queueName={}", queueName, e);
}
}
}
这里就一个方法,按照队列名称,把消息对象放入队列即可。这里放入队列的方法使用了BlockQueue的put方法(队列有空间,就直接放入;队列如果满了,则会阻塞在这里)。如果你项目有需要,这里可以改成BlockQueue的offer方法。
队列的对象
package com.hp.springboot.queue.model;
import java.util.concurrent.BlockingQueue;
import com.hp.springboot.common.bean.AbstractBean;
/**
* 描述:队列的对象
* 作者:黄平
* 时间:2021年3月23日
*/
public class QueueExecutorBO extends AbstractBean {
/**
*
*/
private static final long serialVersionUID = 7257129042392330250L;
/**
* 队列名称
*/
private String queueName;
/**
* 队列最大容量
*/
private int maxSize;
/**
* 队列
*/
private BlockingQueue<Object> queue;
/**
* 消费者数量
*/
private int consumerSize;
public QueueResponseBO toQueueResponseBO() {
QueueResponseBO resp = new QueueResponseBO();
resp.setConsumerSize(consumerSize);
resp.setCurrentSize(queue.size());
resp.setMaxSize(maxSize);
resp.setQueueName(queueName);
return resp;
}
get and set...
这样,生产者也准备好了,余下的就交给消费者慢慢的消费你的消息吧。
代码很简单,上面的简单代码就已经实现了一个进程内的生产者/消费者模型。
4.使用示例
使用方法很简单
1、写个消费者
消费者只要继承我们上面的AbstractQueueConsumer这个类,并且实现一下必须的方法。写个简单的消费者:
package com.test.mvc.queue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.hp.springboot.queue.AbstractQueueConsumer;
/**
* 描述:
* 作者:黄平
* 时间:2021年3月22日
*/
@Component
public class TestQueueConsumer extends AbstractQueueConsumer {
private static Logger log = LoggerFactory.getLogger(TestQueueConsumer.class);
@Override
public String getQueueName() {
return "test";
}
@Override
protected void execute(Object message) {
log.info("get message={}", message);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public int getConsumerSize() {
return 1;
}
}
- 只要实现一下 getQueueName()和 execute(Object message)这两个方法。
- 如果你消费者需要多线程处理,那就覆盖一下getConsumerSize()这个方法,返回消费者个数就行。
- 每个队列的默认容量是5000,如果需要修改,只要覆盖一下getQueueSize()这个方法,返回队列大小就可以了。
2、往队列里面放消息。这个很简单,只要使用生产者对象,调用send方法即可,会按照传递的queueName投递到对应的队列中
@Autowired
private QueueProducer queueProducer;
@Test
public void testQueue() {
queueProducer.send("test", "123");
queueProducer.send("test", "456");
}
怎么样,简单吧。
5.队列监控
其实我们还可以做的更人性化一些
- 增加队列的监控页面
- 数据持久化
下面我们就实现一个队列数据监控的页面
创建一个队列监控返回给view的实体类
package com.hp.springboot.queue.model;
import com.hp.springboot.common.bean.AbstractBean;
/**
* 描述:队列监控响应的实体类
* 作者:黄平
* 时间:2021年3月23日
*/
public class QueueResponseBO extends AbstractBean {
/**
*
*/
private static final long serialVersionUID = -8517091372489035970L;
/**
* 队列名称
*/
private String queueName;
/**
* 队列最大容量
*/
private int maxSize;
/**
* 队列当前数据总数
*/
private int currentSize;
/**
* 消费者数量
*/
private int consumerSize;
get and set...
}
如果需要其他字段,可以自己添加
添加一个controller,返回对应的视图
package com.hp.springboot.queue.controller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import com.hp.springboot.queue.QueueFactory;
import com.hp.springboot.queue.model.QueueExecutorBO;
import com.hp.springboot.queue.model.QueueResponseBO;
/**
* 描述:展示当前队列的使用情况
* 作者:黄平
* 时间:2021年3月22日
*/
@RestController
public class QueueController {
/**
* @Title: queueList
* @Description: 队列监控
* @param queueName
* @return
*/
@RequestMapping("/queueList")
public ModelAndView queueList(String queueName) {
// 通过工厂类,获取所有的队列
Map<String, QueueExecutorBO> queueMap = QueueFactory.getInstance().getQueueMap();
List<QueueResponseBO> queueList = new ArrayList<>();
if (StringUtils.isNotBlank(queueName)) {
// 如果传入queueName,则只查询这个queue的数据
QueueExecutorBO queue = queueMap.get(queueName);
if (queue != null) {
queueList.add(queue.toQueueResponseBO());
}
} else {
// 返回全部的queue
// 遍历map,放入queueList中
for (Entry<String, QueueExecutorBO> entry : queueMap.entrySet()) {
queueList.add(entry.getValue().toQueueResponseBO());
}
}
// 按照key排序
Collections.sort(queueList, new Comparator<QueueResponseBO>() {
@Override
public int compare(QueueResponseBO o1, QueueResponseBO o2) {
return o1.getQueueName().compareTo(o2.getQueueName());
}
});
Map<String, Object> map = new HashMap<>();
map.put("queueList", queueList);
return new ModelAndView("queueList", map);
}
}
这里提供一个queueList接口,可以接受一个参数,根据queueName查询对应的queue,如果不传参数,则查询所有queue。返回到对应的视图QueueList。看下这个页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" href="data:image/ico;base64,aWNv">
<meta http-equiv="refresh" content="10"><!-- 每隔10秒刷新一次页面 -->
<style>
table {
width: 60%;
border-top: 1px solid #999;
border-left: 1px solid #999;
border-spacing: 0;/*去掉单元格间隙*/
}
table td,table th {
padding: 10px 30px;
border-bottom: 1px solid #999;
border-right: 1px solid #999;
}
</style>
<title>队列监控</title>
</head>
<body>
<table align="center">
<thead>
<tr>
<th>队列名称</th>
<th>消费者数量</th>
<th>队列容量(当前数量/最大容量)</th>
</tr>
</thead>
<tbody>
<#list queueList as queue>
<tr align="center">
<td>${queue.queueName}</td>
<td>${queue.consumerSize}</td>
<td>${queue.currentSize}/${queue.maxSize}</td>
</tr>
</#list>
</tbody>
</table>
<script>
</script>
</body>
</html>
这个页面很简单,就直接用表格显示出查询出来的queue信息,并且这个页面10秒自动刷新一次。看下效果图
是不是很简单,很方便的监控我们队列的情况,遇到问题立刻报警。同时这边也可以加上一些告警手段,给每个队列设置一个阈值,超过这个阈值就发短信或者邮件告警,这样就完美了。
至于数据持久化,这里就不实现了。如果让你设计一下数据持久化,你该如何实现?
好了,上面就是很简单的进程内的生产者/消费者实现,欢迎大家留言讨论。完整代码见我的gitee 我的gitee