Redis Steam实现消息队列

4,328 阅读5分钟

前言

Stream 是 Redis 5.0 引入的一种新数据类型,是一个新的强大的支持多播的可持久化的消息队列。具体可参考 Redis 5.0 新特性 Stream 里面的介绍。

单服务单节点接收

消息发往单个服务,且一条消息仅被一个节点接收。这是MQ使用当中最常见的场景。

单服务单节点接收

这里有个消费组的概念,同一消费组内的成员共同消费到达该组的消息。也就是发往 Group A 的消息只会被其中一个成员消费。

那么只需要把单服务的所有节点加入到同一个消费组,就能达到我们要的效果。

接收端代码

@Bean
public StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
    // 创建配置对象
    StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> streamMessageListenerContainerOptions = StreamMessageListenerContainerOptions
            .builder()
            // 一次性最多拉取多少条消息
            .batchSize(10)
            .build();

    // 根据配置对象创建监听容器
    return StreamMessageListenerContainer
            .create(redisConnectionFactory, streamMessageListenerContainerOptions);
}

@Bean
public StreamListener<String, MapRecord<String, String, String>> streamListener(
        StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer,
        RedisTemplate<String, String> redisTemplate, Environment environment) throws UnknownHostException {

    String streamKey = "OSON_STREAM_MQ_KEY";
    String groupId = environment.getRequiredProperty("spring.application.name");
    String consumerName = Inet4Address.getLocalHost().getHostName() + ":" + environment.getProperty("server.port");
    System.out.println("streamKey = " + streamKey + ", groupId = " + groupId + ", consumerName = " + consumerName);

    // 判断 stream 是否初始化,未初始化则进行初始化
    if (!Boolean.TRUE.equals(redisTemplate.hasKey(streamKey))) {
        // 往 stream 发送消息,会自动创建 stream
        RecordId recordId = redisTemplate.opsForStream().add(streamKey, Collections.singletonMap("_up", "up"));

        // 创建 消费者组
        redisTemplate.opsForStream().createGroup(streamKey, groupId);

        // 删除创建
        redisTemplate.opsForStream().delete(streamKey, recordId);
    }

    // 监听器
    StreamListener<String, MapRecord<String, String, String>> listener = message -> message.forEach(System.out::println);

    // 使用监听容器监听消息,并且自动应答
    streamMessageListenerContainer.receiveAutoAck(
            Consumer.from(groupId, consumerName),
            StreamOffset.create(streamKey, ReadOffset.lastConsumed()),
            listener);
    return listener;
}

@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
    // 启动redis stream 监听
    applicationStartedEvent.getApplicationContext()
            .getBeanProvider(StreamMessageListenerContainer.class)
            .ifAvailable(container -> container.start());
}

redisTemplate 没有提供创建 stream 的 api,所以往 redis stream 发送消息让 redis 自动创建 stream ,然后把消息清除掉。同时,消费者组也需要先创建才能够被监听,消费者组是绑定到 stream 上的,不同 stream 的消费者组互不干扰。

streamMessageListenerContainer.receiveAutoAck 是接收消息后自动应答,表示这条消息已经被成功消费,也可以使用 streamMessageListenerContainer.receive 方法收到应答,实现更加灵活的功能。消息不应答的话,redis 会重新尝试发送,处理逻辑如果是非幂等的话,会导致重复数据。

发送端代码

BoundStreamOperations<String, String, String> operations = redisTemplate.boundStreamOps("OSON_STREAM_MQ_KEY");
for (int i = 0; i < 10; i++) {
    RecordId recordId = operations.add(Collections.singletonMap("index", String.valueOf(i)));
    System.out.printf("send message %d , message id %s\n", i, recordId);
}

stream 的消息都是 map 的方式,所以会有三个类型,stream key, map key, map value , stream key 即是 redis key。

StreamMessageListenerContainerOptions 的参数:

属性 默认 说明
batchSize 一次性最多拉取多少条消息
pollTimeout 2秒 超时时间,设置为0,表示不超时
serializer StringRedisSerializer.UTF_8 序列化器
executor SimpleAsyncTaskExecutor 消费消息的线程池
errorHandler 默认写出到 log 消息消费异常的handler
targetType String 消息 map 的 value 类型
objectMapper ObjectHashMapper 消息键值对类型

多服务单节点接收

消息发往多个服务,在每个服务内一条消息仅被一个节点接收。

多服务单节点接收

和单服务一样,只需要让不同的服务归属不同的消费组,即可达到效果,到达 Stream 的消息会被分发到其下的所有消费组。

接收端代码和单服务基本一致,仅仅在创建消费组的时候有一点点不同。

...

// 判断 stream 是否初始化,未初始化则进行初始化
if (!Boolean.TRUE.equals(redisTemplate.hasKey(streamKey))) {
    // 往 stream 发送消息,会自动创建 stream
    RecordId recordId = redisTemplate.opsForStream().add(streamKey, Collections.singletonMap("_up", "up"));
    // 删除创建
    redisTemplate.opsForStream().delete(streamKey, recordId);
}

try {
    // 创建 消费者组
    redisTemplate.opsForStream().createGroup(streamKey, groupId);
} catch (Exception ignored) {
}

...

因为有多个组,所以不能在创建 stream 时把 group 建好,并且 redis 没有提供判断 group 是否存在的 api,所以这里采用直接创建,并且 catch 住 group 已经存在的异常。

发送端和单服务一致:

广播

消息被所有节点接收,例如本地缓存的更新。

广播

所有节点不分组监听 stream 即可实现广播效果。

接收端关键代码

// 判断 stream 是否初始化,未初始化则进行初始化
if (!Boolean.TRUE.equals(redisTemplate.hasKey(streamKey))) {
    // 往 stream 发送消息,会自动创建 stream
    RecordId recordId = redisTemplate.opsForStream().add(streamKey, Collections.singletonMap("_up", "up"));
    // 删除创建
    redisTemplate.opsForStream().delete(streamKey, recordId);
}

// 监听器
StreamListener<String, MapRecord<String, String, String>> listener = message -> {
    message.forEach(System.out::println);
};

// 使用监听容器监听消息
streamMessageListenerContainer.receive(
        StreamOffset.create(streamKey, ReadOffset.lastConsumed()),
        listener);

因为不需要消费组,所以去除了消费组相关的代码,同时广播消息不需要应答,默认获取最新的消息,也去除了应答的代码。

发送端和单服务一致。

docker 安装 redis

在学习各种中间件时,环境安装是个很头疼的问题,但有了 docker 之后一切都变的简单了,所以,学技术的人一定得在自己电脑上安装 docker。
用 docker 运行 redis :

docker pull redis:5.0.5
docker run -d --name redis5.0.5 -p 6379:6379 redis:5.0.5

简单两个命令即完成了 redis 环境,完全可以满足开发、学习的需要。

在本地模拟多个节点部署

有时候我们需要多节点部署的环境,但不是每个人都有多台机器可供折腾,开虚拟机或 docker 是个办法,但还可以用更加简单的办法来满足需求。

    1. 执行 maven install 完成打包,得到可运行的 jar 包 (Spring boot)。
    1. IDE 运行项目,得到一个节点。
    1. 进入 jar 包所在目录,打开cmd 执行 java -jar 命令 用 -Dserver.port 指定不同的端口,启动得到节点。

java 启动命令示例:

java -jar "-Dserver.port=18080" ./redis-mq-oson-list-1.0.jar

pom依赖

本示例使用 springboot 2.2.6.RELEASE 版本
具体依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
</parent>

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

示例中完整的源码: mq-learning