19、整合Redis发布订阅与Caffeine本地缓存实现多级缓存架构

一、Redis与Caffeine:

1、Redis简介:

Redis 是一款开源的分布式内存数据库,支持多种数据结构(字符串、哈希、列表等),常被用作分布式缓存。

核心特性:

(1)、独立于应用进程的分布式服务,可部署在单独的服务器 / 集群中。

(2)、支持持久化(RDB/AOF),数据可落地到磁盘,重启后不丢失。

(3)、天然支持多实例共享,所有应用节点访问同一份缓存数据。

(4)、提供过期策略、发布订阅、事务等高级功能。

2、Caffeine简介:

Caffeine 是 Java 领域的高性能本地缓存库,基于内存存储,常嵌入在应用进程内部使用。

核心特性:

(1)、本地内存缓存,数据存储在应用进程的 JVM 堆内存中。

(2)、采用W-TinyLFU 淘汰算法,缓存命中率极高(优于 Guava 等其他本地缓存)。

(3)、支持过期策略(写入后过期、访问后过期)、异步加载等功能。

(4)、无网络开销,访问速度极快(微秒级,比 Redis 快 1-2 个数量级)。

特性

Caffeine

Guava Cache

Ehcache

ConcurrentHashMap

核心定位

高性能本地缓存(Java 首选)

谷歌出品的老牌缓存库

全功能缓存(支持本地 / 分布式)

JDK 内置哈希表(无缓存特性)

淘汰算法

W-TinyLFU(最优命中率)

LRU(较简单,命中率一般)

LRU/LFU/FIFO 等(可配置)

无(需手动管理)

并发性能

极高(无锁并发,微秒级)

中等(分段锁,毫秒级)

一般(适合低并发)

高(CAS 操作)

过期策略

支持(写入后 / 访问后 / 自定义)

支持(写入后 / 访问后)

支持(丰富,含磁盘过期)

异步支持

原生支持(异步加载 / 刷新)

有限(需手动结合线程池)

支持(配置复杂)

持久化

不支持(纯内存)

不支持(纯内存)

支持(磁盘持久化)

不支持

内存占用

低(高效数据结构)

中(较早期实现)

高(功能复杂,冗余度大)

低(原生结构)

Spring 集成

完美支持(默认本地缓存实现)

支持(需适配)

支持(需额外配置)

需手动封装

适用场景

高并发、高频读、低延迟需求

简单场景,兼容老项目

需持久化或复杂缓存策略

简单临时缓存(无过期需求)

典型优势

性能和命中率碾压同类

生态成熟,兼容性好

功能全面,支持磁盘存储

零依赖,JDK 原生

典型劣势

不支持持久化

性能落后,算法较旧

重量级,高并发下性能一般

无缓存核心功能(需手写)

3、Redis与Caffeine的区别:

维度

Redis(分布式缓存)

Caffeine(本地缓存)

存储位置

独立服务器 / 集群的内存(可持久化到磁盘)

应用进程的 JVM 堆内存

访问速度

快(毫秒级,受网络延迟影响)

极快(微秒级,无网络开销)

分布式支持

天然支持(多实例共享同一份数据)

不支持(每个实例有自己的本地缓存,可能不一致)

数据容量

大(可通过集群扩容,不受单节点内存限制)

小(受 JVM 内存限制,过大易导致 OOM)

可靠性

高(支持主从、哨兵、集群,数据可持久化)

低(应用重启数据丢失,依赖本地内存稳定性)

适用场景

分布式环境数据共享、跨实例缓存同步

单实例高频访问、低延迟需求、本地临时数据缓存

 

二、Spring Boot整合Redis发布订阅与Caffeine本地缓存实现多级缓存架构:

1、POM配置:

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Caffeine本地缓存 -->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.3</version>
        </dependency>

2、YML配置:

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    timeout: 300000
    pool:
      # 连接池最大连接数(使用负值表示没有限制)
      max-active: 8
      # 连接池中的最大空闲连接
      max-idle: 5
      # 连接池最大阻塞等待时间(使用负值表示没有限制)
      max-wait: -1
      #  连接池中的最小空闲连接
      min-idle: 0

3、Entity类声明:

public class MsgConstant {

    /**
     * REDIS监听的消息主题(常量)
     */
    public static final String MSG_TOPIC_INFO = "msg:topic:info";

    /**
     * REDIS消息键
     */
    public static final String MSG_INFO = "msg:info:";

}

4、Redis与Caffeine配置:

RedisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * RedisTemplate内部的序列化配置器默认采用JDK序列化器
     * 使用默认序列化配置器查看数据时会导致数据乱码,需重新定义RedisTemplate序列化方案
     * 修改存储对象的序列化问题
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }

}

CaffeineConfig

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 缓存配置类:定义 Caffeine 缓存实例及策略(Spring 管理)
 *
 */
@Configuration
public class CaffeineConfig {

    /**
     * 缓存 Bean:
     *
     * 默认无过期时间
     * 默认无最大容量限制
     * 默认使用 LRU(最近最少使用)淘汰策略,但因未设置最大容量,实际不会触发淘汰
     */
    @Bean("oneCache")
    public Cache<String, Object> oneCache() {
        return Caffeine.newBuilder()
                // 最大容量:避免内存溢出
//                .maximumSize(1000)
                // 过期策略:写入后30分钟过期
//                .expireAfterAccess(30, TimeUnit.DAYS)
                // 记录缓存统计(命中率、淘汰数,便于监控)
//                .recordStats()
                .build();
    }

}

RedisPubAndSubConfig

import com.iven.rediscaffeinedemo.constants.MsgConstant;
import com.iven.rediscaffeinedemo.listener.CacheSyncByRedisListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

/**
 * Redis消息发布配置
 */
@Configuration
public class RedisPubAndSubConfig {

    /**
     * 注入监听消息的处理器Bean
     */
    @Autowired
    private CacheSyncByRedisListener cacheSyncByRedisListener;

    /**
     * 配置消息监听容器
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 注册监听器与主题
        container.addMessageListener(
            defaultMediaListenerAdapter(), 
            new PatternTopic(MsgConstant.MSG_TOPIC_INFO)
        );
        return container;
    }

    /**
     * 配置消息监听适配器
     * (绑定处理器和方法)
     *
     * @return
     */
    @Bean
    public MessageListenerAdapter defaultMediaListenerAdapter() {
        // 绑定处理器Bean和处理方法名
        return new MessageListenerAdapter(cacheSyncByRedisListener, "receiveAndHandleSubMsg");
    }

}

5、Redis订阅监听:

import com.github.benmanes.caffeine.cache.Cache;
import com.iven.rediscaffeinedemo.constants.MsgConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * Redis消息订阅者
 */
@Slf4j
@Component
public class CacheSyncByRedisListener {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    @Qualifier("oneCache")
    private Cache<String, Object> oneCache;

    /**
     * 订阅消息处理方法
     *
     * @param message
     */
    public void receiveAndHandleSubMsg(String message) {
        String msg = message;
        if (message != null && message.startsWith("\"") && message.endsWith("\"")) {
            msg = message.substring(1, message.length() - 1);
        }
        log.info("订阅消息:{}", msg);
        if("delete".equals(msg)){
            // 清理本地缓存
            deleteMsgCache(msg);
        }
        if("saveOrUpdate".equals(msg)){
            // 更新默认视频
            saveOrUpdateMsgCache(msg);
        }
        log.info("已处理订阅消息:{}", msg);
    }

    /**
     * 删除 Caffeine 缓存
     */
    private void deleteMsgCache(String msg) {
        String key = MsgConstant.MSG_INFO + msg;
        oneCache.invalidate(key);
        log.info("已清理缓存:{}", key);
    }

    /**
     * 更新 Caffeine 缓存
     */
    private void saveOrUpdateMsgCache(String msg) {
        String key = MsgConstant.MSG_INFO + msg;
        Object redisMsgInfo = redisTemplate.opsForValue().get(key);
        if(redisMsgInfo == null){
            log.error("saveOrUpdateMsgCache can not find redisMsgInfo!");
            return;
        }
        oneCache.put(key, redisMsgInfo);
        log.info("已更新缓存:{}", key);
    }

    /**
     * 批量配置缓存
     * key: MsgConstant.MSG_INFO + msg;
     *
     * @param batchMsg 批量消息
     */
    public void putAllMsgCache(Map<String, Object> batchMsg) {
        redisTemplate.opsForValue().multiSet(batchMsg);
        oneCache.putAll(batchMsg);
    }
    
}

6、服务启动-缓存初始化:

import com.iven.rediscaffeinedemo.constants.MsgConstant;
import com.iven.rediscaffeinedemo.listener.CacheSyncByRedisListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class MsgHandleInitRunner implements CommandLineRunner {

    @Autowired
    private CacheSyncByRedisListener cacheSyncByRedisListener;

    @Override
    public void run(String... args){
        log.info("开始初始化缓存...");
        Map<String, Object> batchMsg = new HashMap<>();
        batchMsg.put(MsgConstant.MSG_INFO + "delete", "初始化DELETE标签");
        batchMsg.put(MsgConstant.MSG_INFO + "saveOrUpdate", "初始化SAVEORUPDATE标签");
        cacheSyncByRedisListener.putAllMsgCache(batchMsg);
        log.info("初始化缓存完成!");
    }

}

7、Service处理:

MsgHandleService

public interface MsgHandleService {

    /**
     * 删除缓存
     *
     */
    void deleteMsgCache();

    /**
     * 更新缓存
     *
     */
    void updateMsgCache();

    /**
     * 查询缓存
     *
     * @param key delete/saveOrUpdate
     * @return
     */
    Object queryCache(String key);

}

MsgHandleServiceImpl

import com.github.benmanes.caffeine.cache.Cache;
import com.iven.rediscaffeinedemo.constants.MsgConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class MsgHandleServiceImpl implements MsgHandleService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    @Qualifier("oneCache")
    private Cache<String, Object> oneCache;

    @Override
    public void deleteMsgCache() {
        String redisKey = MsgConstant.MSG_INFO + "delete";
        redisTemplate.opsForValue().set(redisKey, "已清理");
        // 发送消息
        redisTemplate.convertAndSend(MsgConstant.MSG_TOPIC_INFO, "delete");
        log.info("已清理缓存:{}", redisKey);
    }

    @Override
    public void updateMsgCache() {
        String redisKey = MsgConstant.MSG_INFO + "saveOrUpdate";
        redisTemplate.opsForValue().set(redisKey, "已更新");
        // 发送消息
        redisTemplate.convertAndSend(MsgConstant.MSG_TOPIC_INFO, "saveOrUpdate");
        log.info("已更新缓存:{}", redisKey);
    }

    @Override
    public Object queryCache(String key) {
        Object result = oneCache.getIfPresent(MsgConstant.MSG_INFO + key);
        log.info("已查询缓存:{}", result);
        return result;
    }

}

 

三、Redis消息队列:

相关学习笔记

1、Redis实现消息队列方式:

方式

核心优势

核心劣势

适用场景

List数据结构

简单、高性能

无 ACK、无持久化保证

简单通知、低可靠性场景

Pub/Sub发布订阅模式

广播能力、实时性高

无持久化、无 ACK

实时通知、日志同步

Stream数据结构

可靠(持久化 + ACK)、负载均衡

略复杂

核心业务消息、分布式任务

ZSet数据结构

支持延迟消息

轮询延迟、无 ACK

延迟任务(如超时处理)

2、Redis Stream数据结构:

 1

Redis Stream 是 Redis 5.0 引入的专为为消息队列设计的持久化数据结构,其核心设计可概括为 “一份日志消息 + 两种消费模式”,底层通过结构化存储和状态跟踪实现可靠消息传递:

(1)、消息存储:

日志式持久化

底层形态:

每个Stream都有唯一的名称,作为Redis的key。所有消息按写入顺序存储在一个 “不可变日志” 中,每条消息有唯一 ID(时间戳 + 序列号),物理上只追加、不修改。

可靠性保障:

1、消息写入时同步持久化到磁盘(AOF 日志或 RDB 快照),Redis 重启后可完整恢复。

2、支持通过 MAXLEN 限制日志长度,自动淘汰最旧消息,避免内存溢出。

核心作用:

为所有消费者提供 “单一可信数据源”,确保消息不丢失、有序性。

(2)、无状态模式-消费者简单消费:

对应命令:

XREAD(无消费组)

底层处理:

消费者直接从 “消息日志” 中按 ID 范围读取消息(如从 0-0 读所有消息,或从 $ 读最新消息)

无状态跟踪:

Redis 不记录 “谁读了消息”“是否处理完成”,消息读取后与消费者彻底无关(类似 “读文件”,文件本身不记录读者状态)。

支持阻塞等待:

无新消息时消费者阻塞,有新消息写入时立即唤醒,减少空轮询。

适用场景:

日志实时查看、简单通知(不关心消息是否被处理成功)。

(3)、有状态模式-消费组相关消费:

基于消费组的消费是 Stream 实现 “可靠分布式消息传递” 的核心,底层通过 “消费组状态跟踪” 区分两种场景:

  • 1)场景一:负载均衡消费

同组消费者互斥

底层处理:

1、消费组内所有消费者共享一个 “已处理进度”(last_delivered_id),标识组内已处理到的最新消息 ID。

2、新消息到来时,Redis 按顺序将消息分配给组内不同消费者(轮询或负载均衡),确保同一条消息仅被一个消费者获取。(支持并行处理多条消息)

3、消息分配后,临时记录在组内的 “待确认列表(PEL)” 中,标记归属的消费者。

核心目标:

多消费者分摊任务,避免重复处理(如订单消息由多个服务实例分担处理)。

  • 2)场景二:消息重试与确认

确保处理成功

底层处理:

1、消费者处理完消息后,需通过 XACK 命令通知 Redis,Redis 从 PEL 中删除该消息(标记为 “已处理”)。

2、若消费者崩溃或未 ACK,消息会一直保留在 PEL 中,组内消费者(包括新加入的)可重新读取并重试(通过 XREADGROUP 优先读取 PEL 中的消息)。

3、不同消费组的 PEL 完全独立,彼此不影响(如 group1 的未确认消息不干扰 group2 的消费)。

核心目标:

通过 PEL 和 ACK 机制,确保消息即使处理失败也能重试,最终被成功处理。

注:

PEL:

消费组内跟踪未确认消息的核心结构,确保消息处理的可靠性

pending IDs:

PEL 中记录的具体消息 ID,代表那些 “已读取但未完成处理” 的消息

3、Redis Stream相关操作命令:

(1)、XADD:

写入消息并持久化

模板

XADD stream_key [MAXLEN [~] count] [ID id] field1 value1 [field2 value2 ...]

核心参数

stream_key

必选,Stream 名称(如 stream:order)

MAXLEN [~] count

可选,限制最大消息数;

1、~:近似限制(性能优先)

2、count:具体条数

ID id

可选,消息 ID(格式 时间戳-序列号)

1、*:自动生成,推荐;

2、手动指定需大于当前最大 ID

field/value

必选,消息内容(键值对,至少 1 对)

简单案例

# 自动生成 ID,写入订单支付消息,持久化到 Stream

XADD stream:order * orderId 1001

(2)、XLEN:

查询 Stream 消息总数

模板

XLEN stream_key

核心参数

stream_key

必选,要查询的 Stream 名称

简单案例

# 统计订单 Stream 当前消息总数

XLEN stream:order

(3)、XDEL:

删除 Stream 内单条 / 多条消息

模板

XDEL stream_key msg_id [msg_id ...]

核心参数

stream_key

必选,Stream 名称(如 stream:order)

msg_id

必选,要删除的消息 ID(支持批量)

简单案例

# 物理删除单条订单消息,删除后不可恢复

# XDEL stream:order 1763607608605-0 1763607608606-0

XDEL stream:order 1763607608605-0

(4)、DEL:

删除整个 Stream 及所有关联数据

模板

DEL stream_key

核心参数

stream_key

必选,Stream 名称(如 stream:order)

简单案例

# 彻底删除订单 Stream,包括所有消息、消费组、PEL 等(不可恢复)

DEL stream:order

(5)、EXISTS:

模板

EXISTS stream_key

核心参数

stream_key

必选,Stream 名称(如 stream:order)

简单案例

# Redis 通用命令,返回 1 表示键存在,0 表示不存在,支持批量检查

EXISTS stream:order

(6)、XREAD:

直接读取消息,无状态跟踪

模板

XREAD [BLOCK milliseconds] [COUNT count] STREAMS stream_key id

核心参数

BLOCK milliseconds

可选,阻塞时间(毫秒);

1、0 :永久阻塞

2、不写则非阻塞(默认)

COUNT count

可选,单次最多读取条数

STREAMS

必选,固定关键字

stream_key

必选,要读取的 Stream 名称

id

必选,起始 ID

1、0-0:读所有历史

2、$:只读新增消息

简单案例

# 非阻塞,读取所有历史订单消息

XREAD STREAMS stream:order 0-0

(7)、XGROUP CREATE:

创建消费组

模板

XGROUP CREATE stream_key group_name id [MKSTREAM]

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,消费组名称(如 group:pay)

id

必选,起始 ID

1、$:从最新消息,

2、0-0:从第一条历史消息

MKSTREAM

可选,Stream 不存在则自动创建(避免报错)

简单案例

# 为订单 Stream 创建消费组,从最新消息开始消费

XGROUP CREATE stream:order group:pay $ MKSTREAM

(8)、XREADGROUP:

组内消费者读取消息

模板

XREADGROUP GROUP group_name consumer_name [BLOCK milliseconds] [COUNT count] STREAMS stream_key id

核心参数

GROUP group_name

必选,消费组名称(需先创建)

consumer_name

必选,消费者名称(自定义,组内唯一,如 consumer_1)

BLOCK milliseconds

可选,阻塞时间(毫秒);

1、0:永久阻塞

COUNT count

可选,单次最多读取条数

stream_key

必选,关联的 Stream 名称

id

必选,起始 ID

1、>:读未分配新消息

2、0-0:读所有未 ACK 消息

简单案例

# 消费组内消费者阻塞 3 秒,读取未分配新消息

XREADGROUP GROUP group:pay consumer_1 BLOCK 3000 STREAMS stream:order >

(9)、XPENDING:

查看消费组待确认消息

模板

XPENDING stream_key group_name [start] [stop] [count] [consumer_name]

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,消费组名称

start

可选参数,开始ID,用于指定查看挂起消息的起始点

1、-:最小 ID

stop

可选参数,结束ID,用于指定查看挂起消息的终点

1、+:最大 ID

count

可选,最多返回条数

consumer_name

可选,指定消费者(只查看该消费者未确认消息)

简单案例

# 查看支付组内所有未 ACK 消息统计(总数、起止 ID)

XPENDING stream:order group:pay

# 查看消费者未确认的 5 条日志消息

XPENDING stream:order group:pay - + 5 consumer_1

(10)、XACK:

消息确认,从PEL移除

模板

XACK stream_key group_name msg_id [msg_id ...]

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,消费组名称

msg_id

必选,要确认的消息 ID(支持批量确认)

简单案例

# 确认单条消息已处理,从消费组PEL中删除

XACK stream:order group:pay 1763608382529-0

(11)、XGROUP CREATECONSUMER:

添加消费者到消费组

模板

XGROUP CREATECONSUMER stream_key group_name consumer_name

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,目标消费组名称(已创建)

consumer_name

必选,新增消费者名称(组内唯一)

简单案例

# 向支付组添加新消费者,参与负载均衡

XGROUP CREATECONSUMER stream:order group:pay consumer_2

(12)、XINFO CONSUMERS:

查看消费组内消费者状态

模板

XINFO CONSUMERS stream_key group_name

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,消费组名称

简单案例

# 查看支付组内所有消费者的状态(名称、已处理消息数、PEL 消息数)

XINFO CONSUMERS stream:order group:pay

(13)、XGROUP DELCONSUMER:

从消费组删除消费者

模板

XGROUP DELCONSUMER stream_key group_name consumer_name

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,消费组名称

consumer_name

必选,要删除的消费者名称

简单案例

# 从支付组删除消费者,其未ACK消息保留在组PEL中

XGROUP DELCONSUMER stream:order group:pay consumer_2

(14)、XGROUP DESTROY:

删除整个消费组

模板

XGROUP DESTROY stream_key group_name

核心参数

stream_key

必选,关联的 Stream 名称

group_name

必选,要删除的消费组名称

简单案例

# 删除支付消费组,组内 PEL、消费者同步删除(Stream 消息不受影响)

XGROUP DESTROY stream:order group:pay

(15)、XINFO STREAM:

查看Stream详细元数据

模板

XINFO STREAM stream_key

核心参数

stream_key

必选,要查询的 Stream 名称

简单案例

# 查看订单 Stream 的元数据(最新消息 ID、消费组数、消息总数等)

XINFO STREAM stream:order

注:

  • 1、消息存储:
XADD stream:order * orderId 1001

XLEN stream:order

XDEL stream:order 1763607608605-0

XLEN stream:order

DEL stream:order

EXISTS stream:order

2

  • 2、无状态模式-消费者简单消费:
XADD stream:order * orderId 1001

XREAD STREAMS stream:order 0-0

3

  • 3、有状态模式-消费组相关消费:
XGROUP CREATE stream:order group:pay $ MKSTREAM

XREADGROUP GROUP group:pay consumer_1 BLOCK 3000 STREAMS stream:order >

XADD stream:order * orderId 1002

XREADGROUP GROUP group:pay consumer_1 BLOCK 3000 STREAMS stream:order >

XREADGROUP GROUP group:pay consumer_1 BLOCK 3000 STREAMS stream:order >

XPENDING stream:order group:pay

XACK stream:order group:pay 1763608382529-0

XPENDING stream:order group:pay

4

  • 4、消费组管理:
XGROUP CREATECONSUMER stream:order group:pay consumer_2

XINFO CONSUMERS stream:order group:pay

XGROUP DELCONSUMER stream:order group:pay consumer_2

XINFO CONSUMERS stream:order group:pay

XGROUP DESTROY stream:order group:pay

XINFO CONSUMERS stream:order group:pay

XINFO STREAM stream:order

5

4、Redis Stream-Demo操作:

 RedisStreamTargetConsumer

public interface RedisStreamTargetConsumer {

    /**
     * Redis Stream定向消费
     */
    void redisStreamTargetConsumerHandle();

}

RedisStreamTargetConsumerImpl

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.PreDestroy;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class RedisStreamTargetConsumerImpl implements RedisStreamTargetConsumer {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 消费者线程池
    private ExecutorService executorService;

    // Redis Stream流名称
    private static final String STREAM_NAME = "stream:order";
    // Redis Stream消费组名称
    private static final String GROUP_NAME = "group:pay";
    // Redis Stream消费组消费者名称
    private static final String CONSUMER_NAME = "consumer_1";

    /**
     * 销毁方法,用于在Spring容器销毁时关闭线程池
     */
    @PreDestroy
    public void destroy() {
        if (executorService != null && !executorService.isShutdown()) {
            executorService.shutdown();
            try {
                if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                    executorService.shutdownNow();
                }
            } catch (InterruptedException e) {
                executorService.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public void redisStreamTargetConsumerHandle() {
        // 初始化线程池(单线程,避免多线程竞争)
        executorService = Executors.newSingleThreadExecutor(r -> {
            Thread thread = new Thread(r);
            thread.setName("redis-stream-target-consumer");
            // 守护线程:JVM 退出时自动终止
            thread.setDaemon(true);
            return thread;
        });

        // 1. 初始化Stream和消费组(确保存在)
        initStreamAndGroup();

        // 2. 发送测试消息
        sendTestMessage();

        // 3. 持续消费目标订单消息
        startTargetConsume();
    }

    /**
     * 初始化Stream和消费组(不存在则创建)
     */
    private void initStreamAndGroup() {
        try {
            // 检查消费组是否存在
            StreamInfo.XInfoGroups groups = redisTemplate.opsForStream().groups(STREAM_NAME);
            boolean groupExists = groups.stream()
                    .anyMatch(group -> GROUP_NAME.equals(group.groupName()));

            if (!groupExists) {
                // 不存在则创建消费组,从Stream开头开始消费(0-0表示从头开始)
                redisTemplate.opsForStream().createGroup(STREAM_NAME, ReadOffset.from("0-0"), GROUP_NAME);
                log.info("成功创建Redis Stream消费组,streamName: {}, groupName: {}", STREAM_NAME, GROUP_NAME);
            } else {
                log.info("Redis Stream消费组已存在,streamName: {}, groupName: {}", STREAM_NAME, GROUP_NAME);
            }
        } catch (Exception e) {
            // 如果Stream不存在,创建消费组会失败,此时先创建Stream
            if (e.getMessage().contains("no such key")) {
                log.info("Redis Stream不存在,先创建stream: {}", STREAM_NAME);

                // 初始化创建Stream
                Map<String, Object> initMsg = new HashMap<>();
                initMsg.put("init", true);
                redisTemplate.opsForStream().add(StreamRecords.newRecord().ofMap(initMsg).withStreamKey(STREAM_NAME).withId(RecordId.autoGenerate()));
                // 再次创建消费组
                redisTemplate.opsForStream().createGroup(STREAM_NAME, ReadOffset.from("0-0"), GROUP_NAME);
                log.info("创建Stream后,成功创建消费组: {}", GROUP_NAME);
            } else {
                log.error("初始化Redis Stream消费组失败", e);
                throw new RuntimeException("初始化Redis Stream消费组失败", e);
            }
        }
    }

    /**
     * 发送测试消息
     */
    private void sendTestMessage() {
        try {
            Map<String, Object> testMessage = new HashMap<>();
            testMessage.put("orderId", "1001");
            testMessage.put("createTime", System.currentTimeMillis());

            // 发送消息到Stream, 自动生成消息ID
            RecordId recordId = redisTemplate.opsForStream().add(
                    StreamRecords.newRecord()
                            .ofMap(testMessage)
                            .withStreamKey(STREAM_NAME)
                            .withId(RecordId.autoGenerate())
            );

            log.info("发送测试消息成功,recordId: {}, 消息内容: {}", recordId, testMessage);
        } catch (Exception e) {
            log.error("发送测试消息失败", e);
        }
    }

    /**
     * 启动定向消费
     */
    private void startTargetConsume() {
        log.info("启动目标订单消费者,开始监听orderId: {} 的消息,consumerName: {}", "1001", CONSUMER_NAME);

        executorService.submit(() -> {
            while (true) {
                try {
                    // 1. 构建消费者(消费组+消费者名称)
                    Consumer consumer = Consumer.from(GROUP_NAME, CONSUMER_NAME);

                    // 2. 构建读取配置(不调用noack(),保持默认noack=false → 手动ACK)
                    StreamReadOptions readOptions = StreamReadOptions.empty()
                            // 无消息时阻塞,避免空轮询
                            .block(Duration.ofSeconds(10))
                            // 每次最多拉取10条消息
                            .count(10);

                    // 3. 构建Stream偏移量:从消费组上次消费位置继续(ReadOffset.lastConsumed())
                    StreamOffset<String> streamOffset = StreamOffset.create(STREAM_NAME, ReadOffset.lastConsumed());

                    // 4. 读取消息(消费组模式:未ACK的消息会进入Pending List)
                    List<MapRecord<String, String, Object>> records = redisTemplate.opsForStream()
                            .read(consumer, readOptions, new StreamOffset[]{streamOffset});

                    // 5. 处理消息,手动ACK
                    handleRecords(records);

                } catch (Exception e) {
                    log.error("消费者异常,休眠1秒后重试", e);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        });
    }

    /**
     * 处理拉取到的消息
     */
    private void handleRecords(List<MapRecord<String, String, Object>> records) {
        if (CollectionUtils.isEmpty(records)) {
            log.debug("未拉取到任何消息,继续等待");
            return;
        }

        // 处理目标消息并确认
        for (MapRecord<String, String, Object> record : records) {
            try {
                // 1. 获取消息详情
                RecordId recordId = record.getId();
                Map<String, Object> message = record.getValue();

                // 2. 业务处理(这里可以替换为实际的业务逻辑)
                log.info("成功消费目标订单消息,recordId: {}, 消息内容: {}", recordId, message);

                // 3. 手动确认消息(标记为已处理,从Pending List移除)
                redisTemplate.opsForStream().acknowledge(STREAM_NAME, GROUP_NAME, recordId);
                log.info("消息确认成功,recordId: {}", recordId);

            } catch (Exception e) {
                log.error("处理目标订单消息失败,recordId: {}, 消息内容: {}", record.getId(), record.getValue(), e);
                // 消息处理失败,不会ack,会留在Pending List,后续会重新消费
                // 可以根据实际需求添加重试机制或死信队列处理
            }
        }
    }

}

6

 

posted on 2025-11-24 11:16  爱文(Iven)  阅读(34)  评论(0)    收藏  举报

导航