Redis Stream +php7 实现消息队列

本文介绍如何使用 Redis 5.0 新增的数据结构 RedisStream 构建消息队列。RedisStream 支持消息持久化,克服了传统发布订阅模型的限制。文章通过实例展示了 PHP 操作 RedisStream 的过程,包括生产者发送消息及消费者处理消息的完整流程。

Redis Stream  是从5.0才有的新数据结构,基础知识参考网址

Redis Stream | 菜鸟教程Redis Stream Redis Stream 是 Redis 5.0 版本新增加的数据结构。 Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。 简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。 而 ..https://www.runoob.com/redis/redis-stream.html但是php要想使用确有很多障碍。网上相关资料少的出奇。
很多命令php都不支持,要使用 $redis->rawCommand。
有一位网友整理了相关资料,可以参考。
PHP 操作 Redis Stream 消息队列_王宣成的博客-优快云博客_php redis stream需要redis5.0 ,借鉴kafka,消息可持久化<?php //连接reids $redis = new Redis(); $redis->connect('127.0.0.1', 6379); //xadd:追加消息 //xdel:删除消息,删除标志位,不影响消息总长度 //xrange:消息列表,过滤已删除的消息 //xlen:消息长度 //del: 删除所有消息 $redis->rawCommandhttps://blog.youkuaiyun.com/qq_18743819/article/details/107276380我自己也做了一遍消息队列,贴上代码。抛砖引玉吧。
生产者代码就一个。

$redis->xadd('queue','*',['type'=>0,'dat'=>'']);

这个特别说明 第三个参数是数组,按原生代码这个就得拼接key/val 如果有1000个,是不是很傻呀。php用数组封装一下完美。

消费者代码有点复杂

set_time_limit(0);
$this->queue="queue"; //消息队里非key
$this->group='pin-group'  //消费者组
$consumer='consumerA';  //消费者

if(!$this->redis->exists($this->queue)){  /*队列不存在需要初始化*/
    $this->redis->xadd($this->queue,'*',['type'=>0,'dat'=>'']);
}
$res=$this->redis->xinfo('groups',$this->queue);  /*获取strarm消费组信息*/
if(!$res){
     $res=$this->redis->xgroup('create',$this->queue,$this->group,'0');  /*创建消费组*/
     var_dump($res);
}
while (1) {
     echo 'start============'.microtime(true)." \n";
//从最后读取一条,阻塞5秒
     $read = $this->redis->rawCommand('xreadgroup','group',$this->group,$consumer,'block','5000', 'count', '1' ,'streams',$this->queue,'>');

     //$res = $this->redis->rawCommand('xpending',$this->queue,$this->group,'-','+','10',$consumer);  //消费者的待处理消息
            $info = $read[0][1] ?? [];
            if (empty($info)) {
                continue;
            }
            $msgCount = count($info);
            for ($a = 0; $a < $msgCount; $a ++) {
                $msgId = $info[$a][0] ?? 0;  //每条消息的id
                $msg=$info[$a][1];
                $type=$msg[1];
                $dat=json_decode($msg[3],true);
                print_r([$type,$dat]);
                $xack = $this->redis->rawCommand('xack',$this->queue,$this->group,$msgId);  //确认消息已经处理
                echo $msgId.':'.$xack.PHP_EOL;
            }            
            //sleep(1);
            echo "end============".microtime(true)." \n";
}

消费者代码,需要持久化。

<think> 首先,用户的问题是:“redis的zset消息队列介绍、使用方法及相关原理”。我需要基于用户提供的引用内容来回答。 关键引用: - 引用[1]:强调使用ZSET作为消息队列时需要注意的问题,如消息唯一性、顺序、删除消费消息、消息堆积、客户端能力和负载均衡。还提到了分组功能。 - 引用[2]:描述了使用Redis zset作为消息队列的背景(Redis版本不支持Stream),实现思路包括线程轮询、查询score、抢占消息、处理消息等。 - 引用[3]:提到PHP中使用Redis zset实现延迟队列。 - 引用[4]:展示了一个DelayTask类如何使用泛型来存储消息内容。 用户的问题是介绍、使用方法及相关原理,所以我需要结构化回答: 1. **介绍**:解释什么是Redis zset消息队列,为什么用它(如替代Stream)。 2. **使用方法**:基于引用,描述如何实现。 3. **相关原理**:解释背后的机制,如zset的特性(有序、score用于排序)。 系统级指令: - 使用中文回答。 - 行内数学表达式用$...$格式(但在这个上下文中,可能不需要数学表达式)。 - 独立公式用$$...$$(同样,可能不相关)。 - LaTeX语法正确。 - 生成相关问题。 - 回答中引用的段落末尾添加引用标识如[^1]。 具体到回答结构: - 开头介绍Redis zset作为消息队列。 - 然后分部分:介绍、使用方法、相关原理。 - 结尾生成相关问题。 基于引用内容: - 介绍:Redis zset(有序集合)可以用来实现消息队列,因为它支持score排序和时间戳,适合延迟队列等场景[^2]。尤其在Redis早期版本(<5.0)没有Stream时,zset是一个常见选择[^2]。 - 使用方法: - 生产者添加消息:使用ZADD命令,score设置为延迟时间(如未来时间戳)。 - 消费者轮询:使用ZRANGEBYSCORE查询score小于等于当前时间的消息。 - 抢占消息:消费者删除消息(ZREM)来确保只有一个消费者处理它[^2]。 - 处理消息:使用策略模式处理不同类型的消息[^2]。 - 实现细节: - 消息唯一性:需要确保消息ID唯一。 - 顺序:zset基于score排序,可以保证消息按score顺序处理。 - 删除消费消息:消费者处理成功后删除消息。 - 分组功能:如引用[1]提到,可以使用多个zset或额外数据结构实现分组。 - 相关原理: - zset是sorted set,元素按score排序。 - 通过score设置为时间戳,实现延迟队列。 - 消费者轮询可能导致空轮询问题,需要优化(如休眠时间)[^2]。 生成相关问题: - 类似于示例,生成3个相关问题。 - 例如:1. 如何优化Redis zset消息队列以避免空轮询?2. Redis zset消息队列如何处理消息堆积?3. 如何在Redis zset消息队列实现分组功能? 引用标识: - 在回答中,当引用特定部分时,添加[^1]、[^2]等。例如,谈到实现思路时引用[^2]。 完整回答结构: 1. **Redis zset消息队列介绍** 2. **使用方法** - 生产者端 - 消费者端 - 分组实现 3. **相关原理** 4. **相关问题** 确保回答真实可靠:基于提供的引用,不要添加未提及的内容。 </think> ### Redis zset消息队列介绍、使用方法及相关原理 基于您提供的引用内容,我将全面介绍Redis zset作为消息队列实现方式。Redis zset(有序集合)常用于实现消息队列,特别是在Redis早期版本(如3.x)不支持Stream功能时,作为替代方案。它利用score属性排序实现延迟队列、消息优先级等功能,但需注意消息唯一性、顺序性和消费者抢占等问题[^1][^2]。以下内容参考了您提供的引用信息,确保回答真实可靠。 #### 1. Redis zset消息队列介绍 Redis zset是一个有序集合数据结构,每个元素关联一个score(分数),元素按score从小到大排序。这使得它天然适合实现消息队列: - **核心特性**: - **延迟队列**:通过score设置为消息的过期时间戳(例如未来时间),实现定时消费。 - **消息顺序性**:zset保证元素按score有序,确保消息按时间或优先级处理(score越小越先处理)。 - **持久化**:与PubSub不同,zset消息持久存储在Redis中,避免消息丢失[^2][^4]。 - **适用场景**: - Redis版本低于5.0(不支持Stream),如引用[2]所述的公司使用Redis 3.2.9。 - 需要延迟处理消息的场景,如定时任务、订单超时取消等。 - 消息量不大、对吞吐要求不高的应用(zset在高并发下可能因轮询产生性能瓶颈)。 - **优缺点**: - **优点**:简单易实现、支持延迟消息、消息有序持久化。 - **缺点**:需手动处理消费者抢占、消息堆积可能导致内存压力、空轮询问题需优化[^1][^2]。 #### 2. 使用方法 基于引用[2][^2]和[4][^4],Redis zset消息队列实现可分为生产者、消费者和消息处理三部分。以下以Java/PHP为例(核心逻辑通用),使用标准代码示例说明。 ##### 生产者端:添加消息 生产者通过`ZADD`命令添加消息,score设置为延迟时间戳(单位毫秒)。消息内容需序列化,并确保消息ID唯一。 ```python import redis import time # 连接Redis r = redis.Redis(host='localhost', port=6379, db=0) queue_name = "delay_queue" # 添加消息:score为延迟时间(当前时间 + 10秒) delay_time = int(time.time() * 1000) + 10000 # 10秒后执行 message_id = "msg_001" # 确保ID唯一 message_data = {"task": "process_order", "data": "order_123"} # 消息内容序列化 r.zadd(queue_name, {message_id: delay_time}) # ZADD命令添加消息 ``` - **关键点**:消息ID必须唯一(如使用UUID),避免重复消费;score表示执行时间,支持延迟队列[^2][^4]。 ##### 消费者端:轮询与消费 消费者轮询查询score小于等于当前时间的消息,并通过抢占机制确保消息只被一个消费者处理。 ```java // Java示例(基于引用[2][^2]) @Component public class ZSetConsumer { @Autowired private StringRedisTemplate stringRedisTemplate; private volatile boolean destroyFlag = false; // 控制线程退出 public void startConsumer(String queueName) { new Thread(() -> { while (!Thread.interrupted() && !destroyFlag) { try { // 查询当前可消费的消息(score <= 当前时间) long now = System.currentTimeMillis(); Set<String> messages = stringRedisTemplate.opsForZSet() .rangeByScore(queueName, 0, now); // ZRANGEBYSCORE命令 if (messages.isEmpty()) { // 空轮询优化:避免CPU占用,可添加休眠 Thread.sleep(100); // 休眠100ms减少空转 continue; } // 抢占消息:每个消息尝试删除(ZREM),成功者处理 for (String msg : messages) { if (removeMessage(queueName, msg)) { // 自定义删除方法确保抢占 processMessage(msg); // 处理消息 } } } catch (Exception e) { // 错误处理 } } }).start(); } private boolean removeMessage(String queueName, String message) { // 使用ZREM命令删除消息,成功删除表示抢占成功(Redis操作是原子的) Long removed = stringRedisTemplate.opsForZSet().remove(queueName, message); return removed != null && removed > 0; } private void processMessage(String message) { // 反序列化消息内容 DelayTask<?> task = deserialize(message); // 引用[4][^4]的DelayTask泛型类 String beanName = task.getTaskName(); // 如"orderTask" RedisQueueProcessService handler = applicationContext.getBean(beanName, RedisQueueProcessService.class); handler.process(task.getMsg()); // 调用策略模式处理 } } ``` - **关键点**: - **轮询机制**:消费者线程不断查询`ZRANGEBYSCORE`,score范围设为0到当前时间戳。 - **抢占式消费**:通过`ZREM`删除消息实现抢占。多个消费者可能同时查询到同一消息,但只有删除成功者能处理(避免重复消费)[^2]。 - **空轮询优化**:无消息时休眠(如100ms),减少CPU占用。 - **分组功能实现**:如引用[1][^1]所述,可使用多个zset队列(如`group1:queue`, `group2:queue`)实现分组,生产者按组名添加消息,消费者监听特定组队列。 ##### 消息处理 消息处理采用策略模式(引用[2][^2]),支持不同类型消息: - **消息序列化**:使用JSON或Protobuf序列化消息内容,如引用[4][^4]的`DelayTask<T>`泛型类。 - **处理服务**:定义接口`RedisQueueProcessService`,不同业务实现自定义处理逻辑(如订单处理、通知发送)。 ```java public interface RedisQueueProcessService { void process(Object message); // 处理消息方法 } @Service("orderTask") public class OrderProcessService implements RedisQueueProcessService { @Override public void process(Object message) { // 具体业务逻辑,如处理订单超时 System.out.println("Processing order: " + message); } } ``` #### 3. 相关原理 Redis zset消息队列的核心原理基于zset数据结构的特性和消费者轮询机制: - **zset排序原理**:zset底层使用跳跃表(skip list)和哈希表实现,元素按score排序(时间复杂度$O(\log N)$)。在消息队列中: - score设置为消息执行时间戳,`ZRANGEBYSCORE`查询score ≤ 当前时间的消息,保证消息按时间顺序处理。 - 数学上,score排序满足有序性:对于任意两个消息$m_i$和$m_j$,若$ \text{score}_i < \text{score}_j $,则$m_i$优先消费[^2][^4]。 - **延迟队列实现**:生产者设置score = 当前时间 + 延迟时间(例如$$ \text{score} = t_{\text{current}} + \Delta t $$),消费者轮询时只处理score ≤ 当前时间的消息,实现定时触发。 - **消费者抢占原理**:Redis命令是原子的,`ZREM`删除操作保证只有一个消费者成功抢占消息(类似于分布式锁)。但需注意: - **竞争问题**:多个消费者可能同时查询同一消息,但只有一个`ZREM`成功。失败者需跳过(引用[2][^2]建议添加抢占日志)。 - **Lua优化**:使用Redis Lua脚本将查询和删除合并为原子操作,避免竞争(如`ZREMRANGEBYSCORE`结合删除)。 - **问题与优化**: - **消息堆积**:大量消息时score查询可能变慢,可分区或使用多队列。 - **负载均衡**:消费者组需监控节点负载,避免单点瓶颈(引用[1][^1])。 - **可靠性**:消息处理失败需重试机制(如添加回退队列)。 ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hangbobo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值