解题思路
- 前置条件
项目中使用了reids, 但是有不想集成MQ,可以采用。使用MQ请跳过 使用 redis
zset
有序集合,zset
是Redis
提供的一个非常特别的数据结构。每一个value
后面会携带一个score
。我们可以使用score
来存储过期时间,然后以当前时间为条件轮询zset
集合。从而达到延时的效果.
实现方式
本文介绍基于SpringBoot的方式实现.
1. 队列配置
可通过配置文件配置是否开启队列模式
package com.sword.starter.cache.redis.config;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* knife4j 属性
*
* @author tan
* @version 1.0 2021-11-12
*/
@Getter
@Setter
@ToString
@ConfigurationProperties(prefix = "bluerosa.redis")
public class RedisProps {
private Queue queue = new Queue();
@Getter
@Setter
@ToString
public static class Queue {
/**
* 启用队列
*/
private boolean enable = false;
/**
* 轮询间隔
*/
private long pollingInterval = 1000;
}
}
2. message 对象
封装消息对象
package com.sword.starter.cache.redis.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* redis queue message, 简单的消息队列实现
*
* @author Tan
* @version 1.0 2022/5/23
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Message {
/**
* 消息Id
*/
private long id;
/**
* 延时时间,单位毫秒
*/
private Long delay;
/**
* 消息内容
*/
private String content;
/**
* 主题
*/
private String topic;
}
3. 消息事件
package com.sword.starter.cache.redis.queue;
import com.sword.starter.cache.redis.domain.Message;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* redis 队列事件
*
* @author Tan
* @version 1.0 2022/5/23
*/
@Getter
public class RedisQueueEvent extends ApplicationEvent {
public RedisQueueEvent(Message message) {
super(message);
}
public Message getSource() {
return (Message) source;
}
}
4. 轮询zset集合(核心)
SpringBoot 项目启动时加入线程,轮询zset
集合获取队列值
package com.sword.starter.cache.redis.queue;
import com.alibaba.fastjson.JSON;
import com.sword.starter.cache.redis.config.RedisProps;
import com.sword.starter.cache.redis.domain.Message;
import com.sword.starter.cache.redis.enums.RedisKey;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Set;
/**
* redis 延时队列
*
* @author Tan
* @version 1.0 2022/5/23
*/
@Slf4j
@Order(1000)
@RequiredArgsConstructor
@EnableConfigurationProperties(RedisProps.class)
public class RedisQueueRunner implements CommandLineRunner {
private final TaskExecutor taskExecutor;
private final StringRedisTemplate stringRedisTemplate;
private final ApplicationContext applicationContext;
private final RedisProps redisProps;
@Override
public void run(String... args) {
log.info("start redis delay queue: {}", redisProps);
taskExecutor.execute(() -> {
while (redisProps.getQueue().isEnable()) {
try {
long max = System.currentTimeMillis();
Set<String> queue = stringRedisTemplate.opsForZSet().rangeByScore(RedisKey.REDIS_QUEUE.getKey(), 0, max);
log.debug("get queue: {}", queue);
if (CollectionUtils.isEmpty(queue)) {
sleep();
continue;
}
queue.forEach(it -> {
Message message;
if (StringUtils.hasText(it)) {
message = JSON.parseObject(it, Message.class);
} else {
message = null;
}
log.debug("redis queue message: {}", message);
applicationContext.publishEvent(new RedisQueueEvent(message));
});
log.debug("remove queue: {}", queue);
stringRedisTemplate.opsForZSet().removeRangeByScore(RedisKey.REDIS_QUEUE.getKey(), 0, max);
} catch (Exception e) {
log.error("get redis queue message error", e);
sleep();
}
}
});
}
private void sleep() {
try {
Thread.sleep(redisProps.getQueue().getPollingInterval());
} catch (InterruptedException e) {
log.error(" sleep error", e);
}
}
}
5. 发送消息模版
package com.sword.starter.cache.redis.template;
import com.sword.starter.cache.redis.domain.Message;
/**
* redis 队列模板
*
* @author Tan
* @version 1.0 2022/5/23
*/
public interface RedisQueueTemplate {
/**
* 发送消息
*
* @param message 消息
*/
void sendMessage(Message message);
}
package com.sword.starter.cache.redis.template.impl;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.sword.starter.cache.redis.domain.Message;
import com.sword.starter.cache.redis.enums.RedisKey;
import com.sword.starter.cache.redis.template.RedisQueueTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.Optional;
/**
* redis 简单的消息队列实现
*
* @author Tan
* @version 1.0 2022/5/23
*/
@Slf4j
@RequiredArgsConstructor
public class RedisQueueTemplateImpl implements RedisQueueTemplate {
private final StringRedisTemplate stringRedisTemplate;
@Override
public void sendMessage(Message message) {
log.debug("sendMessage: {}", message);
Assert.notNull(message, "message 不能为空");
Assert.isTrue(StringUtils.hasText(message.getTopic()), "topic 不能为空");
Assert.isTrue(StringUtils.hasText(message.getContent()), "content 不能为空");
message.setId(IdUtil.getSnowflakeNextId());
stringRedisTemplate.opsForZSet().add(RedisKey.REDIS_QUEUE.getKey(), JSON.toJSONString(message), Optional.ofNullable(message.getDelay()).map(it -> System.currentTimeMillis() + it).orElse(0L));
}
}
6.监听消息
package com.sword.example.listener;
import com.sword.starter.cache.redis.queue.RedisQueueEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @author Tan
* @version 1.0 2022/5/23
*/
@Component
public class ConsumeListener implements ApplicationListener<RedisQueueEvent> {
@Override
public void onApplicationEvent(RedisQueueEvent event) {
System.out.println("消费者接收到消息:" + event.getSource());
}
}