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数据结构:

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、无状态模式-消费者简单消费:
XADD stream:order * orderId 1001 XREAD STREAMS stream:order 0-0

- 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、消费组管理:
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

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,后续会重新消费 // 可以根据实际需求添加重试机制或死信队列处理 } } } }

浙公网安备 33010602011771号