
本文讲解如何使用 redis 的发布/订阅机制,在多个后端服务实例间实现缓存一致性通知(如用户更新后广播清空缓存),重点解决因复用 jedis 连接导致 publish 失败的常见错误。
本文讲解如何使用 redis 的发布/订阅机制,在多个后端服务实例间实现缓存一致性通知(如用户更新后广播清空缓存),重点解决因复用 jedis 连接导致 publish 失败的常见错误。
在分布式 Java 后端系统中,当多个服务实例共享本地缓存(如 Caffeine 或 Guava Cache)时,必须确保数据变更能及时同步到所有节点。Redis 的 Pub/Sub 功能是轻量、低延迟的理想选择:一个实例发布 user_update 事件,其余所有订阅该频道的实例收到消息后主动清理对应缓存。
但实践中,一个典型误区是复用同一个 Jedis 连接对象既做订阅又做发布。Jedis 的 subscribe() 方法会进入阻塞式监听模式(底层调用 SUBSCRIBE 命令),此时该连接被独占用于接收消息流,Redis 服务器将拒绝在此连接上执行任何非订阅类命令(如 PUBLISH),从而抛出异常:
redis.clients.jedis.exceptions.JedisDataException: ERR Can't execute 'publish': only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context
✅ 正确做法是:严格分离连接职责——为订阅(Subscriber)和发布(Publisher)分别创建独立的 Jedis 实例:
// 1. 创建专用订阅连接(长期存活,用于监听)
Jedis subscriberJedis = new Jedis("localhost", 6379);
JedisPubSub userUpdateListener = new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("[SUB] Received on " + channel + ": " + message);
// 触发本地用户缓存清除逻辑,例如:
// userCache.invalidateAll();
}
};
new Thread(() -> {
try {
subscriberJedis.subscribe(userUpdateListener, "user_update");
} catch (Exception e) {
// 处理连接中断等异常,建议重连
e.printStackTrace();
}
}).start();
// 2. 创建专用发布连接(按需使用,非阻塞)
Jedis publisherJedis = new Jedis("localhost", 6379);
// 在用户更新成功后调用:
publisherJedis.publish("user_update", "{\"userId\":\"123\",\"timestamp\":" + System.currentTimeMillis() + "}");
publisherJedis.close(); // 发布完成后及时释放⚠️ 注意事项:
立即学习“Java免费学习笔记(深入)”;
-
不要复用、不要共享:
subscriberJedis和publisherJedis必须是两个物理独立的连接(即使指向同一 Redis 地址); - 线程安全:Jedis 实例不支持多线程并发访问,每个线程应持有自己的 Jedis 对象;
-
连接管理:生产环境推荐使用
JedisPool管理发布连接池,避免频繁创建销毁开销;订阅连接因长期运行,建议封装为单例 + 心跳保活; - 可靠性补充:Redis Pub/Sub 是“即发即弃”模型(无持久化、无确认),若需高可靠通知,可结合 Redis Streams 或外部消息队列(如 Kafka);但对于缓存失效这类“尽力而为”场景,Pub/Sub 已足够高效。
通过职责分离与连接隔离,即可稳定实现跨 JVM 实例的实时通信,保障多副本服务下的缓存最终一致性。


















