Redis Pub/Sub — 概念、原理、场景与代码示例

Redis Pub/Sub --- 概念、原理、场景与代码示例

一、什么是 Redis Pub/Sub

Redis Pub/Sub(Publish/Subscribe) 是 Redis 内置的发布/订阅消息机制。发布者向一个"频道(Channel)"发送消息,所有订阅了该频道的客户端都能实时收到消息。

类比理解

复制代码
广播电台模型:

  发布者 = 电台主播
  频道   = FM 103.7
  订阅者 = 正在收听103.7的听众

  主播说了一句话 → 所有正在听的人都能听到
  如果你的收音机关着(不在线)→ 这句话你就永远听不到了(不持久化)

核心特点

特点 说明
实时性 发布后订阅者立即收到
不持久化 消息发完就没了,不会存储
无确认机制 不知道订阅者是否成功处理
不可回溯 不在线时发的消息,上线后拿不到
轻量级 无需额外中间件,Redis 自带
无队列 不是消息队列,是实时广播

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、与消息队列(MQ)的核心区别

复制代码
Redis Pub/Sub(广播模型):
  发布者 ──► 频道 ──► 订阅者A(在线✓ → 收到)
                  ──► 订阅者B(在线✓ → 收到)
                  ──► 订阅者C(离线✗ → 丢失)

RabbitMQ(队列模型):
  生产者 ──► 队列 ──► 消费者A(在线✓ → 消费)
                      消费者A离线?消息在队列中等着,
                      等A上线了再消费(持久化)
维度 Redis Pub/Sub RabbitMQ/Kafka
消息持久化 ❌ 不持久化 ✅ 持久化到磁盘
离线消费 ❌ 离线就丢 ✅ 上线后继续消费
消息确认 ❌ 无ACK ✅ 有ACK机制
消息堆积 ❌ 不支持 ✅ 可以堆积
重试机制 ❌ 无 ✅ 支持重试/死信
消费者分组 ❌ 所有订阅者都收 ✅ 同组只一个消费
性能 极高(内存操作) 高(需要磁盘IO)
部署依赖 只需Redis 需要额外中间件
适用场景 实时通知、缓存同步 业务解耦、可靠投递

三、Redis Pub/Sub 工作原理

复制代码
┌──────────────────────────────────────────────────────────┐
│                      Redis Server                         │
│                                                          │
│  Channel: "order.created"                                │
│     │                                                    │
│     ├── Subscriber 1 (连接中) ← 收到消息                  │
│     ├── Subscriber 2 (连接中) ← 收到消息                  │
│     └── Subscriber 3 (断开了) ← 收不到,消息丢失           │
│                                                          │
│  Channel: "stock.warning"                                │
│     │                                                    │
│     └── Subscriber 4 (连接中) ← 收到消息                  │
│                                                          │
└──────────────────────────────────────────────────────────┘

Publisher ──PUBLISH "order.created" "{...}"──► Redis
Redis ──推送消息──► 所有订阅了 "order.created" 的客户端

关键: 订阅者必须保持与 Redis 的长连接。连接断了就收不到消息。


四、Redis 命令

基础命令

bash 复制代码
# 订阅频道(客户端1)
SUBSCRIBE order.created stock.warning

# 发布消息(客户端2)
PUBLISH order.created '{"orderId":"SO001","userId":123}'

# 订阅模式匹配(通配符)
PSUBSCRIBE order.*      # 匹配 order.created、order.paid、order.shipped 等
PSUBSCRIBE stock.*      # 匹配 stock.warning、stock.low 等

命令演示

bash 复制代码
# 终端1:订阅者
127.0.0.1:6379> SUBSCRIBE order.created
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "order.created"
3) (integer) 1
# 等待消息...

# 终端2:发布者
127.0.0.1:6379> PUBLISH order.created '{"orderId":"SO001"}'
(integer) 1    # 返回值 = 收到消息的订阅者数量

# 终端1 立即收到:
1) "message"
2) "order.created"
3) "{\"orderId\":\"SO001\"}"

五、Java 代码示例

1. 使用 Spring Data Redis

配置:

java 复制代码
@Configuration
public class RedisPubSubConfig {

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 注册监听器
        container.addMessageListener(orderCreatedListener(), 
            new PatternTopic("order.*"));
        container.addMessageListener(stockWarningListener(), 
            new ChannelTopic("stock.warning"));
        return container;
    }

    @Bean
    public MessageListenerAdapter orderCreatedListener() {
        return new MessageListenerAdapter(new OrderCreatedSubscriber(), "onMessage");
    }

    @Bean
    public MessageListenerAdapter stockWarningListener() {
        return new MessageListenerAdapter(new StockWarningSubscriber(), "onMessage");
    }
}

发布者:

java 复制代码
@Service
@Slf4j
public class RedisEventPublisher {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 发布事件到Redis频道.
     */
    public void publish(String channel, Object event) {
        String message = JSON.toJSONString(event);
        log.info("Redis发布消息, channel:{}, message:{}", channel, message);
        stringRedisTemplate.convertAndSend(channel, message);
    }
}

订阅者:

java 复制代码
/**
 * 订单创建事件订阅者.
 */
@Slf4j
public class OrderCreatedSubscriber implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        log.info("收到Redis消息, channel:{}, body:{}", channel, body);

        try {
            OrderCreatedEvent event = JSON.parseObject(body, OrderCreatedEvent.class);
            // 处理业务逻辑
            handleOrderCreated(event);
        } catch (Exception e) {
            log.warn("处理Redis消息失败, channel:{}", channel, e);
        }
    }

    private void handleOrderCreated(OrderCreatedEvent event) {
        log.info("处理订单创建事件, orderId:{}", event.getOrderId());
        // 业务逻辑...
    }
}

2. 使用注解方式(Spring Boot)

java 复制代码
@Configuration
public class RedisPubSubConfig {

    @Bean
    public RedisMessageListenerContainer container(
            RedisConnectionFactory factory,
            OrderEventSubscriber orderSubscriber,
            CacheInvalidateSubscriber cacheSubscriber) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        
        // 订阅多个频道
        container.addMessageListener(orderSubscriber, new ChannelTopic("order.created"));
        container.addMessageListener(orderSubscriber, new ChannelTopic("order.paid"));
        container.addMessageListener(cacheSubscriber, new PatternTopic("cache.invalidate.*"));
        
        return container;
    }
}

@Component
@Slf4j
public class OrderEventSubscriber implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        log.info("订单事件, channel:{}, body:{}", channel, body);
    }
}

@Component
@Slf4j
public class CacheInvalidateSubscriber implements MessageListener {

    @Resource
    private CacheManager cacheManager;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String cacheKey = new String(message.getBody());
        log.info("缓存失效通知, channel:{}, key:{}", channel, cacheKey);
        // 清除本地缓存
        cacheManager.getCache("localCache").evict(cacheKey);
    }
}

3. 完整业务示例:多实例缓存同步

java 复制代码
/**
 * 场景:应用部署了3个实例,一个实例更新了缓存,需要通知其他实例清除本地缓存.
 */
@Service
@Slf4j
public class CacheSyncService {

    @Resource
    private StringRedisTemplate redisTemplate;
    @Resource
    private CaffeineCacheManager localCacheManager;

    private static final String CACHE_SYNC_CHANNEL = "cache.sync";

    /**
     * 数据更新后,发布缓存失效通知.
     */
    public void notifyCacheInvalidate(String cacheName, String key) {
        CacheSyncMessage msg = new CacheSyncMessage();
        msg.setCacheName(cacheName);
        msg.setKey(key);
        msg.setSourceInstance(getInstanceId());  // 标记来源,避免自己处理自己发的消息
        msg.setTimestamp(System.currentTimeMillis());

        redisTemplate.convertAndSend(CACHE_SYNC_CHANNEL, JSON.toJSONString(msg));
        log.info("发布缓存同步通知, cache:{}, key:{}", cacheName, key);
    }

    /**
     * 收到其他实例的缓存失效通知.
     */
    public void onCacheSyncMessage(String messageBody) {
        CacheSyncMessage msg = JSON.parseObject(messageBody, CacheSyncMessage.class);

        // 忽略自己发的消息
        if (getInstanceId().equals(msg.getSourceInstance())) {
            return;
        }

        // 清除本地缓存
        Cache cache = localCacheManager.getCache(msg.getCacheName());
        if (cache != null) {
            cache.evict(msg.getKey());
            log.info("本地缓存已清除, cache:{}, key:{}, from:{}", 
                    msg.getCacheName(), msg.getKey(), msg.getSourceInstance());
        }
    }

    private String getInstanceId() {
        // 每个实例的唯一标识(如 IP:PORT 或 UUID)
        return System.getProperty("instance.id", UUID.randomUUID().toString());
    }
}

@Data
class CacheSyncMessage {
    private String cacheName;
    private String key;
    private String sourceInstance;
    private Long timestamp;
}

六、模式匹配订阅(Pattern Subscribe)

Redis Pub/Sub 支持通配符订阅:

bash 复制代码
# 精确订阅
SUBSCRIBE order.created              # 只收 order.created

# 模式订阅
PSUBSCRIBE order.*                   # 收 order.created、order.paid、order.shipped...
PSUBSCRIBE *.warning                 # 收 stock.warning、price.warning...
PSUBSCRIBE *                         # 收所有频道的消息

代码中的模式订阅:

java 复制代码
// 精确频道
container.addMessageListener(listener, new ChannelTopic("order.created"));

// 模式匹配
container.addMessageListener(listener, new PatternTopic("order.*"));
container.addMessageListener(listener, new PatternTopic("cache.invalidate.*"));

七、适用场景

✅ 适合用 Redis Pub/Sub 的场景

场景 说明
多实例缓存同步 一个实例更新缓存,通知其他实例清除本地缓存
实时通知/聊天 WebSocket 配合 Pub/Sub 实现多节点消息分发
配置变更广播 配置中心修改后通知所有应用实例热加载
在线状态通知 用户上线/下线通知给好友列表
实时监控仪表盘 指标数据实时推送到前端

共同特征: 消息丢了不要紧,要求实时性,不需要历史回溯。

❌ 不适合的场景

场景 原因 应该用什么
订单处理 消息不能丢 RabbitMQ/Kafka
异步任务 需要重试和确认 RabbitMQ
日志采集 需要持久化和回溯 Kafka
业务解耦 需要可靠投递 RabbitMQ
延迟任务 需要延迟投递 RabbitMQ(延迟队列)/Redis(Sorted Set)

八、Redis Pub/Sub 的局限性

1. 消息丢失

复制代码
时间线:
  T1: 订阅者A在线,订阅者B离线
  T2: 发布者发布消息
  T3: 订阅者A收到 ✓,订阅者B收不到 ✗
  T4: 订阅者B上线
  T5: 订阅者B永远拿不到T2时刻的消息(已经丢了)

2. 无消息积压能力

如果订阅者处理速度跟不上发布速度,消息会堆积在 Redis 的输出缓冲区中,当缓冲区超过限制时 Redis 会断开订阅者的连接。

复制代码
# Redis配置中的保护机制
client-output-buffer-limit pubsub 32mb 8mb 60
# 含义:pubsub客户端输出缓冲区超过32mb,或持续60秒超过8mb,强制断开

3. 无消费确认

发布者不知道消息是否被成功处理:

复制代码
发布者:PUBLISH channel "message"  →  返回值只是"有几个订阅者收到了"
                                      不是"有几个订阅者成功处理了"

4. 集群模式下的限制

Redis Cluster 中,Pub/Sub 消息会广播到集群的所有节点,带来额外的网络开销。


九、Redis Pub/Sub 的升级方案:Redis Stream

Redis 5.0 引入了 Stream,弥补了 Pub/Sub 的不足:

维度 Pub/Sub Stream
持久化 ✅ 消息持久化存储
消费确认 ✅ ACK机制
消费者组 ✅ Consumer Group
历史回溯 ✅ 可从任意位置消费
消息积压 ❌ 会被丢弃 ✅ 按需存储
bash 复制代码
# Stream 基本用法
# 发布消息
XADD order.stream * orderId SO001 userId 123

# 消费消息(消费者组)
XREADGROUP GROUP notification-group consumer1 COUNT 1 BLOCK 5000 STREAMS order.stream >

# 确认消费
XACK order.stream notification-group 1234567890-0

如果你需要"Redis + 可靠性",用 Stream 而不是 Pub/Sub。


十、总结

复制代码
Redis Pub/Sub 是什么:
  Redis内置的实时广播机制
  发布者发消息到频道,所有在线订阅者立即收到

核心特征:
  实时性极高(毫秒级)
  不持久化(发完就没了)
  不可靠(离线就丢)
  极轻量(无需额外中间件)

适合场景:
  缓存同步、实时通知、配置广播
  共同点 = "丢了无所谓,要的是实时"

不适合场景:
  业务消息、订单处理、异步任务
  共同点 = "消息不能丢"

选择建议:
  需要可靠 → RabbitMQ/Kafka
  需要实时 + 可靠 → Redis Stream
  只需要实时广播 + 丢了无所谓 → Redis Pub/Sub
相关推荐
李高钢1 小时前
C# WPF Prism 进阶(二):区域(Region)与模块化(Module)
java·前端·数据库
long3161 小时前
枚举(Enums)
java·开发语言·数据库
TJHHH.1 小时前
SQL注入学习总结
数据库·笔记·sql·学习·注入
ACP广源盛139246256733 小时前
Qwen3.8‑2.4T 开源落地@ACP#国产 MoE 大模型私有化部署中 MST 多屏转换芯片 GSV2231 硬件价值与应用场景
大数据·数据库·人工智能·嵌入式硬件
java_logo3 小时前
Docker 部署 openGauss:轻松搭建企业级开源关系型数据库平台
数据库·docker·开源·opengauss·轩辕镜像·opengauss部署教程·opengauss部署文档
旺仔学长 哈哈3 小时前
52486|高校专业评估不再靠表格:Spring Boot 本科专业质量评估管理系统实战
数据库·spring boot·毕业设计·教育管理
梦想的旅途24 小时前
企业微信Webhook回调怎么做自动回复和AI客服
数据库·机器人·自动化·企业微信·rpa
Zguigo4 小时前
OCR 核心原理 + 模型 + 检测与识别
数据库·ocr
凤山老林4 小时前
零停机数据库演进:Spring Boot 集成 Flyway 与平滑DDL变更策略
数据库·spring boot·后端