Redis的Stream + WebSocket实现消息的实时推送

目录

一、引入jar

二、yml配置

三、配置类

websocket配置

redis序列化配置

redis stream配置-绑定消费者监听类

四、写监听类

五、WebSocket接口类编写

六、生产者生产消息到redis的stream中

七、测试

八、使用webSocket实现对数据的实时推送详解

1.什么是webSocket?

2.实时推送数据的实现方式以及应用场景

实现方式

九、封装工具类

十、补充


一、引入jar

         <!--WebSocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

二、yml配置

server:
  port: 9085
spring:
  profiles:
    active: dev
  application:
    name: websocket
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 3000ms
    password: 123456
    database: 1
ws:
  listen:
    sever:
      name: websocket

三、配置类

websocket配置

@Configuration
public class WebSocketConfig {

    /**
     * 注入ServerEndpointExporter
     * 这个bean会自动注册 使用了@ServerEndpoint注解声明的websocket endpoint
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class ServerEncoder implements Encoder.Text<ValueHolder> {

    @Override
    public void destroy() {
        // TODO Auto-generated method stub
        // 这里不重要
    }

    @Override
    public void init(EndpointConfig arg0) {
        // TODO Auto-generated method stub
        // 这里也不重要
    }

    /*
     *  encode()方法里的参数和Text<T>里的T一致,如果你是Student,这里就是encode(Student student)
     */
    @Override
    public String encode(Result类 responseMessage) throws EncodeException {
        try {
            /*
             * 这里是重点,只需要返回Object序列化后的json字符串就行
             * 你也可以使用gosn,fastJson来序列化。
             */
            return JSONObject.toJSONString(responseMessage, SerializerFeature.WriteDateUseDateFormat);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

redis序列化配置

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
@Configuration
public class RedisSerializerConfig {
    /**
     * 设置redis序列化规则
     */
    @Bean
    public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(){
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }

    /**
     * RedisTemplate配置
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory,
                                                       Jackson2JsonRedisSerializer jackson2JsonRedisSerializer) {

        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        RedisSerializer<?> stringSerializer = new StringRedisSerializer();
        // key序列化
        redisTemplate.setKeySerializer(stringSerializer);
        // value序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash key序列化
        redisTemplate.setHashKeySerializer(stringSerializer);
        // Hash value序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

redis stream配置-绑定消费者监听类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import java.time.Duration;
import java.util.Collections;

@Slf4j
@Configuration
public class RedisStreamConfig {
    @Autowired
    private Listener Consummer;

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Bean
    public StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String,String,String>> workOrderListenerOptions(){

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        return StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                .builder()
                //block读取超时时间
                .pollTimeout(Duration.ofSeconds(3))
                //count 数量(一次只获取一条消息)
                .batchSize(1)
                //序列化规则
                .serializer( stringRedisSerializer )
                .build();
    }

    /**
     * 开启监听器接收消息   注意此方法的结尾是Container
     */
    @Bean
    public StreamMessageListenerContainer<String,MapRecord<String,String,String>> ListenerContainer(RedisConnectionFactory factory,
                                                                                                         StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String,String,String>> streamMessageListenerContainerOptions){

        StreamMessageListenerContainer<String,MapRecord<String,String,String>> listenerContainer = StreamMessageListenerContainer.create(factory,
                streamMessageListenerContainerOptions);
        //如果 流不存在 创建 stream 流
        if( !redisTemplate.hasKey(stringKey){
            redisTemplate.opsForStream().add(streamKey, Collections.singletonMap("", ""));
            log.info("初始化stream {} success", streamKey);
        }
        //创建消费者组
        try {
            redisTemplate.opsForStream().createGroup(streamKey, groupName);
        } catch (Exception e) {
            log.info("消费者组 {} 已存在", groupName);
        }
        //注册消费者 消费者名称,从哪条消息开始消费,消费者类
        // > 表示没消费过的消息
        // $ 表示最新的消息
        listenerContainer.receive(
                Consumer.from(groupName, consumerName),
                StreamOffset.create(stringKey, ReadOffset.lastConsumed()),
                Consummer);   //Consummer为消费者的监听类
        listenerContainer.start();
        return listenerContainer;
    }
}

四、写监听类

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;

@Slf4j
@Component
public class Listener implements StreamListener<String, MapRecord<String, String, String>> {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private WebsocketService websocketService;

    //redis的stream数据发生变化 监听到的数据 我存储的是map形式如<"count":"6">
    @Override
    public void onMessage(MapRecord<String, String, String> message) {
        log.info("stream名称-->{}", message.getStream());
        log.info("消息ID-->{}", message.getId());
        log.info("消息内容-->{}", message.getValue());
        Map<String, String> msgMap = message.getValue();
        String changeCounts = msgMap.get("counts");
        //消息确认
        stringRedisTemplate.opsForStream().acknowledge(streamKey, groupName, message.getId());
        //发送消息
        websocketService.sendCountMessage(changeCounts);
        //删除消息
        try{
          stringRedisTemplate.opsForStream().delete(message.getStream(),message.getId());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

五、WebSocket接口类编写

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.dubbo.config.spring.ReferenceBean;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

@Slf4j
@Component
@ServerEndpoint(value = "/monitor/count", encoders = {ServerEncoder.class})
public class WebsocketService {
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    /**
     * 连接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        try {
            this.session = session;
            websocketServices.add(this);
            log.info("【websocket消息】有新的连接,总数为:{}", websocketServices.size());
        } catch (Exception e) {
            log.info("【websocket消息】有新的连接,总数为:{}", websocketServices.size());
        }
    }

    /**
     * 连接关闭调用的方法
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        try {
            session.close();
            websocketServices.remove(this);
            log.info("【websocket消息】连接断开,总数为:{}", websocketServices.size());
        } catch (IOException e) {
        }
    }

    /**
     * 发送错误时的处理
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("【websocket消息】错误原因:{}", error.getMessage());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message) {        
         try{
               result.setMessage("连接成功");
            } catch (Exception ex) {
                result.setMessage("连接失败");
            }
    }


  /**
     * 发送消息
     * @param message
     */
    public void sendMessage(String message) {
        try {
            if (null == session) {
                return;
            }
            session.getAsyncRemote().sendText(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 推送消息
     * @param message
     */
    public void sendCountMessage(String message) {
        try {
            if (null == session) {
                return;
            }
            session.getAsyncRemote().sendText(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

六、生产者生产消息到redis的stream中

@RequestMapping("/test")   
public void createMessage() {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            //redis的消息数量发生改变
           Long count= 5;
        Map<String, Object> msgMap = new HashMap<>();
        msgMap.put("counts", count);
redisTemplate.opsForStream().add(streamkey, msgMap);
        });
        try {
            future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

七、测试

发送请求 创建连接 请求test接口 查看消息是否自动返回给前端

八、使用webSocket实现对数据的实时推送详解

1.什么是webSocket?

相对于 HTTP 这种非持久的协议来说,websocket是 HTML5 出的一个持久化的协议。

2.实时推送数据的实现方式以及应用场景

实现方式

1.轮询:客户端通过代码定时向服务器发送AJAX请求,服务器接收请求并返回响应信息。
优点:代码相对简单,适用于小型应用。
缺点:在服务器数据没有更新时,会造成请求重复数据,请求无用,浪费带宽和服务器资源。

2.长连接:在页面中嵌入一个隐藏的iframe,将这个隐藏的iframe的属性设置为一个长连接的请求或者xrh请求,服务器通过这种方式往客户端输入数据。
优点:数据实时刷新,请求不会浪费,管理较简洁。
缺点:长时间维护保持一个长连接会增加服务器开销。

3.webSocket:websocket是HTML5开始提供的一种客户端与服务器之间进行通讯的网络技术,通过这种方式可以实现客户端和服务器的长连接,双向实时通讯。
优点:减少资源消耗;实时推送不用等待客户端的请求;减少通信量;
缺点:少部分浏览器不支持,不同浏览器支持的程度和方式都不同

应用场景:聊天室、智慧大屏、消息提醒、股票k线图监控等。

九、封装工具类

import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import java.time.Duration;
import java.util.List;
import java.util.Map;

/*Redis消息队列的工具类*/
public class RedisStreamUtil {

    private static RedisTemplate<String, Object> redisTemplate = new RedisTemplate();

    private static StreamOperations<String, Object, Object> streamOperations = redisTemplate.opsForStream();

    /**
     * 创建消费组
     * XGROUP CREATE key groupname id-or-$
     * XGROUP SETID key groupname id-or-$ (消费组已创建,重新设置读取消息顺序)
     * id为0表示组从stream的第一条数据开始读,
     * id为$表示组从新的消息开始读取。(默认)
     */
    public static String xGroupCreate(String key, String group){
        return streamOperations.createGroup(key, group);
    }

    public String xGroupCreate(String key, ReadOffset offset, String group) {
        return streamOperations.createGroup(key, offset, group);
    }

    /**
     * 生产消息
     * XADD key * hkey1 hval1 [hkey2 hval2...]
     * key不存在,创建键为key的Stream流,并往流里添加消息
     * key存在,往流里添加消息
     */
    public static String xAdd(String key, Map<String, String> value){
        return streamOperations.add(key, value).getValue();
    }

    /**
     * 消息确认(从PEL中删除一条或多条消息)
     * XACK key group ID[ID ...]
     */
    public static Long xAck(String key, String group, String... recordIds){
        return streamOperations.acknowledge(key, group, recordIds);
    }

    /**
     * 批量删除消息
     * XDEL key ID [ID ...]
     */
    public static Long xDel(String key, RecordId... recordIds){
        return streamOperations.delete(key, recordIds);
    }

    /**
     * 查看Stream的详情
     * XINFO STREAM key
     */
    public StreamInfo.XInfoStream xInfo(String key) {
        return streamOperations.info(key);
    }

    /**
     * 查看Stream的消息个数
     * XLEN key
     */
    public Long xLen(String key) {
        return streamOperations.size(key);
    }

    /**
     * 查询消息
     * XRANGE key start end [COUNT count]
     * range:表示查询区间,比如区间(消息ID,消息ID2),查询消息ID到消息ID2之间的消息,特殊值("-","+")表示流中可能的最小ID和最大ID
     * Range.unbounded():查询所有
     * Range.closed(消息ID,消息ID2):查询[消息ID,消息ID2]
     * Range.open(消息ID,消息ID2):查询(消息ID,消息ID2)
     * limit:表示查询出来后限制显示个数
     * Limit.limit().count(限制个数)
     */
    public List<MapRecord<String, Object, Object>> xRange(String key, Range<String> range, RedisZSetCommands.Limit limit) {
        return streamOperations.range(key, range, limit);
    }

    List<MapRecord<String, Object, Object>> xRange(String key, Range<String> range) {
        return this.xRange(key, range, RedisZSetCommands.Limit.unlimited());
    }

    /**
     * 查询消息
     * XREVRANGE key end start [COUNT count]
     * xReverseRange用法跟xRange一样,只是最后显示的时候是反序的,即消息ID从大到小显示
     */
    public List<MapRecord<String, Object, Object>> xReverseRange(String key, Range<String> range, RedisZSetCommands.Limit limit) {
        return streamOperations.reverseRange(key, range, limit);
    }

    /**
     * 修剪/保留消息
     * XTRIM key MAXLEN | MINID [~] count
     * count:保留消息个数,当count是具体的消息ID时,表示移除ID小于count这个ID的所有消息
     */
    public Long xTrim(String key, long count) {
        return streamOperations.trim(key, count);
    }

    /**
     * 销毁消费组
     * XGROUP DESTROY key groupname
     */
    public Boolean xGroupDestroy(String key, String group) {
        return streamOperations.destroyGroup(key, group);
    }

    /**
     * 查看消费组详情
     * XINFO GROUPS key
     */
    public StreamInfo.XInfoGroups xInfoGroups(String key) {
        return streamOperations.groups(key);
    }

    /**
     * 读取消息
     * XREAD [COUNT count] [BLOCK milliseconds] STREAMS key[key ...] id[id ...]
     * 从一个或者多个流中读取数据
     * 特殊ID=0-0:从队列最先添加的消息读取
     * 特殊ID=$:只接收从我们阻塞的那一刻开始通过XADD添加到流的消息,对已经添加的历史消息不感兴趣
     * 在阻塞模式中,可以使用$,表示最新的消息ID。(在非阻塞模式下$无意义)。
     */
    @SafeVarargs
    public final List<MapRecord<String, Object, Object>> xRead(StreamReadOptions options, StreamOffset<String>... offsets) {
        return streamOperations.read(options, offsets);
    }

    /**
     * 读取消息,强制带消费组、消费者
     * XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key[key ...] ID[ID ...]
     * 特殊符号 0-0:表示从pending列表重新读取消息,不支持阻塞,无法读取的过程自动ack
     * 特殊符号 > :表示只接收比消费者晚创建的消息,之前的消息不管
     * 特殊符号 $ :在xReadGroup中使用是无意义的,报错提示:ERR The $ ID is meaningless in the context of XREADGROUP
     */
    @SafeVarargs
    public final List<MapRecord<String, Object, Object>> xReadGroup(Consumer consumer, StreamReadOptions options, StreamOffset<String>... offsets) {
        return streamOperations.read(consumer, options, offsets);
    }

    /**
     * 消费者详情
     * XINFO CONSUMERS key group
     */
    public StreamInfo.XInfoConsumers xInfoConsumers(String key, String group) {
        return streamOperations.consumers(key, group);
    }

    /**
     * 删除消费者
     * XGROUP DELCONSUMER key groupname consumername
     */
    public Boolean xGroupDelConsumer(String key, Consumer consumer) {
        return streamOperations.deleteConsumer(key, consumer);
    }

    /**
     * Pending Entries List (PEL)
     * XPENDING key group [consumer] [start end count]
     * 查看指定消费组的待处理列表
     */
    public PendingMessagesSummary xPending(String key, String group) {
        return streamOperations.pending(key, group);
    }

    /**
     * 查看指定消费者的待处理列表
     */
    PendingMessages xPending(String key, Consumer consumer) {
        return this.xPending(key, consumer, Range.unbounded(), -1L);
    }

    public PendingMessages xPending(String key, Consumer consumer, Range<?> range, long count) {
        return streamOperations.pending(key, consumer, range, count);
    }

    /**
     * 消息转移
     * XCLAIM key group consumer min-idle-time ID[ID ...]
     * idleTime:转移条件,进入PEL列表的时间大于空闲时间
     */
    List<ByteRecord> xClaim(String key, String group, String consumer, long idleTime, String recordId) {
        return xClaim(key, group, consumer, idleTime, RecordId.of(recordId));
    }


    public List<ByteRecord> xClaim(String key, String group, String consumer, long idleTime, RecordId... recordIds) {
        return redisTemplate.execute(new RedisCallback<List<ByteRecord>>() {
            @Override
            public List<ByteRecord> doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.streamCommands().xClaim(key.getBytes(), group, consumer, Duration.ofSeconds(idleTime), recordIds);
            }
        });
    }
}

十、补充

1.测试也可用定时任务实现  

2.上文中注入的RedisTemplate与StringRedisTemplate可能有出入 望读者统一下

3.redis的stream也可是配置多个消费者组和消费者 同一消费者组消息只读取一次 类似游标移动

4.上述demo为过程总结,具体代码或有问题存在!!!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值