轻量级的进程内生产者/消费者实现

本文介绍了如何使用Java的BlockingQueue实现一个简单的生产者/消费者模型,包括优缺点分析、代码实现及使用示例。通过这个模型,可以在进程内实现消息的生产和消费,但不支持数据持久化和复杂路由功能。文中还展示了如何添加队列监控,以方便监控队列状态。
摘要由CSDN通过智能技术生成


前言

    在我们项目中在处理一些耗时比较长,而且不太关心处理结果的任务时,我们经常会使用到消息中间件。我们把任务扔进消息队列里,然后由消费者慢慢的处理一条一条的任务。我们最常见的消息中间件有:activeMQ,rabbitMQ,kafka等等。
    上面的这些消息中间件都是比较典型的生产者/消费者模型,在项目中根据自己业务规模和能力选择不同的中间件来处理。
    这里我给大家介绍一个利用BlockingQueue手写实现一个先进先出的生产者/消费者模型。


一、优缺点

1、优点

  1. 使用简单,无需引入第三方jar包或额外中间件
  2. 进程内生产和消费,无需额外网络传输
  3. 根据需要很方便调整队列大小和消费者数量

2、缺点

  1. 只是在进程内的生成和消费,无法与其他进程进行关联
  2. 未实现数据持久化,重启服务后,如果队列里有未消费的数据会丢失数据
  3. 未实现复杂的路由key绑定功能
  4. 未实现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;
	}
}
  1. 只要实现一下 getQueueName()和 execute(Object message)这两个方法。
  2. 如果你消费者需要多线程处理,那就覆盖一下getConsumerSize()这个方法,返回消费者个数就行。
  3. 每个队列的默认容量是5000,如果需要修改,只要覆盖一下getQueueSize()这个方法,返回队列大小就可以了。

2、往队列里面放消息。这个很简单,只要使用生产者对象,调用send方法即可,会按照传递的queueName投递到对应的队列中

@Autowired
private QueueProducer queueProducer;

@Test
public void testQueue() {
	queueProducer.send("test", "123");
	queueProducer.send("test", "456");
}

怎么样,简单吧。

5.队列监控

    其实我们还可以做的更人性化一些

  1. 增加队列的监控页面
  2. 数据持久化

下面我们就实现一个队列数据监控的页面

创建一个队列监控返回给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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值