使用DelayQueue的初衷是为了实现类似于消息免打扰的功能:一定的时间过后才会把消息发送给用户,本来打算用定时器定时扫描,当不处在消息免打扰的时间段里面的时候就发送。但这样即使优化也会对数据库造成一定的压力。后来无意中看到了延时队列,感觉应该能用得上,本着学习新技术的态度,我就直接用上了。
先吐槽一下使用过程中的坑爹之处吧:
1.很多个时间段之间的重叠,为了计算不同时间段交叉顺延之后的延时时间花了很大的气力,当然这是另外一个故事了。
2.延时队列里面存储的值重启了会消失掉,为了避免这个情况引入了redis的操作。
3.延时队列里面的remove方法其实如果需要移除你想要的话需要重写equals方法,所以相应的也要重写hashcode方法,当然这里的hashcode最好采用业务的方式,这样才能保证唯一性吧。
下面就来进行具体的操作啦:
首先我们需要新建一个实现了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的。