一、背景

     公司的服务是集群的模式,也就是一个服务多台服务器部署。

     在A服务器调用T接口处理逻辑后,需要同步通知B服务器清空服务器本地缓存。考虑了下,觉得redis的发布/订阅模式很适合。A服务器发布,BCD服务器订阅相关的topic,A服务器一有变动,就推送到redis,订阅了对应topic的BCD就能感知到,获取到相同的一份数据,BCD都进行逻辑处理

二、代码实现

1、RedisConfig核心类,实现了Redis连接,订阅以及发布配置

import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class MyRedisMQConfig {

    private int corePoolSize = 10;
    private int maxPoolSize = 20;
    private int queueCapacity = 900000;

    @Bean
    public Executor redisMqAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("redisMqThread-");
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.initialize();
        return TtlExecutors.getTtlExecutor(executor);
    }

    /**
     * 消息监听器,使用MessageAdapter可实现自动化解码及方法代理
     *
     * @return
     */
    @Bean
    public MessageListenerAdapter listenerAdapter(MyMsgSubscriber myMsgSubscriber) {
        MessageListenerAdapter adapter = new MessageListenerAdapter(myMsgSubscriber, "onMessage");
        return adapter;
    }

    /**
     * redis消息监听器容器
     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
     * @return
     */

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,Executor redisMqAsyncExecutor,
                                                   MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setTaskExecutor(redisMqAsyncExecutor);
        container.addMessageListener(listenerAdapter, new PatternTopic("myRedisTopic"));
        return container;
    }
}

2、订阅者-消费消息

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.msedu.kaoshi.client.vo.ClientExamVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;


/**
 * 订阅者-消费处理业务逻辑
 *
 * @author zhengkegui
 * @date 2022/2/25 15:43
 */
@Component
public class MyMsgSubscriber {
    private static final Logger logger = LoggerFactory.getLogger(MyMsgSubscriber.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void onMessage(String message, String pattern) {

        logger.info("请求参数:{}", message);
        try {
            //将从channel拿到的message进行json转换为对象
            ClientExamVO dto = JSONObject.parseObject((String) JSON.parse(message), ClientExamVO.class);
            //做业务逻辑 ...
            logger.info("MyMsgSubscriber成功:{}", dto);
        } catch (Exception e) {
            logger.error("MyMsgSubscriber失败", e);
        }
    }
}

3、发布者-生产消息,将message请求放入redis channel中

/**
     * 发布请求到redis channel,等待订阅者获取消费
     *
     * @return
     */
    @RequestMapping("/test")
    public RespMessage test() {
        for (int i = 0; i < 10; i++) {
            String channel = new ChannelTopic("myRedisTopic").getTopic();
            ClientExamVO vo = new ClientExamVO();
            vo.setTeExamId(Long.parseLong("" + i));
            String message = JSON.toJSONString(vo);
            redisTemplate.convertAndSend(channel, message);
        }
        return RespHandler.success();
    }

三、redis的发布订阅原理

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

Redis 客户端可以订阅任意数量的频道。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

springboot订阅rediskey过期消息 redis发布订阅springboot_ListenerAdapter

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

springboot订阅rediskey过期消息 redis发布订阅springboot_ListenerAdapter_02

发布者订阅者模式,区别于生产者消费者模式:

定义:
        生产者消费者模式(点对点):生产者生产消息放到队列里,多个消费者同时监听队列,谁先抢到消息谁就会从队列中取走消息;即对于每个消息只能被最多一个消费者拥有

springboot订阅rediskey过期消息 redis发布订阅springboot_ListenerAdapter_03


        发布者订阅者模式:发布者生产消息放到队列里,多个监听队列的消费者都会收到同一份消息;即正常情况下每个消费者都能收到一份消息。 

springboot订阅rediskey过期消息 redis发布订阅springboot_RedisMessage_04

 

四、在哪些业务场景使用Redis发布订阅

1、异步消息通知

比如渠道在调支付平台的时候,我们可以用回调的方式给支付平台一个我们的回调接口来通知我们支付状态,还可以利用Redis的发布订阅来实现。比如我们发起支付的同时订阅频道`pay_notice_` + `wk` (假如我们的渠道标识是wk,不能让其他渠道也订阅这个频道),当支付平台处理完成后,支付平台往该频道发布消息,告诉频道的订阅者该订单的支付信息及状态。收到消息后,根据消息内容更新订单信息及后续操作。

当很多人都调用支付平台时,支付时都去订阅同一个频道会有问题。比如用户A支付完订阅频道`pay_notice_wk`,在支付平台未处理完时,用户B支付完也订阅了`pay_notice_wk`,当A收到通知后,接着B的支付通知也发布了,这时渠道收不到第二次消息发布。因为同一个频道收到消息后,订阅自动取消,也就是订阅是一次性的。

所以我们订阅的订单支付状态的频道就得唯一,一个订单一个频道,我们可以在频道上加上订单号`pay_notice_wk`+orderNo保证频道唯一。这样我们可以把频道号在支付时当做参数一并传过去,支付平台处理完就可以用此频道发布消息给我们了。(实际大多接口用回调通知,因为用Redis发布订阅限制条件苛刻,系统间必须共用一套Redis)

springboot订阅rediskey过期消息 redis发布订阅springboot_channel_05

2、任务通知

比如通过跑批系统通知应用系统做一些事(跑批系统无法拿到用户数据,且应用系统又不能做定时任务的情况下)。

如每天凌晨3点提前加载一些用户的用户数据到Redis,应用系统不能做定时任务,可以通过系统公共的Redis来由跑批系统发布任务给应用系统,应用系统收到指令,去做相应的操作。

springboot订阅rediskey过期消息 redis发布订阅springboot_发布订阅_06

这里需要注意的是在线上集群部署的情况下,所有服务实例都会收到通知,都要做同样的操作吗?完全没必要。可以用Redis实现锁机制,其中一台实例拿到锁后执行任务。另外如果任务比较耗时,可以不用锁,可以考虑一下任务分片执行。当然这不在本文的讨论范畴,这里不在赘述。

3、参数刷新加载

众所周知,我们用Redis无非就是将系统中不怎么变的、查询又比较频繁的数据缓存起来,例如我们系统首页的轮播图啊,页面的动态链接啊,一些系统参数啊,公共数据啊都加载到Redis,然后有个后台管理系统去配置修改这些数据。

打个比方我们首页的轮播图要再增加一个图,那我们就在后管系统加上,加上就完事了吗?当然没有,因为Redis里还是老数据。那你会说不是有过期时间吗?是的,但有的过期时间设置的较长如24小时并且我们想立即生效怎么办?这时候我们就可以利用Redis的发布订阅机制来实现数据的实时刷新。当我们修改完数据后,点击刷新按钮,通过发布订阅机制,订阅者接收到消息后调用重新加载的方法即可。

springboot订阅rediskey过期消息 redis发布订阅springboot_发布订阅_07