使用Redis发布订阅功能实现动态注册监听及异步获取结果

介绍及目标

        发布/订阅在我们的系统开发中非常常用,redis也提供了这项功能,但实现的是比较基本的功能,对于可靠消息的发布订阅及消息堆积这些功能Redis是无法做到的。但我们依旧可以用来处理很多业务场景,比如:

        我们系统夸服务去调用某些请求,但被调用的服务又没办法及时响应,而需要异步返回结果的时候,我们使用同步阻塞的话比较浪费资源拉低系统的吞吐能力,对于一些耗时的操作,因为长时间的等待还容易导致请求超时处理失败(实际可能已经处理成功了)。

        对于上述的场景,我们可以考虑用redis的发布订阅功能去处理。

思路

        基本思路是:

        服务A先动态订阅主题1,随后调用服务B并把主题1的地址带过去,随后等待主题1返回消息。服务A订阅的主题设置超时机制,避免被调用服务长期不响应导致业务卡死。

        服务B收到请求之后异步处理消息,在处理完业务之后通过主题1通知到服务A。

        服务A收到服务B发送的消息之后在唤起等待的线程处理接下来的业务即可。这即有效的实现了应用的解耦,也提高了我们系统的吞吐能力。流程图如下:

核心代码

RedisChannelListenerConfig

        这个是向spring容器注入redis发布订阅需要的bean

public class RedisChannelListenerConfig {

    @Bean
    public RedisMessageListenerContainer redisContainer(RedisConnectionFactory factory) {
        final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        return container;
    }

}

 RedisChannelService

        封装发布订阅的方法

@Import(RedisChannelListenerConfig.class)
public class RedisChannelService {

    /**
     * 分割符
     */
    private static final String PARTITION = "_";

    @Resource
    private RedisMessageListenerContainer container;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 发布消息
     * @param channel
     * @param message
     */
    public void publish(String channel, Object message) {
        try {
            redisTemplate.convertAndSend(channel, message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 添加订阅渠道(单渠道)
     *
     * @param listener 监听器
     * @param channel
     */
    public void addSubscribe(MessageListener listener, String channel) {
        container.addMessageListener(listener, new ChannelTopic(channel));
    }

    /**
     * 模式订阅(支持通配符)
     *
     * @param listener
     * @param channel
     */
    public void addPSubscribe(MessageListener listener, String channel) {
        container.addMessageListener(listener, new PatternTopic(channel));
    }

    /**
     * 模式订阅(支持通配符)
     *
     * @param listener
     * @param channels 多个通配主题
     */
    public void addPSubscribe(MessageListener listener, List<String> channels) {
        List<PatternTopic> list = new ArrayList<>();
        for (String channel : channels) {
            list.add(new PatternTopic(channel));
        }
        container.addMessageListener(listener, list);
    }

    /**
     * 移除订阅
     *
     * @param listener
     */
    public void removeSubscribe(MessageListener listener) {
        container.removeMessageListener(listener);
    }

    /**
     * 格式化消息
     * @param isSuccess
     * @param message
     * @return
     */
    public String formatMessage(Boolean isSuccess, String message){
        return new StringBuilder().append(isSuccess.toString()).append(PARTITION).append(message).toString();
    }

    /**
     * 获取消息值
     * @param message
     * @return
     */
    public RedisChannelMessage getMessage(String message){
        if(message == null || message.indexOf(PARTITION) == -1){
            return null;
        }
        String[] split = message.split(PARTITION);
        return new RedisChannelMessage(Boolean.valueOf(split[0]), split[1]);
    }
}

RedisChannelMessage

@Data
@AllArgsConstructor
public class RedisChannelMessage {
    private boolean isSuccess;
    private String message;
}

RedisChannelListener

        自定义监听器,设置topic并获取返回值

/**
 * channel订阅模式通用类
 *
 * @author: yan
 * @date: 2023/04/27
 */
public class RedisChannelListener<T> implements Consumer<T> {

    private String channel;
    private volatile T value;
    private Thread thread;

    public RedisChannelListener(String channel, Thread thread) {
        this.channel = channel;
        this.thread = thread;
    }

    @Override
    public void accept(T t) {
        this.value = t;
    }

    public String getChannel() {
        return channel;
    }

    public T getValue() throws TimeoutException {
        return getValue(10, TimeUnit.SECONDS);
    }

    public Thread getThread() {
        return this.thread;
    }

    public T getValue(long time, TimeUnit timeUnit) throws TimeoutException {
        long end = System.nanoTime() + timeUnit.toNanos(time);
        while (System.nanoTime() < end) {
            if (value != null) {
                return value;
            }
            LockSupport.parkNanos(1000 * 1000 * 1000);
        }
        throw new TimeoutException();
    }
}

        以上就是对发布订阅模式的封装了

使用

        以上配置默认不会注入容器,可根据项目需要初始化

RedisChannelConfig

        初始化类

@Slf4j
@Component
@Import(RedisChannelService.class)
public class RedisChannelConfig {
    @Resource
    private RedisChannelService redisChannelService;
    private static Cache<String, RedisChannelListener<String>> listenerMap = CacheBuilder.newBuilder()
            // 300秒自动过期
            .expireAfterWrite(300, TimeUnit.SECONDS)
            .build();


    @PostConstruct
    public void init(){
        // 开启全局订阅
        final MessageListener messageListener = (message, bytes) -> {
            String channel = new String(message.getChannel());
            System.out.println(channel);
            RedisChannelListener<String> listener = listenerMap.getIfPresent(channel);
            if (listener != null) {
                listener.accept(message.toString());
                listenerMap.invalidate(channel);
            }
        };
        // 这里限定统配订阅
        ArrayList<String> channels = Lists.newArrayList("test_*");
        redisChannelService.addPSubscribe(messageListener, channels);
    }


    public void registerListener(RedisChannelListener listener) {
        listenerMap.put(listener.getChannel(), listener);
    }

    public void removeListener(RedisChannelListener listener) {
        listenerMap.invalidate(listener.getChannel());
    }
}

RedisChannelTest

        使用方式,模拟调用远程服务

/**
 * @author yan
 * @date 2023-10-23
 */
@Service
public class RedisChannelTest {
    @Resource
    private RedisChannelConfig redisChannelConfig;
    @Resource
    private RedisChannelRemote redisChannelRemote;

    public boolean sendTest() throws TimeoutException {
        // 先订阅test_001消息
        String channel = "test_001";
        RedisChannelListener<String> listener = new RedisChannelListener<>(channel, Thread.currentThread());
        redisChannelConfig.registerListener(listener);

        // 调用外部服务
        String handle = redisChannelRemote.handle("test,test", channel);
        System.out.println(handle);

        // 获取结果
        String value = listener.getValue();
        System.out.println(value);
        return true;
    }
}

RedisChannelRemote

        远程服务

/**
 * @author yan
 * @date 2023-10-23
 */
@Service
public class RedisChannelRemote {
    @Resource
    private RedisChannelService redisChannelService;

    public String handle(String msg, String topic){
        new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            redisChannelService.publish(topic, "back:" + msg);
        }).start();
        return "success";
    }
}

RedisController

        最后写个controller试一下

@RestController
@RequestMapping(value = "redis")
public class RedisController {

    @Resource
    private RedisChannelTest redisChannelTest;


    @GetMapping("channel")
    public String channel() throws TimeoutException {
        if (redisChannelTest.sendTest()) {
            return "success";
        }
        return "fail";
    }

}

总结        

        以上就是使用Redis发布订阅功能实现动态注册监听及异步获取结果。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis发布订阅Redis提供的一种消息传递模式,它允许多个客户端订阅一个或多个频道,同时可以通过发布消息的方式将消息发送给订阅了相应频道的客户端。 要实现Redis发布订阅,可以按照以下步骤进行操作: 1. 客户端订阅频道: 客户端使用SUBSCRIBE命令来订阅一个或多个频道。例如,使用以下命令来订阅一个名为"channel1"的频道: ``` SUBSCRIBE channel1 ``` 2. 客户端发布消息: 客户端使用PUBLISH命令来发布消息到指定的频道。例如,使用以下命令来向"channel1"频道发布一条消息: ``` PUBLISH channel1 "Hello, World!" ``` 3. 服务器推送消息: 一旦有客户端订阅了某个频道并且有其他客户端向该频道发布消息,Redis服务器会将消息推送给所有订阅了该频道的客户端。 4. 客户端接收消息: 客户端可以通过监听服务器推送的消息来接收订阅的频道中的消息。当有消息到达时,客户端会收到一个特定格式的响应,可以通过解析响应来获取到具体的消息内容。 需要注意的是,Redis发布订阅是一种异步通信方式,发布者和订阅者之间是解耦的,发布者无需知道订阅者的具体信息。同时,Redis支持多对多的消息传递模式,即多个发布者可以向同一个频道发布消息,多个订阅者可以同时订阅同一个频道。 希望以上内容对你有所帮助!如果还有其他问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值