Redis Stream 使用 Redis 的消息队列

2,075 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第N天,点击查看活动详情

Redis Stream

Redis Stream 是 Redis 5.0 版本新增加的数据结构。

Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。

而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容:

111.jpg

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。

上图解析:

  • Consumer Group :消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。
  • last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
  • pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。

消息队列相关命令:

  • XADD - 添加消息到末尾
  • XTRIM - 对流进行修剪,限制长度
  • XDEL - 删除消息
  • XLEN - 获取流包含的元素数量,即消息长度
  • XRANGE - 获取消息列表,会自动过滤已经删除的消息
  • XREVRANGE - 反向获取消息列表,ID 从大到小
  • XREAD - 以阻塞或非阻塞方式获取消息列表

消费者组相关命令:

  • XGROUP CREATE - 创建消费者组
  • XREADGROUP GROUP - 读取消费者组中的消息
  • XACK - 将消息标记为"已处理"
  • XGROUP SETID - 为消费者组设置新的最后递送消息ID
  • XGROUP DELCONSUMER - 删除消费者
  • XGROUP DESTROY - 删除消费者组
  • XPENDING - 显示待处理消息的相关信息
  • XCLAIM - 转移消息的归属权
  • XINFO - 查看流和消费者组的相关信息;
  • XINFO GROUPS - 打印消费者组的信息;
  • XINFO STREAM - 打印流信息

XADD

使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列,XADD 语法格式:

XADD key ID field value [field value ...]
  • key :队列名称,如果不存在就创建
  • ID :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
  • field value : 记录。

XDEL

使用 XDEL 删除消息,语法格式:

XDEL key ID [ID ...]
  • key:队列名称
  • ID :消息 ID

XLEN

使用 XLEN 获取流包含的元素数量,即消息长度,语法格式:

XLEN key
  • key:队列名称

XRANGE

使用 XRANGE 获取消息列表,会自动过滤已经删除的消息 ,语法格式:

XRANGE key start end [COUNT count]
  • key :队列名
  • start :开始值, - 表示最小值
  • end :结束值, + 表示最大值
  • count :数量

XREVRANGE

使用 XREVRANGE 获取消息列表,会自动过滤已经删除的消息 ,语法格式:

XREVRANGE key end start [COUNT count]
  • key :队列名
  • end :结束值, + 表示最大值
  • start :开始值, - 表示最小值
  • count :数量

XREAD

使用 XREAD 以阻塞或非阻塞方式获取消息列表 ,语法格式:

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
  • count :数量
  • milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式
  • key :队列名
  • id :消息 ID

XGROUP CREATE

使用 XGROUP CREATE 创建消费者组,语法格式:

XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
  • key :队列名称,如果不存在就创建
  • groupname :组名。
  • $  : 表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。

XREADGROUP GROUP

使用 XREADGROUP GROUP 读取消费组中的消息,语法格式:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  • group :消费组名
  • consumer :消费者名。
  • count : 读取数量。
  • milliseconds : 阻塞毫秒数。
  • key : 队列名。
  • ID : 消息 ID。

在 SpringBoot 中使用 Redis Stream

Redis 配置

 * @author: xx
 * @date: 2022/4/13 9:38
 * @description: redis 配置类 修改序列化方式
 */
@Configuration
public class RedisConfig {


    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        return redisTemplate;
    }
}

封装 Redis Stream 工具类

import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;
import java.util.List;
import java.util.Map;

public class RedisStreamUtil {

    private RedisTemplate<String, Object> redisTemplate;

    public RedisStreamUtil(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * @author: xx
     * @date: 2022/4/12 17:29
     * @description: 创建分组
     * streamName: 流名称
     * groupName: 分组名称
     * readOffset: 消息读取规则
     */
    public String addGroup(String streamName, String groupName, ReadOffset readOffset) {
        String group = null;
        //获取是否已有该名称 stream
        Long size = redisTemplate.opsForStream().size(streamName);
        if (size == null || size < 1) {
            // 没有设置读取规则
            group = redisTemplate.opsForStream().createGroup(streamName,readOffset, groupName);
        }
        if (size != null && size >= 1) {
            // 已有直接创建新的分组
            group = redisTemplate.opsForStream().createGroup(streamName, groupName);
        }
        return group;
    }

    /**
     * @author: xx
     * @date: 2022/4/12 17:29
     * @description: 发消息
     * streamName: 流名称
     * map: 用来封装消息内容
     * RecordId: 返回消息id
     */
    public RecordId addMessage(String streamName, Map<Object, Object> map) {
        return redisTemplate.opsForStream().add(streamName, map);
    }

    /**
     * @author: xx
     * @date: 2022/4/12 17:30
     * @description: 收消息 每次读取一条消息
     * streamName: 流名称
     * groupName: 分组名称
     * consumerName: 消费者名称 (没有会自动创建)
     * duration: 读不到消息的阻塞时间
     * readOffset: 消息读取规则
     * 
     */
    public MapRecord<String, Object, Object> readOne(String streamName,String groupName,String consumerName,Duration duration, ReadOffset readOffset) {
        List<MapRecord<String, Object, Object>> recordList = redisTemplate.opsForStream().read(
                // 创建消费者
                Consumer.from(groupName, consumerName),
                // 每次读取 1条消息
                StreamReadOptions.empty().count(1).block(duration),
                StreamOffset.create(streamName, readOffset)
        );
        if (recordList == null || recordList.isEmpty()) {
            return null;
        }
        return recordList.get(0);

    }

    /**
     * @author: xx
     * @date: 2022/4/13 8:52
     * @description: 手动消息确认
     * streamName: 流名称
     * groupName: 分组名称
     * record: 消息
     */
    public Long acknowledge(String streamName,String groupName, MapRecord<String, Object, Object> record) {
        return redisTemplate.opsForStream().acknowledge(streamName, groupName, record.getId());
    }

    /**
     * @author: xx
     * @date: 2022/4/13 9:10
     * @description: 删除消息
     * streamName: 流名称
     * record: 消息
     */
    public Long delete(String streamName, MapRecord<String, Object, Object> record) {
        return redisTemplate.opsForStream().delete(streamName, record.getId());
    }
}

使用 Redis Stream 测试

@RestController
@Slf4j
public class RedisController {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * @author: xx
     * @date: 2022/4/13 9:06
     * @description: 创建分组
     */
    @GetMapping("/test1")
    public Object  test1() {
        RedisStreamUtil streamUtil = new RedisStreamUtil(redisTemplate);
        String group = streamUtil.addGroup("stream1", "g1", ReadOffset.from("0"));
        log.info("group:{}",group);
        return "ok";
    }

    /**
     * @author: xx
     * @date: 2022/4/13 9:06
     * @description: 发送消息
     */
    @GetMapping("/test2")
    public Object  test2(String key,String value) {
        Map<Object, Object> map = new HashMap<>();
        map.put(key,value);
        RedisStreamUtil streamUtil = new RedisStreamUtil(redisTemplate);
        RecordId recordId = streamUtil.addMessage("stream1", map);
        log.info("recordId:{}",recordId);
        return recordId;
    }

    /**
     * @author: xx
     * @date: 2022/4/13 9:07
     * @description: 接收消息
     */
    @GetMapping("/test3")
    public Object  test3() {
        RedisStreamUtil streamUtil = new RedisStreamUtil(redisTemplate);
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    MapRecord<String, Object, Object> record = streamUtil.readOne("stream1", "g1", "c1", Duration.ofSeconds(2), ReadOffset.lastConsumed());
                    if (record == null) {
                        log.info("没有最新消息");
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        continue;
                    }
                    Map<Object, Object> map = record.getValue();
                    log.info("最新消息:{}",map);
                    streamUtil.acknowledge("stream1","g1",record);
                    streamUtil.delete("stream1",record);
                }
            }
        }).start();

        return "ok";
    }

}

使用效果如下:

111.jpg

消费完消息需要确认消息,没有确认的消息会保存在pedding-list中,可以被再次消费,也会多占用内存.