Redis实现消息队列

本文探讨Redis作为消息队列的应用,介绍其生产消费模式与发布订阅模式,对比数据持久化与实时性的优劣,深入解析PUSH/POP及BRPOP/BLPOP命令,展示如何利用Redis实现高效、可靠的消息传递。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis实现轻量级的消息队列与消息中间件相比,没有高级特性也没有ACK保证,无法做到数据不重不漏,如果业务简单而且对消息的可靠性不是那么严格可以尝试使用。

列表类型

在这里插入图片描述
Redis中列表List类型是按照插入顺序排序的字符串链表,和数据结构中的普通链表一样,可以在头部left和尾部right添加新的元素。插入时如果键不存在Redis将为该键创建一个新的链表。如果链表中所有元素均被删除,那么该键也会被删除。
在这里插入图片描述
Redis的列表List可以包含的最大元素数量为4294967295,从元素插入和删除的效率来看,如果是在链表的两头插入或删除元素将是非常高效的操作。即使链表中已经存储了数百万条记录,该操作也能在常量时间内完成。然后需要说明的是,如果元素插入或删除操作是作用于链表中间,那将是非常低效的。

Redis中对列表List的操作命令中,L表示从左侧头部开始插入和弹出,R表示从右侧尾部开始插入和弹出。

Redis提供了两种方式来做消息队列,一种是生产消费模式,另一种是发布订阅模式。

生产消费模式

生产消费模式会让一个或多个客户端监听消息队列,一旦消息到达,消费者马上消费,谁先抢到算谁的。如果队列中没有消息,消费者会继续监听。

PUSH/POP

Redis数据结构的列表List提供了push和pup命令,遵循着先入先出FIFO的原则。使用push/pop方式的优点在于消息可以持久化,缺点是一条消息只能被一个消费者接收,消费者完全靠手速来获取,是一种比较简陋的消息队列。

Redis的队列list是有序的且可以重复的,作为消息队列使用时可使用rpush/lpush操作入队,使用lpop/rpop操作出队。当发布消息是执行lpush命令,将消息从列表左侧加入队列。消息接收方执行rpop命令从列表右侧弹出消息。

如果队列空了怎么办呢?

如果队列空了,消费者会陷入pop死循环,即使没有数据也不会停止。空轮询不但消耗消费者的CPU资源还会影响Redis的性能。傻瓜式的做法是让消费者的线程按照一定的时间间隔不停的循环和监控队列,虽然可行但显然会造成不必要的资源浪费,而且循环周期也很难确定。

对于消费者而言存在一个问题,需要不停的调用rpop查看列表中是否有待处理的消息。每调用一次都会发起一次连接,势必造成不必要的资源浪费。如果使用休眠的方式让消费者线程间隔一段时间再消费,但这样做也有两个问题:

如果生产者速度大于消费者消费的速度,消息队列长度会一直增大,时间久了会占用大量内存空间。
如果休眠时间过长,就无法处理一些时效性的消息。如果休眠时间过短也会在连接上造成比较大的开销。
当LPOP返回一个元素给客户端时会从List中将该元素移除,这意味着该元素只存在于客户端的上下文中。如果客户端在处理这个返回元素的过程中崩溃了,这个元素就会永远的丢失掉。

LPUSH/BRPOP

在这里插入图片描述
使用brpop和blpop实现阻塞读取

由于需要一直调用rpop/lpop才可以实现不停的监听且消费消息,为解决这个问题,Redis提供了阻塞命令brpop/blpop。使用brpop会阻塞队列,而且每次只会弹出一个消息,如果没有消息则会阻塞。

Redis列表List支持带阻塞的命令,生产者从列表左侧lpush加入消息到队列,消费者使用brpop命令从列表右侧弹出消息并设置超时时间,如果列表中没有消息则一直阻塞直到超时。这样做的目的在于减小Redis的压力。

对于Redis来说提供了blpop/brpop阻塞读,阻塞读在队列没有数据时会立即进入休眠状态,一旦数据到来则立即被唤醒,消息的延迟几乎为零。需要注意的是如果线程一直阻塞在那里,连接就会被服务器主动断开来减少资源占用,这时blpop/brpop会抛出异常,所以编写消费段时需要注意异常的处理。

BRPOP key [key ...] timeout

BLPOP/BRPOP 列表阻塞式弹出

当给定列表内没有任何元素可供弹出时,连接将被BRPOP命令阻塞,直到等待超时或发现可弹出元素为止。当给定多个key参数时,按参数key的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。另外,BRPOP除了弹出元素的位置和BLPOP不同之处,其他表现一致。

列表的阻塞式弹出特点是如果列表中没有任务时,连接将会被阻塞。连接的阻塞存在一个超时时间,当超时时间设置为0时刻无限等待直到弹出消息。

Redis的PUSH/POP机制,利用Redis的列表list数据结构,生产者lpush消息,消费者brpop消息并设定超时时间以减少Redis压力。这种方案相对于发布订阅模式的好处是数据可靠性提高了,只有在Redis宕机且数据没有持久化的情况下会丢失数据。可以根据业务通过AOF和缩短持久化间隔来保证较高的可靠性,也可以通过多个客户端来提高消息速度。但相对于专业的消息队列中间件,发布订阅模式的状态过于简单(没有状态),而且没有ACK机制,消息取出后消费失败依赖于客户端记录日志或重新push到队列中。

Redis中实现生产者和消费者模型,可使用LPUSH和RPOP来实现该功能。不过当列表为空时消费者就需要轮询来获取消息,这样会增加Redis的访问压力和消费者的CPU时间,另外很多访问也是无用的。为此Redis提供了阻塞式访问BRPOP和BLPOP命令,消费者可以在获取数据时指定如果数据不存在阻塞的时间,如果在时限内获得数据则立即返回,如果超时还没有数据则返回NULL,可使用0表示一直阻塞。同时Redis会为所有阻塞的消费者以先后顺序排序。

使用Redis的列表来实现一个任务队列,开启两个程序,一个作为生产者使用LPUSH写队列,一个作为消费者使用RPOP读队列。由于消费者并不知道什么时候会有消息过来,所以消费者需要一直循环读取数据。两者的消息可以使用JSON进行封装协议传输。由于消费者在没有读到数据的情况下,会一直循环读取,对系统来说十分耗费资源,此时可利用Redis提供的阻塞读取命令BRPOP进行改进。使用BRPOP改进后,消费者不会一直循环读取,而是一直阻塞等到有消息过来时才读取。

发布订阅模式

发布订阅模式是一个或多个客户端订阅消息频道,只要发布者发布消息,所有订阅者都能收到消息,订阅者都是平等的。

PUB/SUB

Redis自带pub/sub机制即发布订阅模式,此模式中生产者producer和消费者consumer之间的关系是一对多的,也就是一条消息会被多个消费者所消费,当只有一个消费者时可视为一对一的消息队列。

发布订阅机制模型

首先发布者将消息发布到频道,客户端订阅频道后就能获得频道的消息。

发布订阅模式命令

psubscribe 订阅一个或多个符合给定模式的频道
publish 将消息发布到指定的频道
pubsub查看订阅与发布系统状态
pubsub channels pattern 列出当前的活跃频道
pubsub numsub channel-1 channel-n 获取给定频道的订阅者数量
pubsub numpat 获取订阅模式的数量
punsubscribe 指示客户端退订所有给定模式
subscribe 订阅给定的一个或多个频道的消息
unsubscribe 指示客户端退订给定的频道

实现

使用PHP+Redis实现消息队列

操作流程

1.PHP接收请求和数据
2.PHP将数据写入Redis队列(入队)
3.Shell定时调用PHP读取队列数据并写入数据库(出队)

入队inqueue.php

$result = $redis->rpush("queue", json_encode($data));
if($result){
  echo "inqueue success";
}

出队 outqueue.php

#! /usr/bin/php
<?php
$result = $redis->lpop("queue");
if($result){
  $data = json_decode($result, true);
}

定时任务:process.sh

每分钟调用一次定时脚本

* * * * * /scripts/process.sh

定时脚本:process.sh

#! /bin/bash
#filename : process.sh
php /scripts/outqueue.php

出队采用死循环方式

$ vim outqueue.php
while(true){
  $result = $redis->lpop("queue");
  if($result){
    $data = json_decode($result, true);
  }
}
<think>我们被要求使用Redis实现消息队列。根据引用[1]和引用[2],Redis提供了三种方式:基于List结构、PubSub(发布订阅)和Stream结构。其中,引用[1]和引用[2]都提到,基于Stream的方式是比较完善的消息队列模型,推荐使用。 下面分别介绍这三种方式: ### 1. 基于List结构 Redis的List是一个双向链表,我们可以利用其`LPUSH`(或`RPUSH`)和`BRPOP`(或`BLPOP`)命令实现消息队列。 - 生产者使用`LPUSH`将消息放入队列的左侧(头部)。 - 消费者使用`BRPOP`从队列的右侧(尾部)阻塞地取出消息。 **优点**: - 实现简单,利用Redis已有的数据结构。 - 支持阻塞等待(使用`BRPOP`),避免消费者轮询。 **缺点**: - 不支持多消费者(一个消息只能被一个消费者消费)。 - 没有消息确认机制,如果消费者处理失败,消息就丢失了。 - 没有消息回溯功能。 #### 示例代码(生产者): ```java Jedis jedis = new Jedis("localhost"); // 向队列list1中放入消息 jedis.lpush("list1", "message1"); ``` #### 示例代码(消费者): ```java Jedis jedis = new Jedis("localhost"); // 阻塞地从队列list1中取出消息,0表示无限等待 List<String> result = jedis.brpop(0, "list1"); String message = result.get(1); ``` ### 2. 基于PubSub(发布订阅) Redis的发布订阅模式允许消息被多个消费者接收(广播)。 - 生产者发布消息到指定频道(channel)。 - 消费者订阅频道,接收消息。 **优点**: - 支持一对多的消息发布。 - 实时性较好。 **缺点**: - 消息是即时的,没有持久化。如果消费者不在线,消息就丢失了。 - 不支持消息的存储,因此无法回溯历史消息。 - 没有消息确认机制。 #### 示例代码(生产者): ```java Jedis jedis = new Jedis("localhost"); // 向频道channel1发布消息 jedis.publish("channel1", "message1"); ``` #### 示例代码(消费者): ```java Jedis jedis = new Jedis("localhost"); // 创建订阅对象 jedis.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { // 处理消息 System.out.println("Received: " + message); } }, "channel1"); ``` ### 3. 基于Stream(推荐) Redis 5.0引入了Stream数据结构,它提供了更完善的消息队列功能: - 支持消息持久化。 - 支持消费者组(Consumer Group):多个消费者可以组成一个组,共同消费一个队列,每条消息只会被组内的一个消费者消费。 - 提供消息确认机制(ACK),确保消息被成功处理。 - 支持消息回溯。 #### 基本命令: - `XADD`:添加消息到流。 - `XREAD`:读取消息(可以阻塞)。 - `XGROUP`:创建消费者组。 - `XREADGROUP`:通过消费者组读取消息。 - `XACK`:确认消息已被处理。 #### 示例代码(生产者): ```java Jedis jedis = new Jedis("localhost"); // 向名为stream1的流中添加消息,消息包含字段key1和value1 jedis.xadd("stream1", StreamEntryID.NEW_ENTRY, Collections.singletonMap("key1", "value1")); ``` #### 示例代码(消费者组): 首先,创建消费者组: ```bash # 在Redis命令行中创建消费者组 XGROUP CREATE stream1 mygroup 0 ``` 然后,在Java中使用消费者组消费: ```java Jedis jedis = new Jedis("localhost"); // 消费者组mygroup中的消费者consumer1读取消息 Map.Entry<String, StreamEntryID> streamQuery = new AbstractMap.SimpleEntry<>("stream1", StreamEntryID.UNRECEIVED_ENTRY); List<Map.Entry<String, List<StreamEntry>>> result = jedis.xreadGroup("mygroup", "consumer1", 1, 0, // 读取1条消息,0表示阻塞直到有消息 streamQuery); // 处理消息 for (StreamEntry entry : result.get(0).getValue()) { System.out.println("Received: " + entry.getFields()); // 确认消息 jedis.xack("stream1", "mygroup", entry.getID()); } ``` ### 总结 - **List结构**:适用于简单的消息队列,单消费者,无持久化要求。 - **PubSub**:适用于广播消息,实时性要求高,但消息可丢失的场景。 - **Stream**:适用于需要持久化、多消费者组、消息确认等高级功能的场景,是最接近专业消息队列实现[^1][^2]。 在实际应用中,根据需求选择合适的方式。对于需要可靠消息队列的场景,推荐使用Stream结构。 ### 相关问题 1. Redis Stream 如何实现消息的持久化和重放? 2. 在 Redis 中,如何监控消息队列的运行状态? 3. Redis 作为消息队列与专业的消息队列(如 RabbitMQ)相比有哪些优缺点?</think>### Redis 实现消息队列的三种方式 Redis 提供了三种主要方式实现消息队列,根据引用[1][2][3]的内容整理如下: #### 1. 基于 List 结构(双向链表) ```bash # 生产者 LPUSH myqueue "message1" # 左侧插入消息 LPUSH myqueue "message2" # 消费者 BRPOP myqueue 0 # 阻塞式右侧弹出(0=无限等待) ``` **特点**: - 支持阻塞式弹出(`BRPOP`) - 消息持久化存储 - 简单高效,适合基本队列需求 - **缺点**:不支持多消费者组[^1][^2] #### 2. 基于 PubSub(发布订阅) ```bash # 消费者订阅 SUBSCRIBE channel1 # 生产者发布 PUBLISH channel1 "message" ``` **特点**: - 实时广播消息 - 支持多消费者 - **缺点**:消息不持久化(离线消费者丢失消息)[^1][^2] #### 3. 基于 Stream(推荐方式) ```bash # 生产者添加消息 XADD mystream * key1 "value1" key2 "value2" # 消费者组消费 XGROUP CREATE mystream mygroup 0 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > ``` **特点**: - 消息持久化存储 - 支持消费者组(同组内竞争消费) - 消息确认机制(ACK) - 支持消息回溯 - 提供阻塞读取能力[^1][^2] ### 完整 Java 实现示例(使用 Jedis + Stream) ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.StreamEntry; import redis.clients.jedis.StreamEntryID; public class RedisStreamQueue { private static final String STREAM_KEY = "orders"; private static final String GROUP_NAME = "order_processor"; public static void main(String[] args) { // 初始化消费者组 initConsumerGroup(); // 启动生产者线程 new Thread(RedisStreamQueue::producer).start(); // 启动消费者线程 new Thread(RedisStreamQueue::consumer).start(); } private static void initConsumerGroup() { try (Jedis jedis = new Jedis("localhost")) { jedis.xgroupCreate(STREAM_KEY, GROUP_NAME, new StreamEntryID(), true); } } private static void producer() { try (Jedis jedis = new Jedis("localhost")) { int count = 1; while (true) { String order = "订单-" + count++; // 添加消息到流 jedis.xadd(STREAM_KEY, null, Map.of("order_id", order, "amount", String.valueOf(count * 10))); System.out.println("生产: " + order); Thread.sleep(1500); } } catch (Exception e) { e.printStackTrace(); } } private static void consumer() { try (Jedis jedis = new Jedis("localhost")) { while (true) { // 从消费者组读取消息 var result = jedis.xreadGroup( GROUP_NAME, "consumer-1", 1, 0, true, Map.of(STREAM_KEY, ">") ); if (!result.isEmpty()) { for (var stream : result) { for (var entry : stream.getValue()) { System.out.println("消费: " + entry.getFields()); // 处理成功后确认消息 jedis.xack(STREAM_KEY, GROUP_NAME, entry.getID()); } } } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } } ``` ### 三种方式对比 | **特性** | **List** | **PubSub** | **Stream** | |------------------|----------|------------|------------| | 消息持久化 | ✓ | ✗ | ✓ | | 阻塞式消费 | ✓ | ✗ | ✓ | | 多消费者组 | ✗ | ✗ | ✓ | | 消息确认(ACK) | ✗ | ✗ | ✓ | | 消息回溯 | ✗ | ✗ | ✓ | | 广播消息 | ✗ | ✓ | ✗ | | 适用场景 | 简单队列 | 实时通知 | 企业级队列 | ### 使用建议 1. **基本需求**:使用 List 结构(`LPUSH`+`BRPOP`) 2. **实时通知**:使用 PubSub(广播场景) 3. **企业级应用**:推荐使用 Stream 结构(支持消费者组和ACK)[^1][^2] Redis Stream 是目前最接近专业消息队列(如 Kafka/RabbitMQ)的实现方式,适合需要消息持久化、消费确认和多消费者组的场景[^3]。 ### 相关问题 1. Redis Stream 如何实现消息的持久化和重放? 2. 在 Redis 中,如何监控消息队列的运行状态? 3. Redis 作为消息队列与专业的消息队列(如 RabbitMQ)相比有哪些优缺点?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值