使用Redis就能实现RabbitMq的消息广播

  • 一.监听类
  • 二.配置文件
  • 三.发送消息



前两天在公司用WebSocket实现了一个订单消息提醒,就是那种“你有一笔新的订单”。功能搞定后,发现一个重大问题,WebSocket是单机的。而公司项目是分布式的,显然WebSocket需要适用分布式的项目。网上查了不少相关资料。


感觉都比较反锁,但还是从中得到了灵感。有一篇文章讲了用RabbitMq来进行消息群发,然后每个服务器拿到消息后都尝试发送。原本我也准备这么实现,准备开干的时候突然想到Redis好像有个类似监听的功能。因为项目里本身就用了Redis缓存。这样不就省去了RabbitMq那部分的开发了吗!!!

redis有一个key失效监听,当你的key失效时触发,但这与我想要的效果不一样,我要的是有消息立即触发。这就是redis的另一种监听 pub/sub消息订阅,我在网上的一篇文章里看到了相关介绍,

一.监听类

package com.xmpp.redis.service;

import com.xmpp.connector.util.Convertor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class ReminderBroadcastEventListener implements MessageListener {
    private Log logger = LogFactory.getLog(ReminderBroadcastEventListener.class);

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 监听redis消息
     *
     * @param message
     * @param bytes
     */
    @Override
    public void onMessage(Message message, byte[] bytes) {
        try {
            byte[] body = message.getBody();
            //反序列化
            String str = (String) redisTemplate.getValueSerializer().deserialize(body);
            String channel=redisTemplate.getValueSerializer().deserialize(bytes);
            logger.info("redis监听到消息内容:" + str);
            logger.info("消息监听通道:" + channel);
        } catch (Exception e) {
            logger.error("-----------------------消息提醒redis监听处理失败-------------------------");
            logger.error(e.getMessage(), e);
        }

    }
}

需要注意的是监听到消息后的处理,redis会对发送的消息进行序列化,所以这里要用redis的序列化工具反序列化。

二.配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:redis="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans		http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/tx		http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

	<!-- 2. redis连接池配置-->
    <bean id="jedisConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!--最大空闲数-->
        <property name="maxIdle" value="#{redis.maxIdle}"/>
        <!--连接池的最大数据库连接数  -->
        <property name="maxTotal" value="#{redis.maxTotal}"/>
        <!--最大建立连接等待时间-->
        <property name="maxWaitMillis" value="#{redis.maxWaitMillis}"/>
        <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟)-->
        <property name="minEvictableIdleTimeMillis" value="#{redis.minEvictableIdleTimeMillis}"/>
        <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3-->
        <property name="numTestsPerEvictionRun" value="#{redis.numTestsPerEvictionRun}"/>
        <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1-->
        <property name="timeBetweenEvictionRunsMillis" value="#{redis.timeBetweenEvictionRunsMillis}"/>
        <!--是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个-->
        <property name="testOnBorrow" value="#{redis.testOnBorrow}"/>
        <!--在空闲时检查有效性, 默认false  -->
        <property name="testWhileIdle" value="#{redis.testWhileIdle}"/>
    </bean>
 
    <!-- 3. redis连接工厂 -->
    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
        <property name="poolConfig" ref="jedisConfig"/>
        <!--IP地址 -->
        <property name="hostName" value="#{redis.hostName}"/>
        <!--端口号  -->
        <property name="port" value="#{redis.port}"/>
        <!--如果Redis设置有密码  -->
        <property name="password" value="#{redis.password}"/>
        <!--客户端超时时间单位是毫秒  -->
        <property name="timeout" value="#{redis.timeout}"/>
        <!--选择具体的数据库-->
        <property name="database" value="#{redis.database}"/>
    </bean>

    <bean id="messageListener" class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
        <constructor-arg>
            <bean class="com.xmpp.redis.service.KeyExpiredListener"/>
        </constructor-arg>
    </bean>

    <bean id="remindListener" class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
        <constructor-arg>
            <bean class="com.xmpp.redis.service.ReminderBroadcastEventListener"/>
        </constructor-arg>
    </bean>

    <bean id="redisContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="messageListeners">
            <map>
                <entry key-ref="messageListener">
                    <bean class="org.springframework.data.redis.listener.PatternTopic">
                        <constructor-arg value="__keyevent@0__:expired"/>
                    </bean>
                </entry>
                <entry key-ref="remindListener">
                    <bean class="org.springframework.data.redis.listener.ChannelTopic">
                        <constructor-arg value="remind:topic"/>
                    </bean>
                </entry>
            </map>
        </property>
    </bean>
    
    <!-- 4. redis操作模板,使用该对象可以操作redis  -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="connectionFactory"/>
        <!--如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!!  -->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
        </property>
        <!-- 开启事务 -->
<!--         <property name="enableTransactionSupport" value="true"/> -->
    </bean>
    
    <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="myBatisConnection"/>
        <!--如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!!  -->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
        </property>
        <!--开启事务  -->
        <!--<property name="enableTransactionSupport" value="true"/>-->
    </bean>

	<!--静态注入中间类,解决RedisCache中RedisTemplate的静态注入,从而使MyBatis实现第三方缓存 -->
	<bean id="cacheTransfer" class="com.xmpp.redis.cache.RedisCacheTransfer">
		<property name="redisTemplate" ref="stringRedisTemplate" />
	</bean>

</beans>

大部分的配置都是之前配置,我添加的监听部分如下:

redis mq 广播 redis 发布广播_spring


将监听类bean至remindListener,然后添加到监听集合里。需要注意的是PatternTopic是用来监听key失效的。ChannelTopic才是我们想要的有消息立即触发的监听。另外<constructor-arg value="remind:topic"/>监听通道好像是可以写通配符的,但我这里只需要固定的就可以满足需求了。

三.发送消息

String channel = "remind:topic";
//其中channel必须为string,而且“序列化”策略也是StringSerializer
//消息内容,将会根据配置文件中指定的valueSerializer进行序列化
//本例中,默认全部采用StringSerializer
//那么在消息的subscribe端也要对“发序列化”保持一致。
redisTemplate.convertAndSend(channel, "from app 1");

到这里就完成了。关于websocket相关的代码我这里没有贴出来,网上有很多相关的文章。