前言

         redis本身也有发布订阅的模式,但是如果想要实现例如rocketmq或者rabbitmq的延时任务功能要怎么做呢,目前比较流行的做法有两种,一种是使用sortedset数据模型,把超时时间设置为score,然后系统启动一个定时任务定时去检查score超时的key然后把key取出来,再进行下一阶段的任务;第二种方法就是利用redis本身的通知机制,当key到期的时候会进行通知,通过捕获通知的信息来实现延时通知的效果。本文使用第二种方式,并且通过注解和反射使得整体功能更具有扩展性和适用性。首先看下效果

初始化监听器成功 com.loveprogrammer.redismq.listener.handler.impl.TestHandler
初始化监听器成功 com.loveprogrammer.redismq.listener.handler.impl.Test2Handler
c.l.r.l.RedisKeyExpirationListener       :获取到延时任务key mq:test:execute:key1
c.l.r.listener.handler.impl.TestHandler  : test-入参是字符串:hello
c.l.r.l.RedisKeyExpirationListener       :获取到延时任务key mq:test:execute2:key2
c.l.r.listener.handler.impl.TestHandler  : test-入参是数字:2
c.l.r.l.RedisKeyExpirationListener       :获取到延时任务key mq:test2:execute:key3
c.l.r.l.handler.impl.Test2Handler        : test2-入参是对象:张三 20
c.l.r.l.RedisKeyExpirationListener       : 获取到延时任务key mq:test2:execute2:key4
c.l.r.l.handler.impl.Test2Handler        : test2-入参是浮点数:3.1415926

目标

           1、通过注解自动注册为监听器

           2、一个topic下支持多个tag,不同的tag指向不同的方法执行。

实现

第一步:初始化项目

        创建一个空的springboot项目,pom依赖如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- redis 缓存操作 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--常用工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!--反射工具类 -->
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.9.10</version>
        </dependency>

        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.34</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

第二步:接入redis

首先把redis的配置信息写入配置文件

# Spring配置
spring:
  # redis 配置
  redis:
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password: 
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

编写配置类

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

        redis操作工具类:RedisCache这里省略,大家可以去源码上下载,源码在文末

第三步:创建注解

@Retention(RetentionPolicy.RUNTIME)
public @interface Listeners {

    /**
     * 转换为DefaultMqPushConsumer后订阅的topic
     * 默认为“DEFAULT_TOPIC”
     */
    String topic() default "DEFAULT_TOPIC";
}
@Retention(RetentionPolicy.RUNTIME)
public @interface MQListener {

    /**
     * 订阅的tag
     */
    String tag() default "*";

    /**
     * 请求方消息类型
     */
    Class<?> messageClass() default Object.class;

}

第四步:创建redis的监听

@Configuration
public class RedisListenerConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

 第五步:设计一个接口IHanlder,其实现类为实际监听业务类

             这里接口及抽象实现类无内容,后续方便扩展

public interface IHandler {

}
public abstract class BaseHandler implements IHandler{



}

       创建一个监听业务类备用,这里类上的注解表示监听的topic,方法上的注解表示监听的tag以及入参的类型,方便进行类型转换。

@Component
@Listeners(topic = "test")
public class TestHandler extends BaseHandler {
    private static final Logger logger = LoggerFactory.getLogger(TestHandler.class);

    @MQListener(tag = "execute",messageClass = String.class)
    public Boolean execute(String key, String message) {
        logger.info("test-入参是字符串:{}" ,message);
        return Boolean.TRUE;
    }

    @MQListener(tag = "execute2",messageClass = Integer.class)
    public Boolean execute2(String key, Integer number) {
        logger.info("test-入参是数字:{}" ,number);
        return Boolean.TRUE;
    }
}

  设计一个工厂方法,自动注册这些监听业务类

 

@Component
@Slf4j
public class HandlerFactory implements CommandLineRunner {

    public Map<String, Class> handlerMap = new HashMap<>();

    @Override
    public void run(String... args) throws Exception {
        // 找到所有实现类
        Reflections reflections = new Reflections("com.loveprogrammer.redismq.listener.handler.impl");
        // 获取在指定包扫描的目录所有的实现类
        Set<Class<? extends BaseHandler>> classes = reflections.getSubTypesOf(BaseHandler.class);
        for (Class<? extends IHandler > aClass : classes) {
            try {
                Listeners listeners = aClass.getAnnotation(Listeners.class);
                String topic = listeners.topic();
                handlerMap.put(topic, aClass);
                log.info("初始化监听器成功 {}",aClass.getName());
            } catch (Exception e) {
                log.error("初始化" + aClass.getName() + "监听器失败",e);
            }
        }
    }
}

第六步:监听主入口

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    @Autowired
    private MqUtil mqUtil;

    @Autowired
    private HandlerFactory handlerFactory;

    @Autowired
    private RedisCache redisCache;

    private static final Logger logger = LoggerFactory.getLogger(RedisKeyExpirationListener.class);

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    // 延时任务执行线程池
    private ThreadPoolExecutor executor = new ThreadPoolExecutor(
            3,
            10,
            0L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2048),
            new ThreadFactoryBuilder()
                    .setNameFormat("hyh-mq-pool-%d").build(),
            new ThreadPoolExecutor.AbortPolicy()
    );

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        if (!expiredKey.startsWith("mq") || StringUtils.isBlank(expiredKey)) {
            return;
        }
        // 分布式锁-防止多次消费的情况
        String lock = redisCache.tryLock(expiredKey, 60);
        if (lock == null) {
            return;
        }
        // 获得值
        String value = mqUtil.getMessage(expiredKey);
        if (value == null) {
            value = "";
        }
        logger.info("获取到延时任务key {}", expiredKey);
        // 都是按照 mq:topic:tag:key
        String[] keys = expiredKey.split(":");
        if (keys.length < 4) {
            return;
        }
        String type = keys[1];
        String tag = keys[2];
        String key = keys[3];
        Class handler = handlerFactory.handlerMap.get(type);
        if (handler == null) {
            logger.warn("未获取到指定的任务对象,key {}", expiredKey);
            return;
        }
        String finalValue = value;
        String lockValue = lock;
        executor.execute(() -> {
            Boolean execute = false;
            try {
                Object bean = SpringContextHelper.getBean(handler);
                // 找到tag 遍历methods
                Method[] methods = handler.getMethods();
                for (Method method : methods) {
                    MQListener mqListener = method.getAnnotation(MQListener.class);
                    if (tag.equals(mqListener.tag())) {
                        Class<?> aClass = mqListener.messageClass();
                        String name = aClass.getName();
                        // 先处理基本类型
                        if("java.lang.String".equals(name)) {
                            method.invoke(bean, key, finalValue);
                        }else if("java.lang.Long".equals(name) ) {
                            Long object = Long.parseLong(finalValue);
                            method.invoke(bean, key, object);
                        }else if("java.lang.Integer".equals(name) ) {
                            Integer object = Integer.parseInt(finalValue);
                            method.invoke(bean, key, object);
                        }else if("java.lang.Short".equals(name) ) {
                            Short object = Short.parseShort(finalValue);
                            method.invoke(bean, key, object);
                        }else if("java.lang.Byte".equals(name) ) {
                            Byte object = Byte.parseByte(finalValue);
                            method.invoke(bean, key, object);
                        }else if("java.lang.Double".equals(name)) {
                            Double object = Double.parseDouble(finalValue);
                            method.invoke(bean, key, object);
                        }else if("java.lang.Float".equals(name)) {
                            Float object = Float.parseFloat(finalValue);
                            method.invoke(bean, key, object);
                        }
                        else{
                            Object object = JSON.parseObject(finalValue, aClass);
                            method.invoke(bean, key, object);
                        }
                        execute = true;
                        break;
                    }
                }
                if (!execute) {
                    logger.error("执行延时任务失败,60秒钟后重试");
                    mqUtil.sendMessage(expiredKey, finalValue, 60L, TimeUnit.SECONDS);
                }
            } catch (Exception e) {
                logger.error("执行延时任务失败,60秒钟后重试", e);
                mqUtil.sendMessage(expiredKey, finalValue, 60L, TimeUnit.SECONDS);
            }
            if (execute) {
                mqUtil.releaseMessage(expiredKey);
            }
            redisCache.unlock(expiredKey, lockValue);
        });


    }

    @PreDestroy
    public void destroy() {
        shutdownAsyncManager();
    }

    /**
     * 停止异步执行任务
     */
    private void shutdownAsyncManager() {
        try {
            logger.info("====关闭延时任务线程池====");
            executor.shutdown();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }
}

               对上面的代码进行简单说明,通过获取到的key,解析得到topic和tag,然后通过第五步的工厂获得topic对应的class,通过反射去调用对应的method。之所以使用了线城池是不希望流量在这里卡住,而是只作为一个任务的分发。

第七步:延时队列工具

@Configuration
public class MqUtil {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String SUFFIX = "_temp";

    public void sendMessage(String topic, String tag, String key, String value, Long timeout, TimeUnit timeUnit){
        String realKey = "mq"+":"+topic +":"+ tag + ":"+  key;
        String realKey2 = "mq"+":"+topic +":"+ tag + ":"+  key + SUFFIX;
        redisTemplate.opsForValue().set(realKey,value,timeout,timeUnit);
        redisTemplate.opsForValue().set(realKey2,value);
    }

    public void sendMessage(String messageKey,String value, Long timeout, TimeUnit timeUnit){
        String realKey = messageKey;
        String realKey2 = messageKey + SUFFIX;
        redisTemplate.opsForValue().set(realKey,value,timeout,timeUnit);
        redisTemplate.opsForValue().set(realKey2,value);
    }

    public String getMessage(String messageKey){
        String realKey = messageKey + SUFFIX;
        return redisTemplate.opsForValue().get(realKey);
    }

    public void releaseMessage(String messageKey){
        redisTemplate.delete(messageKey);
        String realKey = messageKey + SUFFIX;
        redisTemplate.delete(realKey);
    }
}

第八步:测试

        编写一个启动方法来测试。代码如下:

@Component
public class CommonRunner implements ApplicationRunner {

    @Autowired
    private MqUtil mqUtil;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        mqUtil.sendMessage("test","execute","key1","hello",2000L, TimeUnit.MILLISECONDS);
        mqUtil.sendMessage("test","execute2","key2","2",3000L, TimeUnit.MILLISECONDS);
        mqUtil.sendMessage("test2","execute","key3", JSON.toJSONString(new User("张三","20")),4000L, TimeUnit.MILLISECONDS);
        mqUtil.sendMessage("test2","execute2","key4","3.1415926",5000L, TimeUnit.MILLISECONDS);
    }
}

               其余的代码大家请去仓库下载:redis-mq-demo: 使用redis实现延时消息的功能