技术背景
公司需要实现发布信息后如果7天没有发布推广则短信通知用户.
技术方案
1、mq延时队列 碍于项目架构,公司采用的wmb(58消息队列) 该队列最长支持2天超时,不符合需求。
2、定时任务扫表 需求允许推送消息误差时间控制在10分钟左右,当表数据量增大时 频繁扫表容易给数据库造成压力。
3、 定时任务+redis zset 需要处理重复推送问题以及服务器宕机消息丢失问题.
实现思路
定时任务1扫描当天需要推送的消息(6-7天前创建的数据)存入redis有序队列,分值设为 MMddHHmmssSSS 定时任务2每20分钟查询redis 根据当前系统时间取出分值大于当前系统时间的元素 进行短信通知
redis相关实现
放置元素
/**
* @param queueName 队列名称
* @param queueElement 队列元素
* @return boolean
* @author duanshaojie
* @date 2021/8/20
* @Throw
* @deprecated
**/
public boolean MsgEnqueue(String queueName, QueueElement queueElement) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String timeFlag = simpleDateFormat.format(new Date());
return redisTemplate.opsForZSet().add(queueName, queueElement, Long.parseLong(timeFlag));
}
取出元素
/**
* @param queueName 队列名称
* @param delay $分钟前
* @return java.util.Set<com.example.redisspring.entity.QueueElement>
* @author duanshaojie
* @date 2021/8/20 11:54
* @Throw
* @deprecated 从指定队列读取指定时间前的元素
* 只是读取 也可能同时被其他系统读取到
**/
public Set<QueueElement> getMsgForQueue(String queueName, int delay) {
Calendar beforeTime = Calendar.getInstance();
beforeTime.add(Calendar.MINUTE, 0 - delay);
Date beforeD = beforeTime.getTime();
String before = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(beforeD);
ZSetOperations<String, QueueElement> zSetOperations = redisTemplate.opsForZSet();
Set<QueueElement> taskIdSet = zSetOperations.reverseRangeByScore(queueName, 0, Long.parseLong(before));
return taskIdSet;
}
生产者
/**
* @param
* @return
* @Author duanshaojie
* @date 18:05 2021/8/19
* @throw
* @deprecated 延时队列生产者 使用Zset有序集合将时间设定为分数 存入Zset集合中
**/
@Test
void provider() {
for (int i = 0; i < 999; i++) {
System.out.println("入队" + redisTimeQueueService.MsgEnqueue(TIME_QUEUE, redisTimeQueueService.makeElement("provider", i)));
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者
/**
* @param
* @return
* @Author duanshaojie
* @date 18:07 2021/8/19
* @throw
* @deprecated 延时队列消费者 取出ZSet集合中的复合时间条件的分数进行
* 进行处理 这是不是阻塞的 所以不存在消费失败后出现阻塞行为
* 有可能消费失败可以在data中增加一个消费次数值 如果出现异常或者消费失败自增一
* TODO 这里需要先取出 然后进行操作 防止被重复消费 如果消费失败 重新自增进入队列 等待下次消费
* 失败次数到达一定次数后 数据取出进行存入私信队列或者记录操作
**/
@Test
void consumer() {
while (true) {
Set<QueueElement> elements = redisTimeQueueService.getMsgForQueue(TIME_QUEUE, 1);
if (elements == null || elements.isEmpty()) {
System.out.println("没有任务");
} else {
elements.forEach(id -> {
//先取出消息
//业务处理
if (redisTimeQueueService.removeElement(TIME_QUEUE, id)) {
try {
if (id.getFailNum() > 4) {
//进入死信队列 或者记录处理
} else {
System.out.println("从延时队列中获取到任务,taskId:" + id + " , 当前时间:" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
}
} catch (Exception e) {
log.info("队列{}中元素{}处理失败 重新入队", TIME_QUEUE, id);
if (redisTimeQueueService.makeFail(TIME_QUEUE, id)) {
//重新入队成功 不做处理
} else {
//重新入队失败
//进入死信队列或者 记录手动处理
}
} finally {
//进行消费日志记录
}
} else {
log.info("队列{}中元素{}移除失败 可能是已被处理 这里不做处理了", TIME_QUEUE, id);
}
});
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}