spring-boot redis stream消息队列demo-及死信简单处理

SpringBoot+Redis stream实现消息队列 Springboot+Redis stream实现消息队列 阅读详情
AI摘要

本文介绍了Redis Stream这一高性能消息队列,对比此前用Redis实现消息队列的方式,阐述其优势。结合项目中RocketMQ在Windows关机后出现的问题,提出用Redis Stream替换的方案,还探讨了死信问题的处理方式,给出相关命令、配置、代码结构等内容。

Redis stream 是 Redis 5 引入的一种新的数据结构,它是一个高性能、高可靠性的消息队列,主要用于异步消息处理和流式数据处理。在此之前,想要使用 Redis 实现消息队列,通常可以使用例如:列表,有序集合、发布与订阅 3 种数据结构。但是 stream 相比它们具有以下的优势:

  • 支持范围查找:内置的索引功能,可以通过索引来对消息进行范围查找
  • 支持阻塞操作:避免低效的反复轮询查找消息
  • 支持 ACK:可以通过确认机制来告知已经成功处理了消息,保证可靠性
  • 支持多个消费者:多个消费者可以同时消费同一个流,Redis 会确保每个消费者都可以独立地消费流中的消息

情况

当前项目,是在window server 上 部署 rocketmq (ps 单体)使用 消息队列,使用三方exe实现 注册到 window 服务中(实现开机自启-自行搜索实现吧)。
在这里插入图片描述
在这里插入图片描述

问题

当前发现一个问题,window 被关机(主动、断电等),mq的broker无法启动,原因是 delayOffset.json文件损坏造成,文件内容变成如下图。
在这里插入图片描述
修改为,重新启动没问题了
在这里插入图片描述
每次重新都需要处理,虽然可以考虑使用脚本 处理,但是徒增成本。就想着 使用redis实现 进行替换(项目规模较小)。redis-stream 需要将 版本升级为 5(当前是4)。
Redis for Windows
redis下载 直接替换就行。

死信问题

网上看到 处理的方式有好几种。定时调度,处理pending信息(直接ack、重新消费、转移消费组等);人工运维处理 等。看了很多,结合 spring 提供的函数。
StringRedisTemplate.redisTemplate.opsForStream()返回的接口类StreamOperations 参考spring文档,可以用的方法有 acknowledge、 add、 delete、 createGroup、 deleteConsumer、 destroyGroup、 consumers、 groups、 info、 pending、 size、 range、 read、 reverseRange、 trim等。
没有包含命令:转移消费 xClaim,实际在RedisStreamCommands接口中,通过代码也能解决;

redisTemplate.execute((RedisCallback<List<ByteRecord>>) connection -> connection.streamCommands().xClaim("stream".getBytes(), "group", "consumer", Duration.ofSeconds(10), RecordId.of("streamId")))

也没有包含命令:设置消费组的起始消息 ID xgroupSetid,实际在RedisStreamAsyncCommands接口中,代码在比较底层,不能拿来就用。

RedisFuture<String> xgroupSetid(StreamOffset<K> streamOffset, K group);
当前方式

当前,实现的方式。在项目 重启 或 定时调度(暂无代码demo),通过 pending 获取消息 streamId,先复制消息add,在确认旧消息acknowledge,最后删除旧消息delete。

ps:自己感觉还行吧(迷之自信O(∩_∩)O哈哈~),不要拿来就用啊(⊙o⊙)…

redis桌面管理

下载redis可视化管理工具:AnotherRedisDesktopManager
在这里插入图片描述

参考

理解 Redis 新特性:Stream
springboot + redis stream做轻量级消息队列
Redis Stream实现消息队列
redis Stream消息队列 redision redis stream消息队列pending
使用redis流和spring数据获取挂起的消息
在SpringBoot中使用RedisTemplate重新消费Redis Stream中未ACK的消息

说明

redis-stream命令

  • XADD:向流中添加新的消息。
  • XREAD:从流中读取消息。
  • XREADGROUP:从消费组中读取消息。
  • XRANGE:根据消息 ID 范围读取流中的消息。
  • XREVRANGE:与 XRANGE 类似,但以相反顺序返回结果。
  • XDEL:从流中删除消息。
  • XTRIM:根据 MAXLEN 参数修剪流的长度。
  • XLEN:获取流的长度。
  • XGROUP:管理消费组,包括创建、删除和修改。
  • XACK:确认消费组中的消息已被处理。
  • XPENDING:查询消费组中挂起(未确认)的消息。
  • XCLAIM:将挂起的消息从一个消费者转移到另一个消费者。
  • XINFO:获取流、消费组或消费者的详细信息。

pom

spring-boot 加 redis依赖,简单项目,测试用

<parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.7.0</version>
    <relativePath/>
</parent>
<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.7.0</spring-boot.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

配置

server:
  port: 8888
  servlet:
    context-path: /
spring:
  redis:
    msg:
      listener: false
    host: 127.0.0.1
    port: 6379
    database: 1
    client-type: lettuce
    lettuce:
      pool:
        max-active: 8

代码结构

引入spring boot的 StringRedisTemplate

    private static StringRedisTemplate redisTemplate;
    @Autowired
    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        RedisStreamConfig.redisTemplate = redisTemplate;
    }

声明变量。redis 的 key键名称,消费者 组名,消费者 客户端名。

final static String STREAM = "tsp", GROUP = "tsp-g-1", CONSUMER = "tsp-c-1";

生产者-发送消息

这块比较简单,直接调用就行

/** 生产者-发送消息 */
    public static void push(String msg){
        // 创建消息记录, 以及指定stream
        StringRecord record = StreamRecords.string(Collections.singletonMap("data", msg)).withStreamKey(STREAM);
        // 将消息添加至消息队列中 XADD stream [MAXLEN len] id field value [field value ...]
        redisTemplate.opsForStream().add(record);
        log.info("redis-消息队列-stream, {} ,send msg: {}", STREAM,msg);
    }

消费者

消费者-监听类,实际的业务逻辑处理内容。消息队列 创建 会有测试消息,需要跳过。实际业务逻辑需要考虑,挂起消息 重新消费情况(解决 死信问题的简单方法)

/** 消费者 监听 */
    public static class TspStreamListener implements StreamListener<String, ObjectRecord<String,String>> {
        @Override
        public void onMessage(ObjectRecord<String, String> message) {
            RecordId messageId = message.getId();
            // 消息的key和value
            String string = message.getValue();
            if("T".equals(string)){
                log.info("消费者-监听>>>测试消息-ack. msgId={}, stream={}, body={}", messageId, message.getStream(), string);
                redisTemplate.opsForStream().acknowledge(GROUP, message);
                return;
            }
            log.info("消费者-监听>>>get msg. msgId={}, stream={}, body={}", messageId, message.getStream(), string);
            //业务逻辑,需要考虑 被挂起的消息 重新消费情况
            try {
                log.info("消费者-监听>>>睡眠10s,模拟耗时逻辑-start");
                TimeUnit.SECONDS.sleep(10);
                log.info("消费者-监听>>>睡眠10s,模拟耗时逻辑-end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("消费者-监听>>>手动确认消息");
            redisTemplate.opsForStream().acknowledge(GROUP, message);
            log.info("消费者-监听>>>end");
        }
    }

消费者-注册,关闭自动ack,重连自动消费上次处理(已经在pending队列里面了)的下一个消息,拉取消息超时时间50s,每批数量1,使用默认线程池,传递数据类型 String,异常处理 打印错误日志。最后 start 启动消费。

    @Bean
    @ConditionalOnProperty(name = "spring.redis.msg.listener",havingValue = "true")
    public StreamMessageListenerContainer<String, ObjectRecord<String, String>> tspConsumerListener(
            RedisConnectionFactory factory){
        //spring-data-redis 2.3.1.RELEASE及更高版本中,createGroup如果不存在,则会自动使用创建流
        return streamContainer(StreamOffset.create(STREAM, ReadOffset.lastConsumed()),Consumer.from(GROUP,CONSUMER)
                ,factory,new TspStreamListener());
    }
    private StreamMessageListenerContainer<String, ObjectRecord<String, String>> streamContainer(StreamOffset<String> offset
            ,Consumer consumer, RedisConnectionFactory factory, StreamListener<String, ObjectRecord<String, String>> listener) {
        // pollTimeout 拉取消息超时时间,targetType 传递的数据类型, executor 线程池
        StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer
                .create(factory, StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                        .builder().pollTimeout(Duration.ofSeconds(50)).batchSize(1).targetType(String.class).build());
        //指定消费最新的消息
        try {
            //创建消费者,接收上次处理未ACK消费的消息,指定消费者对象,autoAcknowledge 关闭自动ack确认
            container.register(StreamMessageListenerContainer.StreamReadRequest
                    .builder(offset).errorHandler((error) -> log.error(error.getMessage()))
                    .cancelOnError(e -> false).consumer(consumer).autoAcknowledge(false)
                    .build(), listener);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        container.start();
        return container;
    }

预处理

类加载完毕,执行方法。预处理操作,可以使用 spring 启动后处理类实现。本demo就先这样了。

本方法 是为了 解决,项目启动报错(无消息队列、无消费组)和程序重启死信问题(꒦_꒦)

    @PostConstruct
    private void start(){
    }

如果redis 没有 对应key 的 stream 消息队列,消费者启动后会频繁报错。如下代码会在 项目启动 发送测试消息,创建 消息队列,并限制 消息队列 长度(限制内存消耗)。

ps:我好菜呀,不知道spring是否允许消费者 自动创建 消息队列o(╥﹏╥)o

// 生产者 创建 消息队列,防止启动 消费者 报错
        if(!(Boolean.TRUE.equals(redisTemplate.hasKey(STREAM)))){
            log.info("没有key,测试发送(创建key)");
            push("T");
            //限制 消息队列 容量 XTRIM stream MAXLEN len
            redisTemplate.opsForStream().trim(STREAM,1000L);
        }

判断消息队列,是否存在消费组。没有创建消费组

// 创建 消息队列-消费者
        if (redisTemplate.opsForStream().groups(STREAM).isEmpty()) {
            log.info("redis-消息队列-stream,createGroup,{} {}",STREAM,GROUP);
            //XGROUP CREATE stream group [id|$|0] [MKSTREAM],使用 $ 表示仅消费新消息,或者使用 0 表示消费流中的所有消息
            redisTemplate.opsForStream().createGroup(STREAM,GROUP);
        }

死信处理代码

private void handlerPending(String key,String group){
        //判断是否存在挂起的消息 XPENDING stream group [start stop count] [consumer]
        PendingMessagesSummary pending = redisTemplate.opsForStream().pending(key, group);
        long size;
        if(pending == null || (size = pending.getTotalPendingMessages()) <= 0 ) {
            log.debug("redis-消息队列,{},{},暂无挂起消息pending", key, group);
            return;
        }
        //---------------------
        String minId = pending.minMessageId(),maxId = pending.maxMessageId(),id;
        // 从挂起消息开始处理 [1],minId : 1706178903044-0,maxId : 1706178903044-0
        log.info("redis-消息队列,{},{},pending-挂起消息[{}],minId : {},maxId : {}",key,group,size,minId,maxId);
        // - + 4 ---------------------获取挂起所有信息 streamId
        PendingMessages msgIds = redisTemplate.opsForStream().pending(key, group, Range.closed("-", "+"), size);
        log.info("redis-消息队列,{},{},pending-挂起消息,{}",key,group,msgIds);
        List<MapRecord<String, Object, Object>> list;
        // ---------------------循环处理,可以考虑 异步执行
        for (PendingMessage msgId : msgIds) {
            id = msgId.getId().getValue();
            //PendingMessage{id=1706178903044-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=66775, totalDeliveryCount=1}
            log.info("redis-消息队列,{},{},pending-挂起消息>>,{}",key,group,msgId);
            //XRANGE key start end [COUNT count]
            list = redisTemplate.opsForStream().range(key, Range.just(msgId.getIdAsString()));
            log.info("redis-消息队列,{},{},range-挂起消息>> {},{}",key,group,id,list);
            if(list == null || list.isEmpty()){
                continue;
            }
            // 开始 结束 id相同,只返回 一个消息结果
            MapRecord<String, Object, Object> record = list.get(0);
            //MapBackedRecord{recordId=1706233950802-0, kvMap={data=401}}
            if("1706233940011-0".equals(id)){
                MapRecord<String, Object, Object> copy = record.withId(RecordId.autoGenerate());
                log.info("redis-消息队列,{},{},range-挂起消息>0>add+++++copy {},{}",key,group,copy.getStream(),copy);
                //XADD stream-name id field value [field value]
                RecordId add = redisTemplate.opsForStream().add(copy);
                log.info("redis-消息队列,{},{},range-挂起消息>0>add+++++copy {},{}",key,group,add,copy);
                log.info("redis-消息队列,{},{},range-挂起消息>0>ack^^^^^old {},{}",key,group,record.getStream(),record);
                //XACK stream group id [id id ...]
                Long siz = redisTemplate.opsForStream().acknowledge(GROUP, record);
                log.info("redis-消息队列,{},{},range-挂起消息>0>ack^^^^^old [{}],{}",key,group,siz,record);
                log.info("redis-消息队列,{},{},range-挂起消息>0>del-----old {},{}",key,group,record.getStream(),record);
                //XDEL key ID [ID ...]
                siz = redisTemplate.opsForStream().delete(record);
                log.info("redis-消息队列,{},{},range-挂起消息>0>del-----old [{}],{}",key,group,siz,record);
            }else{
                log.info("redis-消息队列,{},{},range-挂起消息>0> {},{},{}",key,group,record.getStream(),id,record);
            }
        }
        log.info("redis-消息队列,{},{},pending-挂起消息-end",key,group);
    }

汇总

代码

package demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author z.y.l
 * @version v1.0
 * @date 2024/1/25
 */
@Configuration
@Import (RedisAutoConfiguration.class)
public class RedisStreamConfig {
    private static final Logger log = LoggerFactory.getLogger(RedisStreamConfig.class);
    final static String STREAM = "tsp", GROUP = "tsp-g-1", CONSUMER = "tsp-c-1";
    private static StringRedisTemplate redisTemplate;
    @Autowired
    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        RedisStreamConfig.redisTemplate = redisTemplate;
    }
    /** 生产者-发送消息 */
    public static void push(String msg){
        // 创建消息记录, 以及指定stream
        StringRecord record = StreamRecords.string(Collections.singletonMap("data", msg)).withStreamKey(STREAM);
        // 将消息添加至消息队列中 XADD stream [MAXLEN len] id field value [field value ...]
        redisTemplate.opsForStream().add(record);
        log.info("redis-消息队列-stream, {} ,send msg: {}", STREAM,msg);
    }
    /** 项目启动 预处理,无消息队列(消费者-启动报错)、限制容量、手动创建消费组、处理异常挂起的消息 */
    @PostConstruct
    private void start(){
        // 生产者 创建 消息队列,防止启动 消费者 报错
        if(!(Boolean.TRUE.equals(redisTemplate.hasKey(STREAM)))){
            log.info("没有key,测试发送(创建key)");
            push("T");
            //限制 消息队列 容量 XTRIM stream MAXLEN len
            redisTemplate.opsForStream().trim(STREAM,1000L);
        }
        // 创建 消息队列-消费者
        if (redisTemplate.opsForStream().groups(STREAM).isEmpty()) {
            log.info("redis-消息队列-stream,createGroup,{} {}",STREAM,GROUP);
            //XGROUP CREATE stream group [id|$|0] [MKSTREAM],使用 $ 表示仅消费新消息,或者使用 0 表示消费流中的所有消息
            redisTemplate.opsForStream().createGroup(STREAM,GROUP);
        }else{
            // 处理 挂起消息,也可以 使用调度 定时处理
            // 当前 挂起消息处理方式:复制新消息-ack旧消息-删除旧消息
            // 也可以使用 转移 消费组
            handlerPending(STREAM,GROUP);
        }
    }
    private void handlerPending(String key,String group){
        //判断是否存在挂起的消息 XPENDING stream group [start stop count] [consumer]
        PendingMessagesSummary pending = redisTemplate.opsForStream().pending(key, group);
        long size;
        if(pending == null || (size = pending.getTotalPendingMessages()) <= 0 ) {
            log.debug("redis-消息队列,{},{},暂无挂起消息pending", key, group);
            return;
        }
        String minId = pending.minMessageId(),maxId = pending.maxMessageId(),id;
        // 从挂起消息开始处理 [1],minId : 1706178903044-0,maxId : 1706178903044-0
        log.info("redis-消息队列,{},{},pending-挂起消息[{}],minId : {},maxId : {}",key,group,size,minId,maxId);
        // - + 4 所有
        PendingMessages msgIds = redisTemplate.opsForStream().pending(key, group, Range.closed("-", "+"), size);
        log.info("redis-消息队列,{},{},pending-挂起消息,{}",key,group,msgIds);
        List<MapRecord<String, Object, Object>> list;
        for (PendingMessage msgId : msgIds) {
            id = msgId.getId().getValue();
            //PendingMessage{id=1706178903044-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=66775, totalDeliveryCount=1}
            log.info("redis-消息队列,{},{},pending-挂起消息>>,{}",key,group,msgId);
            //XRANGE key start end [COUNT count]
            list = redisTemplate.opsForStream().range(key, Range.just(msgId.getIdAsString()));
            log.info("redis-消息队列,{},{},range-挂起消息>> {},{}",key,group,id,list);
            if(list == null || list.isEmpty()){
                continue;
            }
            // 开始 结束 id相同,只返回 一个消息结果
            MapRecord<String, Object, Object> record = list.get(0);
            //MapBackedRecord{recordId=1706233950802-0, kvMap={data=401}}
            if("1706233940011-0".equals(id)){
                MapRecord<String, Object, Object> copy = record.withId(RecordId.autoGenerate());
                log.info("redis-消息队列,{},{},range-挂起消息>0>add+++++copy {},{}",key,group,copy.getStream(),copy);
                //XADD stream-name id field value [field value]
                RecordId add = redisTemplate.opsForStream().add(copy);
                log.info("redis-消息队列,{},{},range-挂起消息>0>add+++++copy {},{}",key,group,add,copy);
                log.info("redis-消息队列,{},{},range-挂起消息>0>ack^^^^^old {},{}",key,group,record.getStream(),record);
                //XACK stream group id [id id ...]
                Long siz = redisTemplate.opsForStream().acknowledge(GROUP, record);
                log.info("redis-消息队列,{},{},range-挂起消息>0>ack^^^^^old [{}],{}",key,group,siz,record);
                log.info("redis-消息队列,{},{},range-挂起消息>0>del-----old {},{}",key,group,record.getStream(),record);
                //XDEL key ID [ID ...]
                siz = redisTemplate.opsForStream().delete(record);
                log.info("redis-消息队列,{},{},range-挂起消息>0>del-----old [{}],{}",key,group,siz,record);
            }else{
                log.info("redis-消息队列,{},{},range-挂起消息>0> {},{},{}",key,group,record.getStream(),id,record);
            }
        }
        log.info("redis-消息队列,{},{},pending-挂起消息-end",key,group);
    }
    @Bean
    @ConditionalOnProperty(name = "spring.redis.msg.listener",havingValue = "true")
    public StreamMessageListenerContainer<String, ObjectRecord<String, String>> tspConsumerListener(
            RedisConnectionFactory factory){
        //spring-data-redis 2.3.1.RELEASE及更高版本中,createGroup如果不存在,则会自动使用创建流
        return streamContainer(StreamOffset.create(STREAM, ReadOffset.lastConsumed()),Consumer.from(GROUP,CONSUMER)
                ,factory,new TspStreamListener());
    }
    private StreamMessageListenerContainer<String, ObjectRecord<String, String>> streamContainer(StreamOffset<String> offset
            ,Consumer consumer, RedisConnectionFactory factory, StreamListener<String, ObjectRecord<String, String>> listener) {
        // pollTimeout 拉取消息超时时间,targetType 传递的数据类型, executor 线程池
        StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer
                .create(factory, StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                        .builder().pollTimeout(Duration.ofSeconds(50)).batchSize(1).targetType(String.class).build());
        //指定消费最新的消息
        try {
            //创建消费者,接收上次处理未ACK消费的消息,指定消费者对象,autoAcknowledge 关闭自动ack确认
            container.register(StreamMessageListenerContainer.StreamReadRequest
                    .builder(offset).errorHandler((error) -> log.error(error.getMessage()))
                    .cancelOnError(e -> false).consumer(consumer).autoAcknowledge(false)
                    .build(), listener);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        container.start();
        return container;
    }
    
    /** 消费者 监听 */
    public static class TspStreamListener implements StreamListener<String, ObjectRecord<String,String>> {
        @Override
        public void onMessage(ObjectRecord<String, String> message) {
            RecordId messageId = message.getId();
            // 消息的key和value
            String string = message.getValue();
            if("T".equals(string)){
                log.info("消费者-监听>>>测试消息-ack. msgId={}, stream={}, body={}", messageId, message.getStream(), string);
                redisTemplate.opsForStream().acknowledge(GROUP, message);
                return;
            }
            log.info("消费者-监听>>>get msg. msgId={}, stream={}, body={}", messageId, message.getStream(), string);
            //业务逻辑,需要考虑 被挂起的消息 重新消费情况
            try {
                log.info("消费者-监听>>>睡眠10s,模拟耗时逻辑-start");
                TimeUnit.SECONDS.sleep(10);
                log.info("消费者-监听>>>睡眠10s,模拟耗时逻辑-end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("消费者-监听>>>手动确认消息");
            redisTemplate.opsForStream().acknowledge(GROUP, message);
            log.info("消费者-监听>>>end");
        }
    }
}

redis

在这里插入图片描述

日志

2024-01-26 10:33:54.566 - [main] INFO  o.s.b.w.s.c.ServletWebServerApplicationContext.prepareWebApplicationContext(292) - Root WebApplicationContext: initialization completed in 1162 ms
2024-01-26 10:33:58.388 - [main] INFO  demo.RedisStreamConfig.handlerPending(85) - redis-消息队列,tsp,tsp-g-1,pending-挂起消息[3],minId : 1706178903044-0,maxId : 1706233950802-0
2024-01-26 10:33:58.396 - [main] INFO  demo.RedisStreamConfig.handlerPending(88) - redis-消息队列,tsp,tsp-g-1,pending-挂起消息,PendingMessages{groupName='tsp-g-1', range=[--+], pendingMessages=[PendingMessage{id=1706178903044-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=57524430, totalDeliveryCount=1}, PendingMessage{id=1706233940011-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=2477103, totalDeliveryCount=1}, PendingMessage{id=1706233950802-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=1968963, totalDeliveryCount=1}]}
2024-01-26 10:33:58.397 - [main] INFO  demo.RedisStreamConfig.handlerPending(93) - redis-消息队列,tsp,tsp-g-1,pending-挂起消息>>,PendingMessage{id=1706178903044-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=57524430, totalDeliveryCount=1}
2024-01-26 10:33:58.404 - [main] INFO  demo.RedisStreamConfig.handlerPending(95) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>> 1706178903044-0,[]
2024-01-26 10:33:58.405 - [main] INFO  demo.RedisStreamConfig.handlerPending(93) - redis-消息队列,tsp,tsp-g-1,pending-挂起消息>>,PendingMessage{id=1706233940011-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=2477103, totalDeliveryCount=1}
2024-01-26 10:33:58.409 - [main] INFO  demo.RedisStreamConfig.handlerPending(95) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>> 1706233940011-0,[MapBackedRecord{recordId=1706233940011-0, kvMap={data=303}}]
2024-01-26 10:33:58.409 - [main] INFO  demo.RedisStreamConfig.handlerPending(104) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0>add+++++copy tsp,MapBackedRecord{recordId=*, kvMap={data=303}}
2024-01-26 10:33:58.413 - [main] INFO  demo.RedisStreamConfig.handlerPending(106) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0>add+++++copy 1706236438413-0,MapBackedRecord{recordId=*, kvMap={data=303}}
2024-01-26 10:33:58.413 - [main] INFO  demo.RedisStreamConfig.handlerPending(107) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0>ack^^^^^old tsp,MapBackedRecord{recordId=1706233940011-0, kvMap={data=303}}
2024-01-26 10:33:58.418 - [main] INFO  demo.RedisStreamConfig.handlerPending(109) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0>ack^^^^^old [1],MapBackedRecord{recordId=1706233940011-0, kvMap={data=303}}
2024-01-26 10:33:58.419 - [main] INFO  demo.RedisStreamConfig.handlerPending(110) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0>del-----old tsp,MapBackedRecord{recordId=1706233940011-0, kvMap={data=303}}
2024-01-26 10:33:58.420 - [main] INFO  demo.RedisStreamConfig.handlerPending(112) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0>del-----old [1],MapBackedRecord{recordId=1706233940011-0, kvMap={data=303}}
2024-01-26 10:33:58.421 - [main] INFO  demo.RedisStreamConfig.handlerPending(93) - redis-消息队列,tsp,tsp-g-1,pending-挂起消息>>,PendingMessage{id=1706233950802-0, consumer=tsp-g-1:tsp-c-1, elapsedTimeSinceLastDeliveryMS=1968963, totalDeliveryCount=1}
2024-01-26 10:33:58.422 - [main] INFO  demo.RedisStreamConfig.handlerPending(95) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>> 1706233950802-0,[MapBackedRecord{recordId=1706233950802-0, kvMap={data=401}}]
2024-01-26 10:33:58.422 - [main] INFO  demo.RedisStreamConfig.handlerPending(114) - redis-消息队列,tsp,tsp-g-1,range-挂起消息>0> tsp,1706233950802-0,MapBackedRecord{recordId=1706233950802-0, kvMap={data=401}}
2024-01-26 10:33:58.422 - [main] INFO  demo.RedisStreamConfig.handlerPending(117) - redis-消息队列,tsp,tsp-g-1,pending-挂起消息-end

仅供参考-请结合实际情况使用

SpringBoot+Redis-Stream实现简单消息队列 Redis队列 阅读详情

相关推荐

使用 StringRedisTemplate 实现 Redis Stream 异步存储

业务系统产生数据,发送到 Redis Stream消费者从 Stream 中读取数据消费者将数据异步存储到 MySQL处理成功后确认消息,失败则留在 PEL 中等待重试。

Wyc72409的博客 573

在springboot中操作redis的stream

在springboot中操作redis的stream

进击的豌豆的博客 4266

微服务通信模式实战:从RPC到事件驱动,如何避免过度设计

在微服务架构中,服务间通信是核心设计环节,其本质是解决功能依赖、数据同步和系统解耦等问题。理解同步与异步通信的基本原理,是构建稳定、可扩展分布式系统的技术基础。同步调用(如RPC)适用于需要强一致性和实时响应的场景,而异步消息和事件驱动则通过解耦提升了系统的吞吐量和容错能力,其技术价值在于平衡一致性、可用性与性能。在实际应用场景中,例如电商订单流程,扣减库存需强一致性,适合同步调用;发送订单通知则可异步处理以提升用户体验。本文通过对比**RPC**、**消息队列**和**事件驱动**等模式,并结合Sprin

weixin_33853794的博客 457

【Java】SpringBoot中实现Redis Stream队列

在SpringBoot中实现Redis Stream队列

Syals的博客 4395

Springboot使用Redis Stream实现轻量消息队列

文章主要介绍了如何在Spring Boot项目中集成和使用Redis,特别是Redis Streams的功能。首先,通过引入spring-boot-starter-data-redis依赖,确保项目能够使用Spring Boot提供的Redis启动器。接着,配置了RedisTemplate,使用Jackson进行序列化,并支持Java 8日期时间模块。然后,详细说明了如何配置RedisStreamConfig,包括创建消息监听容器、生成流读取请求、检查Redis版本以及创建消费者组等。此外,还实现了Mess

wjianwei666的专栏 882

置顶必读(1) |《滚雪球学 Spring Boot 4.x》专栏目录导航篇(持续更新中)!

🏆 本文收录于 《滚雪球学 Spring Boot 4.x》 专栏。 本专栏面向 有一点 Java 基础,但没有系统学过 Spring Boot 的读者,采用“滚雪球式学习法”:先跑通、再理解、再重构、再上线,最终带你从第一个 Hello API 一路完成一个可部署、可监控、可扩展的后端项目。

**My Coding Family** 8万+

SpringBoot3整合Redis消息队列避坑指南

讲完案例、问题和专家建议,其实技术分享最有价值的还是大家的实践经验。我想问问你:你在项目中用过 Redis 做消息队列吗?有没有遇到过 “消息丢失”“重复消费” 这类问题?你是怎么解决的?或者你觉得 Redis 消息队列和 RabbitMQ、Kafka 比,有哪些优势和不足?欢迎在评论区留言分享你的经历,咱们一起交流避坑技巧。

Sgfcgg的博客 981

关于对鱼眼相机图片进行畸变校正的两种思路

鱼眼畸变主要表现为径向畸变(Radial Distortion),图像中心正常,边缘向外拉伸。畸变模型通常基于入射角θ\thetaθ(光线与光学轴夹角)和图像半径rrr的关系。等距投影(Equidistant Projection)rdfθrd​fθ,其中rdr_drd​为畸变图像半径,fff为焦距,θ\thetaθ为入射角。等立体角投影(Equisolid Angle Projection)rd2fsin⁡θ2rd​2fsinθ/2。

qq_45670407的博客 1543

RabbitMq java版本 demo

queue 队列是rabblt做流量肖峰最常用的一个队列了吧? 首先我们要封装个工厂类用于创建连接关闭连接使用 public class MqConnect { public static Connection connect() throws IOException, TimeoutException { ConnectionFactory connectionFactory=new ConnectionFactory(); connectionFa.

crazyo2jam的博客 519

方法二:旋转机械动平衡精度计算

本文以实际案例为基础,系统阐述基于ISO 1940标准的动平衡精度G GG等级计算方法。通过角速度转换、许用剩余不平衡量e p e r e_{per}e per ​ 推导及G GG等级公式应用,结合惯性力偶矩、转子质量等参数,得出某转子系统的动平衡精度为G 40 G40G40。文中详细解析了公式原理与参数单位转换要点,并附完整ISO 1940动平衡等级参考表,为工程师提供从理论到实践的完整解决方案。研究结果表明,该方法可有效量化旋转机械的平衡精度,适用于风机、泵类等中低速设备的设计与调试,对提升设备运行

weixin_46153372的博客 920

告别环境依赖!用PyInstaller把PaddleOCR项目打包成独立EXE(附完整spec文件配置)

本文详细介绍了如何使用PyInstaller将PaddleOCR项目打包成独立EXE文件,彻底解决Python环境依赖问题。通过完整的spec文件配置和实战技巧,开发者可以轻松创建开箱即用的OCR工具,特别适合需要交付给非技术用户的场景。文章涵盖环境配置、模型打包、性能优化等关键环节,并提供了解决常见问题的方案。

weixin_33712987的博客 573

Java开发者Redis实战指南:从环境搭建到高级应用与生产运维

缓存是提升系统性能、应对高并发的核心技术之一,其核心原理是将高频访问的数据存储在访问速度更快的介质中,以减少对后端数据库的直接压力。在Java生态中,Redis凭借其丰富的数据结构、高性能和原子操作特性,成为实现分布式缓存、会话存储乃至分布式锁等场景的首选技术方案。对于Java开发者而言,掌握Redis不仅意味着能使用Spring Boot的`@Cacheable`注解进行快速集成,更涉及对客户端选型(如Lettuce与Redisson)、序列化策略、连接池配置等工程实践的深入理解。本文聚焦于**Redis

weixin_30540691的博客 435

R 分析裂区试验设计

裂区试验有主区部分和副区部分, 因而有主区部分误差(误差 a,简记为Ea )和副区部分误差(误差b,简记为Eb) 分别用于测验主区处理以及副区处理和主、副互作的显著性。对同一个二因素试验资料作自由度和平方和的分解,则可发现随机区组的误差项自由度和平方和分别为DFe、SSe,而裂区设计有两个误差项,其自由度和平方和分别为DFEa,DFEb, SSEa, SSEb。而区组,处理效应等各个变异项目的自由

samhuairen的专栏 8933

基于Spring Boot构建跨平台内容发布引擎:解耦业务与平台SDK

在内容分发领域,跨平台发布是开发者面临的常见挑战,其核心在于如何实现业务逻辑与具体平台实现的解耦。通过抽象与适配器模式,可以将快手、抖音、哔哩哔哩等平台的SDK差异进行统一封装,构建一个标准化的发布接口。这种设计的技术价值在于显著降低代码重复和维护成本,提升系统的可扩展性和稳定性。在工程实践中,采用异步任务队列和状态机管理,能够高效处理发布流程中的网络超时、平台限流等异常。应用场景广泛适用于需要自动化运营多平台内容矩阵的团队,例如拥有自研内容中台或需要进行规模化分发的业务。本文以Spring Boot和Re

weixin_33804582的博客 412

基于FPGA的函数信号发生器设计

设计采用波形查找表和相位累加器的方法实现DDS,查找表的数据位宽为8位,采样点数为4096。波形产生范围是100Hz-20MHz,最小频率间隔为1Hz,用8个数码管显示频率,可产生正弦波、方波、三角波,波形用两个led显示,select信号控制,全灭为正弦波,一亮一灭为方波,全亮为锯齿波。为了使频率和相位便于调节,用3个拨码开关控制步进,用8个led显示状态。

FPGA入门到跑路 5633

一个基于ZYNQ、AXI接口的PL端卷积加速器,可由PS端调用(附Github源码)

一个基于ZYNQ,AXI接口的PL端卷积加速器,可由PS端调用开发板型号:Zynq-7000 SoC XC7Z305 FPGA开发平台:Vivado 2019.1;Github源码链接:https://github.com/CY0807/ZYNQ_PL_CONV_ACC。

跳跳骑士的博客 1305

消息队列丢消息原因与排查:生产端、Broker、消费端全解析

消息队列是分布式系统中解耦和削峰的核心组件,但“消息到底会不会丢”一直是工程实践中的关键疑问。要理解可靠性,需要先明确一条消息从生产者到消费者的完整生命周期,再逐段分析可能丢失的环节。生产端依赖确认机制、重试与事务保证发送成功;Broker端依赖持久化、副本同步和刷盘策略防止存储丢失;消费端则需手动确认与幂等设计应对重复和遗漏。这些原理不仅适用于Kafka、RocketMQ、RabbitMQ等主流中间件,也适用于基于Redis Stream的轻量级实现。在实际项目中,结合XPENDING、消费组、消息轨迹等

weixin_34220179的博客 330

如何获取SAP BTP账户及BAS启用

返回到 Service Marketplace 点击SAP Business Application Studio 然后go to Application。进入子用户的OverView里面找到Entitlements添加SAP Business Application Studio。注意:在选购BTP免费环境的时候,使用人要选公司才可以成功。在完成上述操作后,就可以登陆Cockpit。这里有SAP官方的BTP试用的手顺链接。配置权限※我绑定的是登录时的邮箱。※过程中有问题可以在评论区讨论。

wangshulong2013的博客 721

消息队列丢消息?从生产端到消费端的全链路可靠方案解析

在分布式系统中,消息队列是实现异步解耦与流量削峰的核心组件,而消息可靠性则是工程实践中无法绕开的关键议题。一条消息从生产者发出到消费者最终处理,需要经过网络传输、Broker持久化、副本同步以及消费位点提交等多个环节,任何一个环节的确认机制或持久化策略设置不当,都可能导致消息丢失。理解Kafka的acks机制、ISR副本同步、RocketMQ的刷盘策略,以及消费端Offset提交与重复消费的微妙关系,是构建高可靠消息链路的基础。工程师需要结合生产环境中的具体参数配置与幂等设计,在吞吐量和数据安全之间做出合理

weixin_30859423的博客 366

Markdown编辑器介绍

这篇文章介绍了Markdown编辑器的功能和使用方法。主要内容包括:Markdown的基本语法如标题、列表、链接插入等;新增功能如图片拖拽、数学公式支持、甘特图和UML图表绘制;常用快捷键说明;以及文本导出导入操作。文章详细说明了如何通过Markdown语法实现文本格式化、代码高亮、表格创建等功能,并提供了多种图表绘制的语法示例,帮助用户更好地利用Markdown编辑器进行写作和内容创作。

soso1968的博客 1178

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

SpringBoot+Kafka+Redis三件套:Java面试深度解析与避坑

现代Java后端系统普遍面临高并发、数据一致性和系统解耦的挑战。理解核心中间件的运作原理,是设计可靠架构的基础。Kafka基于顺序写盘与零拷贝实现高吞吐,适合削峰与异步解耦;Redis依靠内存数据结构提供低延迟访问,并需通过分布式锁、缓存穿透治理等方案保障一致性。两者在Spring Boot工程中常被组合使用,覆盖流量接入、缓存加速与消息通信场景。从概念到原理,再到生产环境中的配置与避坑,系统梳理这三者的协作方式,能有效提升Java开发者的工程实践能力与面试竞争力。文章从基础概念到生产实战,系统拆解Spri

weixin_31309419的博客 62

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

高并发抽奖系统架构设计:从权重概率到保底机制的工业级实现

在游戏和电商等高并发业务场景中,随机奖励发放(如抽奖、盲盒)是提升用户参与度的核心功能。其技术原理通常基于权重概率算法,通过为每个奖品分配权重值来计算中奖几率,确保结果的随机性与公平性。该技术的核心价值在于,能在海量请求下精确控制奖品发放概率,同时支持复杂的业务规则,如保底机制和概率递增,从而平衡用户体验与商业目标。典型的应用场景包括游戏道具抽取、营销活动开奖和虚拟盲盒开启。本文以“蛋仔抽盲盒”为具体案例,深入剖析了在高并发、高流量挑战下,如何通过融合Redis原子操作、异步处理和风控策略,构建一套保证数据

weixin_33910460的博客 316

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Java面试高频考点拆解:从源码原理到分布式架构实战

Java面试考察早已不止于基础知识背诵,而是围绕源码原理、并发机制与分布式场景展开深度追问。从集合框架的ArrayList扩容与HashMap hash算法,到JVM内存结构、垃圾回收与OOM排查,再到synchronized和AQS、volatile和ThreadLocal的底层设计,每一个考点都对应真实工程中的性能瓶颈与异常场景。Spring Boot自动配置、MyBatis的SQL注入防护、MySQL索引与事务隔离级别、Redis缓存穿透与击穿、Kafka消息可靠性以及分布式锁的实现方案,共同构成了当

weixin_34295316的博客 313

消息队列RabbitMQ入门:原生Java客户端连接、生产者消费者与消息可靠性

消息队列是分布式系统架构中用于解耦、异步和削峰填谷的基础中间件,能让高并发场景下的服务间通信更稳定可靠。RabbitMQ作为AMQP协议的标准实现,凭借灵活的路由策略和完善的管理界面,在微服务架构中应用广泛。理解其核心模型——生产者将消息交给交换机,交换机依据路由键分发至队列,消费者从队列拉取并处理——是掌握消息通信原理的关键。在实际工程实践中,原生Java客户端连接方式能更直观地理解底层交互细节,包括连接参数配置、交换机与队列声明、消息发布与消费确认等环节。只有掌握了这些基础操作,才能进一步设计消息不丢不

weixin_34034670的博客 247

一文看懂Redis(附示例代码+高频面试题)

概括来说,Redis是跑在内存里的高性能 KV 数据库,经常拿来做缓存,但是它不只是缓存。

后端胖头鱼的博客 334

Java实战WMS仓储管理系统:DDD领域建模与高并发库存分配设计

在复杂的企业级应用开发中,事务一致性与并发控制是保障系统数据准确性的核心基础。其原理通常涉及数据库锁机制、乐观锁、分布式锁等技术,用于解决多线程或多进程环境下共享资源的竞争问题。这些技术的价值在于确保业务逻辑的原子性和数据完整性,尤其在电商、物流、金融等高并发场景中至关重要。以仓储管理系统(WMS)为例,库存的锁定与扣减是典型的高并发业务场景,直接关系到是否会出现超卖或数据错乱。本文将结合领域驱动设计(DDD)的落地实践,深入探讨如何通过设计合理的库存聚合根、运用乐观锁机制以及实现库存分配策略,来构建一个健

weixin_34348174的博客 376
上一篇: 【Git】git仓库完整迁移
下一篇: springCloudGateway+nacos自定义负载均衡-通过IP隔离开发环境
privateobject
博客等级 码龄9年 107粉丝 86原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值