DelayQueue的初次体验

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq1782/article/details/78870605

     使用DelayQueue的初衷是为了实现类似于消息免打扰的功能:一定的时间过后才会把消息发送给用户,本来打算用定时器定时扫描,当不处在消息免打扰的时间段里面的时候就发送。但这样即使优化也会对数据库造成一定的压力。后来无意中看到了延时队列,感觉应该能用得上,本着学习新技术的态度,我就直接用上了。

   先吐槽一下使用过程中的坑爹之处吧:

  1.很多个时间段之间的重叠,为了计算不同时间段交叉顺延之后的延时时间花了很大的气力,当然这是另外一个故事了。

   2.延时队列里面存储的值重启了会消失掉,为了避免这个情况引入了redis的操作。

   3.延时队列里面的remove方法其实如果需要移除你想要的话需要重写equals方法,所以相应的也要重写hashcode方法,当然这里的hashcode最好采用业务的方式,这样才能保证唯一性吧。

  下面就来进行具体的操作啦:

扫描二维码关注公众号,回复: 2995067 查看本文章

  首先我们需要新建一个实现了Delayed类的方法,我们需要实现其中重要的2个方法,compareTo方法用于排序,确定队列里面元素出列的先后顺序;getDelay则是用来获取延时时间的。下面贴出相关代码:(@component是为了被扫描到,不加也可以)

import java.io.Serializable;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Component;

/**
 * 为了延时队列而创建的Message对象
 * 
 * @author 15293
 *
 */
@Component
public class Message implements Delayed, Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	/**
	 * 消息的Id
	 */
	private Integer messageId;
	/**
	 * 消息的创建时间
	 */
	private Long startTime;

	/**
	 * 消息的等待时间
	 */
	private long waitTime;

	public Message() {

	}

	/**
	 * 
	 * @param messageId
	 *            消息的Id
	 * @param startTime
	 *            消息的创建时间(System.currentTimeMillis())
	 * @param waitTime
	 *            消息的等待时间
	 */
	public Message(Integer messageId, long startTime, long waitTime) {
		this.messageId = messageId;
		this.startTime = startTime;
		this.waitTime = startTime + waitTime;
	}

	public Integer getMessageId() {
		return messageId;
	}

	public void setMessageId(Integer messageId) {
		this.messageId = messageId;
	}

	public Long getStartTime() {
		return startTime;
	}

	public void setStartTime(Long startTime) {
		this.startTime = startTime;
	}

	public long getWaitTime() {
		return waitTime;
	}

	public void setWaitTime(long waitTime) {
		this.waitTime = waitTime;
	}

	/**
	 * 比较队列里面里面元素的顺序,因为DelayedQueue采用的是PriorityQueue的实现方式 等待时间越短的
	 */
	@Override
	public int compareTo(Delayed o) {
		if (o == null || !(o instanceof Message)) {
			return 1;
		}
		if (o == this) {
			return 0;
		}
		Message message = (Message) o;
		if (this.waitTime > message.waitTime) {
			return 1;
		} else if (this.waitTime == message.waitTime) {
			return 0;
		} else {
			return -1;
		}
	}

	/**
	 * 获得消息的等待时间并转换成相应的时间单位 这里选择不转化 传入TimeUnit.NANOSECONDS ;
	 * 
	 * Timeutil.convert(a,b) ---把b时间单位的啊转换成对应的TimeUtil里面的时间单位
	 * 
	 * @return 毫秒数
	 * 
	 */
	@Override
	public long getDelay(TimeUnit unit) {
		return unit.convert(this.waitTime - System.currentTimeMillis(), TimeUnit.NANOSECONDS);
	}

	/**
	 * 讲道理messageId绝对不会重复
	 */
	@Override
	public int hashCode() {
		return this.messageId;
	}

	/**
	 * 重写equals方法 根据hashCode
	 */
	@Override
	public boolean equals(Object object) {
		return object.hashCode() == this.hashCode() ? true : false;
	}

}

下面就是启动一个线程不断的从队列里面take()值了:----里面会涉及到一下redis代码(包括对象的序列化和反序列化,和利用Jedis进行byte[]类型的存取及设置过期时间)还包括一些其余的业务代码 ,怕麻烦就不删了

   
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

import soke.home.message.enums.MsgInboxStatusEnum;
import soke.home.util.SerializerUtil;
import soke.home.util.WeixinUtil;
import soke.memory.redis.RedisDb;

/**
 * 綫程池取消息
 * 
 * @author 15293
 *
 */

@Component
public class MessageManagerPool implements ApplicationListener<ContextRefreshedEvent> {

	static DelayQueue<Message> messages = new DelayQueue<Message>();

	Logger logger = LoggerFactory.getLogger(MessageManagerPool.class);

	/**
	 * Bean实例化的时候就启动
	 */
	public MessageManagerPool() {
		System.err.println("--------------------------开始延时队列取值操作: ");
		daemonThread = new Thread(() -> execute());
		daemonThread.setName("DelayQueueThrad");
		executor.execute(daemonThread);
		System.err.println("初始化完成!");
	}

	// newSingleThreadExecutor:创建一个单线程的Executor,如果该线程因为异常而结束就新建一条线程来继续执行后续的任务
	// newFixedThreadPool:创建可重用且固定线程数的线程池,如果线程池中的所有线程都处于活动状态,
	// 此时再提交任务就在队列中等待,直到有可用线程;如果线程池中的某个线程由于异常而结束时,线程池就会再补充一条新线程。
	/** 线程池 */
	Executor executor = Executors.newSingleThreadExecutor();

	/** 守护线程 */
	private Thread daemonThread;

	/**
	 * 从延迟队列中取值
	 */
	private void execute() {
		System.err.println(Thread.currentThread().getName() + "现在时间是:" + System.currentTimeMillis());
		while (true) {
			try {
				Message message = messages.take();
				// 延时时间应该是负数或者毫秒数 越接近0表明越精确 延时时间表明从队列里面取值的时间
				System.err.println("----------MessageId是" + message.getMessageId() + "-" + message.getStartTime() + "-"
						+ message.getWaitTime() + "----延时时间是" + message.getDelay(TimeUnit.NANOSECONDS));
				if (message != null) {
					Integer messageId = message.getMessageId();
					// 不管如何处理先把redis里面村粗相关的删除了
					String[] messageArray = { messageId + "" };
					RedisDb.srem("messageIdSets", messageArray);

					CpMsgInbox cpMsgInbox = CpMsgInbox.findByMsgId(messageId);
					if (cpMsgInbox == null || cpMsgInbox.getStatus().equals(MsgInboxStatusEnum.READ)
							|| cpMsgInbox.getStatus().equals(MsgInboxStatusEnum.REPLY)) {
						// 已读的话不发送通知
					} else if (cpMsgInbox.getStatus().equals(MsgInboxStatusEnum.RELEASE)) {
						// 发送微信通知(只有在发送状态下)
						String sokeNo = cpMsgInbox.getToSokeNo();
						WeixinUtil.sendWxTemplateMessage(messageId, "", sokeNo);

						cpMsgInbox.setStatus(MsgInboxStatusEnum.PUBLISH);
						cpMsgInbox.save();
					}
				}
			} catch (Exception e) {
				logger.info("从队列中取值报错" + e.getMessage());
				e.printStackTrace();
			}
		}
	}

	/**
	 * 往队列里面添加新的值
	 * 
	 * @param startTime
	 *            创建值
	 * 
	 * @param messageId
	 *            消息Id
	 * @param waitTime
	 *            等待时间(毫秒值)
	 */
	public static void put(Long startTime, Integer messageId, long waitTime) {
		Message message = new Message(messageId, startTime, waitTime);
		// 序列化存入数据到redis,防止重启之后数据丢失
		RedisDb.setObject(("Message" + messageId).getBytes(), SerializerUtil.serializer(message),
				(int) message.getDelay(TimeUnit.NANOSECONDS));
		// messageId也存入redis里面
		String[] messageArray = { messageId + "" };
		RedisDb.sadd("messageIdSets", messageArray);
		System.err.println("开始放值,Id是" + messageId + "CreateTime是: " + startTime + " 等待时间是" + waitTime);
		messages.put(message);
	}

	/**
	 * 确保在所有bean加载到Spring容器之后 再把redis里面存储的所有Message回填到队列里面
	 * 
	 * 
	 */
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		if (event.getApplicationContext().getParent() == null) {
			System.err.println("----------------------开始执行把redis里面的数据回填进队列的操作: ");
			Set<String> strings = RedisDb.smembers("messageIdSets");
			for (String string : strings) {
				byte[] bytes = RedisDb.getObject(("Message" + string).getBytes());
				if (bytes != null) {
					Message message = (Message) SerializerUtil.unserializer(bytes);
					if (!messages.contains(message)) {
						messages.put(message);
						System.err.println("redis存入MessageId是" + message.getMessageId());
					}
				}
			}
		}
	}

	/**
	 * 当用户更新免打扰时间段的时候
	 * 
	 * @param sokeNo
	 */
	public static void update(String sokeNo) {
		List<CpMsgInbox> cpMsgInboxs = new ArrayList<CpMsgInbox>();
		cpMsgInboxs = CpMsgInbox.findByToSenderSokeNo(sokeNo);
		List<Integer> msgIds = cpMsgInboxs.stream().filter(x -> x.getStatus().equals(MsgInboxStatusEnum.RELEASE))
				.map(x -> x.getMsgId()).collect(Collectors.toList());
		System.err.println("------------修改免打扰时间,重新入列");
		for (Integer messageId : msgIds) {
			long delayTime = CpMsgDnd.getDelay(sokeNo);
			Message message = new Message(messageId, System.currentTimeMillis(), delayTime * 1000);
			messages.remove(message);
			messages.put(message);
			for (Message message2 : messages) {
				System.err.println("messageId是: " + message2.getMessageId() + "----- 等待时间是: "
						+ (message2.getWaitTime() - System.currentTimeMillis()) / 1000);
			}
			RedisDb.setObject(("Message" + messageId).getBytes(), SerializerUtil.serializer(message),
					(int) message.getDelay(TimeUnit.NANOSECONDS));
		}

	}

}

以上所有的开发环境都是Java8+SpringBoot+Mybatis的。看一下源码:
   
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();
   可以看出来,DelayQueue的部分实现还是借助了priorityQueue的。




猜你喜欢

转载自blog.csdn.net/qq1782/article/details/78870605