redis实现消息队列的几种方式及其优劣

概述

常用的消息队列有,rabbitMq、kafka、RocketMq、ActiveMq等。这些消息队列需要独立安装部署,作为一个中间件来提供服务,虽然有着高性能、高可靠的优点,但是额外部署这些中间件也会增加运维成本,和服务器成本。

本篇文章探讨了一下如何使用redis实现消息队列。使用redis无需额外的部署,如果原先就有使用redis的话。此外redis更为轻量也更容易维护。但是redis实现消息队列有多种方案,这些方案有其优点也有其缺点,适用于不同的应用场景。以下从“实时性”、“可靠性”、“功能性”这几个维度做一些对比分析探讨。

一、理论部分

“消息队列”是在消息的传输过程中保存消息的容器。
消息队列常被使用在“流量削峰”、“系统解耦”、“异步调用”这几个方面。
消息队列主要面对的几个问题是,
1、并发性能
2、实时性
3、如何防止消息丢失,保证可靠性

从简单的讲,消息队列就是一个“队列”queue,生产者负责发送消息,消息队列存储消息,消费者则负责接收消息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6PXcPYP-1651055823390)(https://www.hengyumo.cn/momoclouddisk/file/download?code=202204151015636_image.png)]

在面对一些亿级流量场景,消息队列届的大哥kafka是如何保证高性能的呢?

  1. Kafka Reactor模型架构
  2. 页缓存技术+磁盘顺序写
  3. ZeroCopy:零拷贝技术
  4. 使用批量消息提升服务端处理能力
    https://www.hengyumo.cn/momoblog/detail/202204162051750

那使用redis能否获得和kafka一样的高性能呢?答案是一定的。
redis是如何实现高性能的呢?

  1. IO多路复用
  2. 单线程
  3. 基于内存存储
  4. 高效数据结构
  5. 写时拷贝(CopyOnWrite)
  6. 客户端管道批量命令
  7. 零拷贝技术
    https://www.hengyumo.cn/momoblog/detail/202204162116630

二、实现消息队列

2.1 基于list实现

消息队列的基础结构是队列,而redis正好有相对应的数据结构:list。

实现方式

  1. 生产者写消息

    lpush mq hello1
    lpush mq hello2
    lpush mq hello3
    lpush mq hello4
    lpush mq hello5

lpush 命令向指定列表的左边推入元素,以上命令模拟了向mq这个消息队列列表中写入五条消息,分别是hello1 ~ hello5。

同时写入多条也可以跟着多个,如

lpush mq hello6 hello7
  1. 消费者读取消息

首先,基于list实现的消息队列是可以有效保证实时性的。消费者要如何检测到有新消息推送过来呢?

  • 要么是不停自旋调用llen mq获取队列的长度,如果不为0则读取。或者自旋调用rpop不停读取数据。虽然能保证高实时性,但是这会造成redis的性能浪费和消费者本身的性能浪费,严重时会导致系统崩溃。
  • 定时调用llen mq获取队列的长度。实时性取决于定时任务的频率,如果每100ms一次,则就有100ms的延迟。
  • brpop,brpop可以理解为rpop命令的阻塞升级版,brpop mq 1,会尝试阻塞读取mq 1秒时间,如果1秒内没有消息则会返回nil,如果有消息,会立即返回。

生产者

127.0.0.1:6379>
127.0.0.1:6379> rpush mqb hello
(integer) 1

消费者

127.0.0.1:6379> brpop mqb 1
1) "mqb"
2) "hello"
127.0.0.1:6379> brpop mqb 1
(nil)
(1.08s)

基于list实现,读取消息可以通过两种方式,一种是rpop,从列表的右边读取并弹出元素。该操作是原子性的,并发下安全。

rpop mq
rpop mq

依次弹出的是hello1,hello2,按照先进先出的顺序弹出。

第二种方式是lrange,使用lrange可以实现消息的批量消费,lrange list start stop 读取list的从start - stop之间的元素。读取之后为了防止重复消费,需要使用ltrim start stop进行清除。因为期间需要进行两个操作,因此不是并发安全的,需要通过分布式锁来保证安全性。此外还存在着事务的问题,如果读取完消息之后进程挂掉,会导致之前已经读取的消息在下次运行时被重复消费。这种方式适用于对消息可靠性要求不高,但是要求处理性能高的情况,如处理大量的日志数据进行分析操作。除了使用ltrim之外也可以使用LREM key count value来删除已经消费的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isAj1MD5-1651055823391)(https://www.hengyumo.cn/momoclouddisk/file/download?code=202204241041249_image.png)]

如图所示,假设mq中有7条消息,每次消费3条消息。那么第一条命令lrange mq -3 -1,读取倒数第3到倒数第1之间的所有元素。第二条命令ltrim mq 0 -4,保留未读取的零到倒数第4条消息,把已经读取的消息删除。

以下演示了在redis-cli中的模拟:

127.0.0.1:6379> lrange mq -3 -1
1) "hello3"
2) "hello2"
3) "hello1"
127.0.0.1:6379> ltrim mq 0 -4
OK
127.0.0.1:6379> lrange mq 0 -1
1) "hello7"
2) "hello6"
3) "hello5"
4) "hello4"
  1. 多生产者多消费者
  • 多生产者
    基于list的多生产者是没有问题的,多个生产者同时向mq中推送消息,仍然能保证消息有序。

  • 多消费者

    • rpop方式 多消费者下并发同样安全,不会出现消息被重复消费的情况
    • lrange + ltrim 方式 多消费者下并发不安全,需要使用分布式锁保证有序,否则会出现消息被重复消费的问题。同时不保证事务安全性。需要通过额外手段记录读取mq的位置,以保证宕机复位时不会出现消息重复读取的问题。
  1. 发布订阅方式

发布订阅简单的理解是将一个消息广播给多个消费者,每个消费者针对该消息只消费一次。

针对发布订阅有两种思路,一种是较为简单的,既然一对一消费可以通过一个list实现,那么一对多消费就使用多个list来一一对应各个消费者:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kWu8BIMc-1651055823391)(https://www.hengyumo.cn/momoclouddisk/file/download?code=202204241058734_image.png)]

这里只需要维护一个消费者和消息队列名称映射的列表,生产者发送消息时发送给所有的消费者对应的队列。
消费者读取自己对应的消息队列。

实现起来简单,但是存在两个问题:

  • 资源浪费,原本只需要一个列表存储,变成了几个消费者就需要几个列表,而且列表的数据都是相同的,这无疑造成了浪费。当数据量不大,消费者不多时可以不顾及这点。
  • 无法保证消息可靠的同步发送到各个队列上。如果生产者写入完mq1之后就宕机了,就好导致只有消费者1接收到了消息,而其它的消费者无法接收到消息,
  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值