基于Redis实现消息队列

简介: 基于Redis实现消息队列


1.业务场景

假设在没有专业消息中间件的情况下,又要通过消息队列去解耦。redis是个更好的选择。

2.实现方式

简要说明实现方式,这里只做个大概的概括

  • 发布与订阅(缺点:典型的一对一,不支持多个消费者公平消费消息,消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃等问题)
  • list队列(缺点:没有很好 ACK 机制,没有 ConsumerGroup 消费组,不支持一对多消费等问题)
  • stream队列(推荐)官方:https://redis.io/docs/data-types/streams/

3.概念

Redis5.0带来了Stream类型。其实就是Redis对消息队列(MQ,Message Queue)的完善实现。

主要有几个概念:

1.消费者组(Consumer Group):一个消费组有多个消费者(Consumer), 这些消费者之间是竞争关系。也就是说不会出现重复消费的场景。

2.pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。

3.last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。

4.消息ID: 消息ID的形式是timestampInMillis-sequence,例如1527846880572-5

这里简要贴出Redis中Stream操作的相关指令

其实像代码,都是基于命令的高度封装

消息队列相关命令:

  • 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 - 打印流信息

4.代码实现

stream相关配置,这里主要配置消费组和消费者相关信息,以及消息的监听机制

@Slf4j
@Configuration
public class RedisStreamConfig {
    @Autowired
    private MyListener myListener;
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 实际生产环境中  我们应该把消费者组等信息  写入配置环境中 
     */
//    @Autowired
//    private StreamProperty streamProperty;
    /**
     * 收到消息后不自动确认,需要用户选择合适的时机确认
     * 当某个消息被ACK,PEL列表就会减少
     * 如果忘记确认(ACK),则PEL列表会不断增长占用内存
     * 如果服务器发生意外,重启连接后将再次收到PEL中的消息ID列表
     */
    @Bean
    public Subscription subscription(RedisConnectionFactory factory) {
        initGroup("mystream", "group1");
        // 创建Stream消息监听容器配置
        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> options = StreamMessageListenerContainer
                .StreamMessageListenerContainerOptions
                .builder()
                // 读取超时时间
                .pollTimeout(Duration.ofSeconds(3))
                // 配置消息类型
                .targetType(String.class)
                // 异常处理器
                .errorHandler(t -> log.info("redis listener error", t))
                .build();
        // 创建Stream消息监听容器
        StreamMessageListenerContainer<String, ObjectRecord<String, String>> listenerContainer = StreamMessageListenerContainer.create(factory, options);
        // 设置消费手动提交配置
        Subscription subscription = listenerContainer.receive(
                // 设置消费者分组和名称
                Consumer.from("group1","consumer-1"),
                // 设置订阅Stream的key和获取偏移量,以及消费处理类
                StreamOffset.create("mystream", ReadOffset.lastConsumed()),
                agendaListener);
        // 监听容器启动
        listenerContainer.start();
        return subscription;
    }
    /**
     * 初始化分组
     */
    private void initGroup(String key, String group) {
        Boolean aBoolean = redisTemplate.hasKey(key);
        // 创建不存在的分组
        if (Boolean.FALSE.equals(aBoolean)) {
            redisTemplate.opsForStream().createGroup(key, group);
        }
    }
}

实现消息的监听

@Slf4j
@Component
public class MyListener implements StreamListener<String, ObjectRecord<String, String>> {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public void onMessage(ObjectRecord<String, String> record) {
        try {
            String value = record.getValue();
            log.info("stream name :{}, body:{}", record.getStream(), value);
            if (StrUtil.isBlank(value)) {
                return;
            }
            // todo 业务逻辑
            // 手动确认消息 如果不ack 消息就会进入到pending队列中 这个队列都是维护消费者的未确认的消息
            redisTemplate.opsForStream().acknowledge("mystream", "group1", record.getId().getValue());
        } catch (Exception e) {
            log.error("error message:{}", e.getMessage());
        }
    }
}

这里说一下消息体类型 Record 官方解释:流中的单个条目,由条目 ID 和实际条目值(通常是字段值对的集合)组成

我们就是可以理解为消息体类型。Record接口,常用的就是

  • MapRecord(键值对类型)
  • ObjectRecord(对象类型)

测试

@PostMapping("/addStream")
public ResponseResult<String> addStream(){
    // 这里的消息体都是string类型
    ObjectRecord<String, String> record = StreamRecords.objectBacked("1234567").withStreamKey("mystream");
    // 这里是消息id,消息id在队列里是唯一的
    RecordId recordId = stringRedisTemplate.opsForStream().add(record);
    // 裁剪队列,因为队列即使被消费者消费后任然不会删除,所以我们队列设定最大容量,也就是上面提到的 XTRIM  命令
    Long count = stringRedisTemplate.opsForStream().trim("mystream", 100000);
    System.out.println("trimCount" + count);
    if (recordId != null) {
        // 返回打印消息id
        return ResponseResult.success(recordId.getValue());
    }
    return ResponseResult.success();
}

基于redisson实现

相关消息监听和消费者配置同上

测试

RStream<Object, Object> stream = redissonClient.getStream("mystream", new SerializationCodec());
StreamAddArgs<Object, Object> entry = StreamAddArgs.entry("a","1");
stream.add(entry);



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
2月前
|
消息中间件 存储 NoSQL
剖析 Redis List 消息队列的三种消费线程模型
Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表。 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 。这篇文章,我们聊聊如何使用 List 命令实现消息队列的功能以及剖析消费者线程模型 。
97 20
剖析 Redis List 消息队列的三种消费线程模型
|
1月前
|
消息中间件 分布式计算 NoSQL
大数据-41 Redis 类型集合(2) bitmap位操作 geohash空间计算 stream持久化消息队列 Z阶曲线 Base32编码
大数据-41 Redis 类型集合(2) bitmap位操作 geohash空间计算 stream持久化消息队列 Z阶曲线 Base32编码
27 2
|
1月前
|
消息中间件 存储 NoSQL
python 使用redis实现支持优先级的消息队列详细说明和代码
python 使用redis实现支持优先级的消息队列详细说明和代码
37 0
|
3月前
|
消息中间件 NoSQL Redis
Redis Stream消息队列之基本语法与使用方式
这篇文章详细介绍了Redis Stream消息队列的基本语法和使用方式,包括消息的添加、读取、删除、修剪以及消费者组的使用和管理,强调了其在消息持久化和主备复制方面的优势。
70 0
|
4月前
|
消息中间件 C语言 RocketMQ
消息队列 MQ操作报错合集之出现"Connection reset by peer"的错误,该如何处理
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
4月前
|
消息中间件 Java C语言
消息队列 MQ使用问题之在使用C++客户端和GBase的ESQL进行编译时出现core dump,该怎么办
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
19天前
|
消息中间件 存储 Kafka
MQ 消息队列核心原理,12 条最全面总结!
本文总结了消息队列的12个核心原理,涵盖消息顺序性、ACK机制、持久化及高可用性等内容。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
|
2月前
|
消息中间件
手撸MQ消息队列——循环数组
队列是一种常用的数据结构,类似于栈,但采用先进先出(FIFO)的原则。生活中常见的排队场景就是队列的应用实例。在数据结构中,队列通常用数组实现,包括入队(队尾插入元素)和出队(队头移除元素)两种基本操作。本文介绍了如何用数组实现队列,包括定义数组长度、维护队头和队尾下标(front 和 tail),并通过取模运算解决下标越界问题。此外,还讨论了队列的空与满状态判断,以及并发和等待机制的实现。通过示例代码展示了队列的基本操作及优化方法,确保多线程环境下的正确性和高效性。
40 0
手撸MQ消息队列——循环数组
|
3月前
|
消息中间件 存储 缓存
一个用过消息队列的人,竟不知为何要用 MQ?
一个用过消息队列的人,竟不知为何要用 MQ?
159 1