Reids stream 非阻塞轮询导致QPS飙到1.8W

本文探讨了Reidsstream中由于非阻塞轮询导致的QPS飙高问题,重点分析了StreamPollTask的工作原理和配置,并提供了设置PollTimeout来解决这一问题的方法。

Reids stream 非阻塞轮询导致QPS飙到1.8W

请添加图片描述

QPS监控 触发报警机制,最近redis被用来做简化版消息队列了

在测试环境也发现这个现象,打印执行中的堆栈命令

jstack -l xxxx > jstack.log
	at org.springframework.data.redis.stream.StreamPollTask.doLoop(StreamPollTask.java:138)
	at org.springframework.data.redis.stream.StreamPollTask.run(StreamPollTask.java:123)

观察到在StreamPollTask方法中不断循环

  • 翻阅DefaultStreamMessageListenerContainer源码发现StreamPollTask是用于执行循环轮询接收消息的线程

  • 消费者注册到container中通过register方法

    	@Override
    	public Subscription register(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
            // 关键在于getReadTask
    		return doRegister(getReadTask(streamRequest, listener));
    	}
    
  • 获取读取任务,该task会放到线程中执行

    	private StreamPollTask<K, V> getReadTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
    
            // 核心代码在这,getReadFunction生成具体的读取命令并使用函数式编程作为方法传递
    		Function<ReadOffset, List<ByteRecord>> readFunction = getReadFunction(streamRequest);
    		Function<ByteRecord, V> deserializerToUse = getDeserializer();
    
    		TypeDescriptor targetType = TypeDescriptor
    				.valueOf(containerOptions.hasHashMapper() ? containerOptions.getTargetType() : MapRecord.class);
    
    		return new StreamPollTask<>(streamRequest, listener, errorHandler, targetType, readFunction, deserializerToUse);
    	}
    
  • 生成读取命令

    	private Function<ReadOffset, List<ByteRecord>> getReadFunction(StreamReadRequest<K> streamRequest) {
    
    		byte[] rawKey = ((RedisSerializer<K>) template.getKeySerializer())
    				.serialize(streamRequest.getStreamOffset().getKey());
    
    		if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) {
    
    			ConsumerStreamReadRequest<K> consumerStreamRequest = (ConsumerStreamReadRequest<K>) streamRequest;
    
                // 这是read命令的具体参数
                // 这个参数点进去可以看到 block/count 两个参数 分别对应阻塞时间/每次读取的消息数量
    			StreamReadOptions readOptions = consumerStreamRequest.isAutoAcknowledge() ? this.readOptions.autoAcknowledge()
    					: this.readOptions;
    			Consumer consumer = consumerStreamRequest.getConsumer();
    
    			return (offset) -> template.execute((RedisCallback<List<ByteRecord>>) connection -> connection.streamCommands()
    					.xReadGroup(consumer, readOptions, StreamOffset.create(rawKey, offset)));
    		}
    
    		return (offset) -> template.execute((RedisCallback<List<ByteRecord>>) connection -> connection.streamCommands()
    				.xRead(readOptions, StreamOffset.create(rawKey, offset)));
    	}
    
  • read参数依赖于生成StreamReadOptions时的配置

    	private static StreamReadOptions getStreamReadOptions(StreamMessageListenerContainerOptions<?, ?> options) {
    
    		StreamReadOptions readOptions = StreamReadOptions.empty();
    
    		if (options.getBatchSize().isPresent()) {
    			readOptions = readOptions.count(options.getBatchSize().getAsInt());
    		}
    
            // 可以看到若设置PollTimeout为0则非阻塞
    		if (!options.getPollTimeout().isZero()) {
    			readOptions = readOptions.block(options.getPollTimeout());
    		}
    
    		return readOptions;
    	}
    
  • 若设置PollTimeout为0则非阻塞,非阻塞轮询导致redisQPS极具增高

  • 解决方案

    • 生成StreamMessageListenerContainerOptions时指定pollTimeout

      // 这个是循环阻塞时间,若不设置则无限轮询
      		StreamMessageListenerContainerOptions
      				.builder()	
                  	// 这个是循环阻塞时间,若不设置则无限轮询
              		.pollTimeout(Duration.ofSeconds(10))
      
<think> 嗯,用户这次问的是Redis是否支持队列功能。从之前的对话历史来看,用户似乎对技术实现细节很感兴趣,之前问过汽车冷却系统和电控系统,都得到了带代码示例的专业回答。这次转向了数据库领域,可能是在开发需要消息队列功能的系统。 Redis确实有强大的队列支持,核心是List数据结构。不过用户可能不知道具体怎么用,需要详细说明操作命令和典型场景。考虑到用户之前接受过带代码示例的回答,这次也准备用Python示例展示LPUSH/BRPOP操作。 用户身份推测是开发者,问题很具体但不够深入,可能需要补充高级特性。要注意解释清楚阻塞操作和发布订阅的区别,这是容易混淆的点。另外用户没问但值得提的还有Stream类型——这是更专业的消息队列方案。 安全方面要提醒ACL控制和持久化配置,毕竟队列数据丢失很致命。最后提出的问题要引导用户思考扩展性问题,比如集群部署和持久化策略,这对实际应用很重要。 </think> ### Redis队列实现详解 Redis原生支持**List数据结构**实现高性能队列,同时提供Stream类型满足高级消息队列需求。以下是核心实现方式及对比: --- #### 1. **List基础队列** **操作命令**: ```bash # 生产者端 (左进右出) LPUSH myqueue "task1" # 左侧插入,时间复杂度O(1) # 消费者端 BRPOP myqueue 30 # 阻塞式右端弹出,超时30秒 ``` **特性对比**: | 操作 | 命令 | 时间复杂度 | 是否阻塞 | |--------------|---------|------------|----------| | 左端入队 | LPUSH | O(1) | 否 | | 右端出队 | RPOP | O(1) | 否 | | 阻塞右端出队 | BRPOP | O(1) | 是 | | 队列长度 | LLEN | O(1) | - | **Python示例**: ```python import redis r = redis.Redis() # 生产者 r.lpush("order_queue", "{"order_id":1001}") # 消费者 while True: _, data = r.brpop("order_queue", timeout=60) # 阻塞等待 print(f"处理订单: {data.decode()}") ``` --- #### 2. **Stream类型 (Redis 5.0+)** **专业级消息队列能力**: ```bash # 创建消息流 XADD mystream * sensor_id 15 temperature 36.5 # 消费者组管理 XGROUP CREATE mystream mygroup $ # 创建消费组 # 消费消息 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > ``` **核心优势**: - **消息回溯**:通过消息ID(时间戳+序列号)精准定位 - **消费组**:支持多消费者负载均衡(类似Kafka) - **ACK机制**:XACK确认消息避免丢失 - **阻塞通知**:XREAD支持阻塞式监听新消息 --- #### 3. **队列模式对比** | 特性 | List队列 | Stream队列 | |---------------------|-------------------|--------------------| | 持久化 | RDB/AOF支持 | 原生持久化 | | 消息确认 | 需自行实现 | 内置XACK机制 | | 消费者组 | 不支持 | 原生支持 | | 性能(QPS) | ≥100,000 | ≥50,000 | | 适用场景 | 简单任务队列 | 金融级消息系统 | --- #### 4. **高级队列方案** - **延迟队列**: ```python # 使用ZSET实现 (score=执行时间戳) r.zadd("delayed_queue", {"task1": 1712345678}) # 定时任务扫描ZSET获取到期任务 ``` - **优先级队列**: 多List分级(high_queue, mid_queue, low_queue) + BRPOP优先级轮询 --- #### 5. **生产环境建议** 1. **内存控制**:`maxmemory-policy allkeys-lru` 防止队列积压撑爆内存 2. **持久化**:开启AOF每秒刷盘(`appendfsync everysec`) 3. **监控**:`INFO commandstats` 监控LPUSH/BRPOP调用频率 4. **安全**:禁用KEYS命令,使用SCAN替代队列遍历
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值