Fix lỗi Redis 'ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context'

intermediate🔴 Redis2026-05-07| Redis 2.x–7.x, mọi hệ điều hành (Linux/macOS/Windows), mọi Redis client (redis-cli, redis-py, ioredis, Jedis)

Error Message

(error) ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT / RESET allowed in this context
#redis#pubsub#subscribe#publish#context

Lỗi Gặp Phải

Bạn chạy một lệnh Redis — SET, GET, HSET, PUBLISH, hay bất kỳ lệnh nào khác — và nhận được phản hồi này:

(error) ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT / RESET allowed in this context

Kết nối đó đang bị kẹt trong chế độ subscriber Pub/Sub. Khi một kết nối đã gọi SUBSCRIBE hoặc PSUBSCRIBE, Redis sẽ khóa nó lại hoàn toàn. Kết nối này chỉ chấp nhận các lệnh liên quan đến subscriber cho đến khi thoát khỏi chế độ đó. Mọi lệnh khác — kể cả PUBLISH — đều gây ra lỗi này.

Nguyên Nhân

Chế độ Pub/Sub được giới hạn theo từng kết nối. Ngay khi client gửi SUBSCRIBE channel, kết nối TCP đó sẽ vào trạng thái lắng nghe chuyên biệt. Redis chặn tất cả các lệnh khác trên kết nối đó là có chủ đích — Redis cần đảm bảo việc gửi nhận tin nhắn không bị gián đoạn bởi các thao tác đọc/ghi trên cùng một socket.

Các tình huống phổ biến gây ra lỗi này:

  • Dùng chung một instance Redis client cho cả việc subscribe và publish/ghi dữ liệu
  • Connection pool phân phối một kết nối đã ở trạng thái subscribe cho các lệnh thông thường
  • Gọi PUBLISH trên kết nối subscriber — lỗi rất hay gặp, vì PUBLISH là lệnh ghi, không phải lệnh subscriber
  • Quên unsubscribe trước khi tái sử dụng một kết nối

Cách Khắc Phục

Kết nối subscriber phải được dùng riêng biệt. Không có ngoại lệ. Dùng một kết nối dành riêng cho SUBSCRIBE/PSUBSCRIBE, và một kết nối hoàn toàn khác cho mọi thứ còn lại — kể cả PUBLISH.

Khắc phục trong redis-cli

Mở hai terminal — mỗi terminal một vai trò:

# Terminal 1 — chỉ dùng để subscribe
redis-cli
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)

# Terminal 2 — publisher / các lệnh thông thường
redis-cli
127.0.0.1:6379> PUBLISH news "hello"
(integer) 1
127.0.0.1:6379> SET foo bar
OK

Để thoát chế độ Pub/Sub ở phía subscriber, nhấn Ctrl-C, hoặc gửi lệnh UNSUBSCRIBE. Trên Redis 6.2+, lệnh RESET cũng hoạt động.

Khắc phục trong Python (redis-py)

redis-py cung cấp đối tượng PubSub tự quản lý kết nối riêng phía sau — chính xác là để bạn không vô tình trộn lẫn chúng:

import redis
import threading

r = redis.Redis(host='localhost', port=6379)

# Subscriber — đối tượng pubsub riêng biệt (kết nối của chính nó)
def message_handler(message):
    print(f"Received: {message['data']}")

pubsub = r.pubsub()
pubsub.subscribe(**{'news': message_handler})

# Chạy subscriber trong thread nền
thread = pubsub.run_in_thread(sleep_time=0.01)

# Kết nối chính — dùng r cho mọi thứ khác
r.set('foo', 'bar')          # OK — dùng kết nối chính
r.publish('news', 'hello')   # OK — dùng kết nối chính, không phải pubsub

thread.stop()

Lỗi thường gặp — dùng trực tiếp đối tượng kết nối thô:

# Cách này ổn — r và pubsub là hai đối tượng riêng biệt
pubsub = r.pubsub()
pubsub.subscribe('news')
r.set('foo', 'bar')  # OK

# Cách này gây lỗi:
raw_conn = r.connection_pool.get_connection('_')
raw_conn.send_command('SUBSCRIBE', 'news')
raw_conn.send_command('SET', 'foo', 'bar')  # ERR only (P)SUBSCRIBE...

Khắc phục trong Node.js (ioredis)

Khởi tạo hai client riêng biệt — một để subscribe, một cho mọi thứ còn lại:

const Redis = require('ioredis');

const subscriber = new Redis();
const publisher = new Redis();

subscriber.subscribe('news', (err, count) => {
  if (err) throw err;
  console.log(`Subscribed to ${count} channel(s)`);
});

subscriber.on('message', (channel, message) => {
  console.log(`[${channel}] ${message}`);
});

// publisher xử lý tất cả các lệnh ghi và PUBLISH
publisher.publish('news', 'hello');
publisher.set('foo', 'bar');

Khắc phục trong Java (Jedis)

JedisPool pool = new JedisPool("localhost", 6379);

// Subscriber — block thread, cần kết nối riêng biệt
new Thread(() -> {
    try (Jedis jedis = pool.getResource()) {
        jedis.subscribe(new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                System.out.println(channel + ": " + message);
            }
        }, "news");
    }
}).start();

// Các lệnh thông thường — lấy kết nối riêng từ pool
try (Jedis jedis = pool.getResource()) {
    jedis.publish("news", "hello");
    jedis.set("foo", "bar");
}

Reset kết nối subscriber bị kẹt (Redis 6.2+)

Có kết nối đang kẹt trong chế độ Pub/Sub mà bạn muốn tái sử dụng? RESET đưa nó trở về trạng thái bình thường ngay lập tức — không cần đóng và mở lại socket:

127.0.0.1:6379> SUBSCRIBE news
...
127.0.0.1:6379> RESET
+RESET

Trên các phiên bản Redis trước 6.2, hãy UNSUBSCRIBE khỏi tất cả các channel trước, hoặc đơn giản là đóng kết nối và mở kết nối mới.

Kiểm Tra Sau Khi Sửa

  • Chạy logic subscriber của bạn — xác nhận rằng nó nhận tin nhắn bình thường.
  • Trên kết nối riêng biệt, thực hiện một lệnh ghi: SET test ok phải trả về OK.
  • Publish từ kết nối đó: PUBLISH news "ping" phải trả về một số nguyên (số lượng subscriber), không phải lỗi.
  • Kiểm tra rằng subscriber của bạn thực sự nhận được tin nhắn — nếu có, cả hai kết nối đều hoạt động tốt.
# Kiểm tra nhanh với hai terminal
# Terminal 1
redis-cli SUBSCRIBE test-channel

# Terminal 2
redis-cli PUBLISH test-channel "works"
# Kết quả mong đợi: (integer) 1
# Terminal 1 sẽ hiển thị:
# 1) "message"
# 2) "test-channel"
# 3) "works"

Tóm Tắt Nhanh

Các lệnh được phép trên kết nối subscriber:

  • SUBSCRIBE / UNSUBSCRIBE — quản lý đăng ký channel
  • PSUBSCRIBE / PUNSUBSCRIBE — quản lý đăng ký theo pattern
  • PING — kiểm tra keepalive (trả về pong dạng Pub/Sub đặc biệt, không phải +PONG thông thường)
  • QUIT — đóng kết nối
  • RESET — thoát chế độ Pub/Sub (Redis 6.2+)

Tất cả những lệnh khác — GET, SET, PUBLISH, HSET, EXPIRE, tất cả — đều phải thực hiện qua kết nối không phải subscriber.

Related Error Notes