Redis队列升级版利用Spring项目BeanDefinition自动注入
利用Redis实现队列
先进先出队列: https://www.cnblogs.com/LiuFqiang/p/16366813.html
延迟队列: https://www.cnblogs.com/LiuFqiang/p/16592522.html
定长队列: https://www.cnblogs.com/LiuFqian/p/17372463.html
在使用Redis做消息队列的时候,需要配置队列属性的bean,如果自己项目生产自己项目消费,还需要配置消费者的bean,如此一来,项目免不了需要大量重复且不可缺少的配置bean的代码
所以这里借助spring自动装配的原理,自动注入这些bean,减去重复的配置文件
首先申明注解
/**
* 标记是否开启redis队列
*
* @author: liufuqiang
* @date: 2023-10-30 17:00
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(value = {ImportRedisAutoConfigure.class, RedisConfig.class})
public @interface EnableRedisQueue {
String[] basePackages() default {""};
}
如果项目需要开启redis队列的生产或者消费,可以在配置文件加入这个注解
配置文件中新加redis的一些连接与队列信息
/**
* reds队列配置信息
*
* @author: liufuqiang
* @date: 2023-10-30 17:10
*/
@Data
@ConfigurationProperties(prefix = RedisConfig.REDIS_QUEUE)
public class RedisConfig implements Serializable {
public RedisConfig() {
System.out.println(1);
}
public static final String REDIS_QUEUE = "redis1";
private List<QueueInfo> infos;
@Data
public static class QueueInfo implements Serializable{
private String queueName;
private Integer threadNum = 1;
private String threadName;
private String monitorClass;
}
// @Bean
// public RedisTemplate redisTemplate() {
// RedisTemplate redisTemplate = new RedisTemplate();
// Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// redisTemplate.afterPropertiesSet();
// return redisTemplate;
// }
}
/**
* redis队列自动注册
*
* @author: liufuqiang
* @date: 2023-10-30 17:02
*/
@EnableConfigurationProperties(RedisConfig.class)
public class ImportRedisAutoConfigure implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private RedisConfig redisConfig;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
List<RedisConfig.QueueInfo> infos = redisConfig.getInfos();
if (CollUtil.isEmpty(infos)) {
return;
}
for (RedisConfig.QueueInfo queue : infos) {
if (registry.containsBeanDefinition(queue.getQueueName())) {
continue;
}
// 注入队列基本信息bean
AbstractBeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(RedisQueue.class).getBeanDefinition();
ConstructorArgumentValues argumentValues = new ConstructorArgumentValues();
argumentValues.addIndexedArgumentValue(0, queue.getQueueName());
rootBeanDefinition.setConstructorArgumentValues(argumentValues);
registry.registerBeanDefinition(queue.getQueueName(), rootBeanDefinition);
// 注入队列消费service
final String consumerQueue = queue.getMonitorClass();
if (StrUtil.isBlank(consumerQueue)) {
continue;
}
BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(queue.getMonitorClass());
definitionBuilder.setDestroyMethodName("destroy");
AbstractBeanDefinition beanDefinition = definitionBuilder.getBeanDefinition();
ConstructorArgumentValues values = new ConstructorArgumentValues();
values.addIndexedArgumentValue(0, queue);
values.addIndexedArgumentValue(1, rootBeanDefinition);
beanDefinition.setConstructorArgumentValues(values);
registry.registerBeanDefinition(queue.getMonitorClass(), beanDefinition);
}
}
@Override
public void setEnvironment(Environment environment) {
ConfigurationProperties annotation = RedisConfig.class.getAnnotation(ConfigurationProperties.class);
Assert.notNull(annotation, "can not find annotation ConfigurationProperties");
redisConfig = Binder.get(environment).bind(annotation.prefix(), RedisConfig.class).get();
}
}
队列接口类
/**
* 队列接口类
*
* @author: liufuqiang
* @date: 2023-10-31 14:50
*/
public interface IQueue<E> extends Queue<E>{
@Override
default boolean add(E e) {
return false;
}
@Override
default boolean offer(E e) {
return false;
}
@Override
default E remove() {
return null;
}
@Override
default E poll() {
return null;
}
@Override
default E element() {
return null;
}
@Override
default E peek() {
return null;
}
@Override
default int size() {
return 0;
}
@Override
default boolean isEmpty() {
return false;
}
@Override
default boolean contains(Object o) {
return false;
}
@Override
default Iterator<E> iterator() {
return null;
}
@Override
default Object[] toArray() {
return new Object[0];
}
@Override
default <T> T[] toArray(T[] a) {
return null;
}
@Override
default boolean remove(Object o) {
return false;
}
@Override
default boolean containsAll(Collection<?> c) {
return false;
}
@Override
default boolean addAll(Collection<? extends E> c) {
return false;
}
@Override
default boolean removeAll(Collection<?> c) {
return false;
}
@Override
default boolean retainAll(Collection<?> c) {
return false;
}
@Override
default void clear() {
}
@Override
default boolean removeIf(Predicate<? super E> filter) {
return false;
}
@Override
default Spliterator<E> spliterator() {
return null;
}
@Override
default Stream<E> stream() {
return null;
}
@Override
default Stream<E> parallelStream() {
return null;
}
@Override
default void forEach(Consumer<? super E> consumer) {
}
}
先入先出的队列实现
/**
* @author: liufuqiang
* @date: 2023-10-31 15:44
*/
public class RedisQueue implements IQueue<String>{
private String queueName;
private StringRedisTemplate stringRedisTemplate ;
public RedisQueue(String queueName, StringRedisTemplate redisTemplate) {
this.queueName = queueName;
this.stringRedisTemplate = redisTemplate;
}
@Override
public boolean offer(String body) {
return stringRedisTemplate.opsForList().rightPush(queueName, body) > 0;
}
@Override
public String poll() {
return "new mshsssss";
}
}
/**
* redis队列抽象处理
*
* @author: liufuqiang
* @date: 2023-10-30 18:31
*/
@Slf4j
public abstract class AbstractMonitorQueue implements MonitorRedisQueue, InitializingBean {
private RedisConfig.QueueInfo queueInfo;
private RedisQueue redisQueue;
private ExecutorService executorService;
private AtomicBoolean isRunning = new AtomicBoolean(false);
public AbstractMonitorQueue(RedisConfig.QueueInfo queueInfo, RedisQueue redisQueue) {
this.queueInfo = queueInfo;
this.redisQueue = redisQueue;
}
@SneakyThrows
@Override
public boolean process() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNamePrefix(queueInfo.getThreadName()).build();
executorService = Executors.newFixedThreadPool(queueInfo.getThreadNum(), threadFactory);
for (int i = 0; i < queueInfo.getThreadNum(); i++) {
executorService.execute(() -> {
while (isRunning.get()) {
String queueMsg = getQueueMsg();
if (StrUtil.isBlank(queueMsg)) {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
this.execute(queueMsg);
log.info("queueMsg;{}", queueMsg);
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
return true;
}
public String getQueueMsg() {
return redisQueue.poll();
}
public void beforeProcess(String msg) {
}
public void afterProcess(String msg) {
}
public abstract void execute(String msg);
@Override
public void afterPropertiesSet() throws Exception {
log.info("开始执行任务");
isRunning.set(true);
this.process();
}
public void destroy() {
System.out.println("开始销毁队列");
isRunning.set(false);
executorService.shutdown();
}
}
队列消费接口
/**
* redis队列监控
*
* @author: liufuqiang
* @date: 2023-10-30 18:17
*/
public interface MonitorRedisQueue {
/**
* 执行队列逻辑
* @param msg
* @return
*/
boolean process();
}
具体队列的消费实现类如下
/**
*
*
* @author: liufuqiang
* @date: 2023-10-30 18:37
*/
@Slf4j
public class MonitorRedisQueueServiceImpl extends AbstractMonitorQueue {
public MonitorRedisQueueServiceImpl(RedisConfig.QueueInfo queueInfo, RedisQueue redisQueue) {
super(queueInfo, redisQueue);
}
@Override
public void execute(String msg) {
log.info(this.getClass().getName() + "执行具体逻辑");
}
}
以上文件可以集成到项目公用jar包,也可以放在公共模块,项目需要使用时引入模块的jar包
在使用的使用只需要开启注解@EnableRedisQueue
并且在配置文件填写需要队列的配置信息,如redis实际消费的key,消费线程的名字,消费线程的数量,消费者的className,如果不需要消费者,只生产消息,则只需要配置queue-name
redis1:
infos:
- queue-name: order111
thread-name: thread111
thread-num: 10
monitor-class: org.dromara.sms4j.example.service.queue.MonitorRedisQueueServiceImpl
- queue-name: order222
消费者只需要新建消费类并且继承AbstractMonitorQueue即可
如果需要进行生产消息,注入的时候 @Qualifier(value = "order111") value选择为配置文件中的queue-name
这里我们新建测试10条测试数据,两个线程进行消费,可以看到消费完成数据之后一直lpop循环取数据
延迟队列实现如下,
配置队列新增属性isDelay
/**
* redis延迟队列实现
* 延迟时间为13位时间戳
*
* @author: liufuqiang
* @date: 2023-11-02 09:22
*/
public class RedisDelayQueue implements IQueue<String> {
private String queueName;
private StringRedisTemplate stringRedisTemplate;
public RedisDelayQueue(String queueName, StringRedisTemplate stringRedisTemplate) {
this.queueName = queueName;
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 添加队列信息
*
* @param msg 队列信息
* @param score 延迟之后的时间
* @return
*/
@Override
public boolean offer(String msg, Long score) {
return stringRedisTemplate.opsForZSet().add(queueName, msg, score);
}
/**
* 添加队列信息
*
* @param msg 队列信息
* @param delayMinutes 延迟分钟数
* @return
*/
public boolean offer(String msg, Integer delayMinutes) {
long delaySeconds = System.currentTimeMillis() + TimeUnit.MINUTES.toSeconds(delayMinutes) * 1000;
return this.offer(msg, delaySeconds);
}
@Override
public String poll() {
Set<String> range = stringRedisTemplate.opsForZSet().rangeByScore(queueName, 0, System.currentTimeMillis());
if (CollUtil.isNotEmpty(range)) {
String msg = range.iterator().next();
if (remove(msg)) {
return msg;
}
}
return null;
}
public boolean remove(String msg) {
return stringRedisTemplate.opsForZSet().remove(queueName, msg) > 0;
}
}
这里新建五条测试数据进行延迟消费
可以看到在刚开始zadd完数据之后,一直zrangebyscore并且进行zrem数据
还需要改进的地方
- 抽象类添加了 beforeProcess()与afterProcess()但是具体逻辑并未实现,before里面可以进行日志打印,数据落库,after里面可以捕获异常进行告警,
- 未添加重试逻辑。队列消息处理失败之后添加重试逻辑,如果重试几次还是失败,进行错误数据落库等
- 延迟队列zrange与zrem造成两次redis连接,改造成lua脚本进行返回并删除数据
测试案例
git@gitee.com:LiuFqiang/springboot-redis.git