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