Cách sửa lỗi Redis 'BUSY Redis is busy running a script'

intermediate🔴 Redis2026-04-16| Redis 2.6+, mọi hệ điều hành (Linux, macOS, Windows WSL), mọi Redis client (redis-cli, Python redis-py, Node ioredis, v.v.)

Error Message

(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
#redis#lua#script#hiệu năng#busy#blocking

Lỗi Gặp Phải

Bạn chạy một lệnh với Redis và nhận được phản hồi này:

(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

Redis đang bị kẹt khi thực thi một script Lua. Cho đến khi script đó hoàn thành — hoặc bị dừng buộc — mọi lệnh khác bạn gửi đều bị từ chối. Redis chạy đơn luồng: một script chạy không kiểm soát có thể giữ toàn bộ máy chủ làm con tin.

Nguyên Nhân

Redis thực thi các script Lua theo cơ chế nguyên tử thông qua EVAL hoặc EVALSHA. Tính nguyên tử này chính là ưu điểm cốt lõi — không có lệnh nào khác có thể chen ngang giữa chừng. Nhưng đánh đổi lại rất khắc nghiệt: một script chạy quá lâu sẽ làm đóng băng toàn bộ Redis instance của bạn.

Thời gian chờ mặc định là 5000ms (lua-time-limit). Vượt ngưỡng đó, Redis ngừng nhận tất cả mọi lệnh ngoại trừ SCRIPT KILLSHUTDOWN NOSAVE. Các nguyên nhân phổ biến khiến script vượt quá giới hạn này:

  • Vòng lặp vô hạn hoặc lặp không giới hạn — ví dụ: quét 10 triệu key bên trong một lần gọi EVAL
  • Vòng lặp redis.call() không có điều kiện thoát
  • Script quét 500 key chạy tốt trên môi trường dev nhưng gặp 5 triệu key trên production
  • Race condition khiến điều kiện kết thúc vòng lặp (cursor về 0) không bao giờ xảy ra

Cách Khắc Phục Từng Bước

Bước 1: Dừng Script Đang Chạy

Mở một terminal mới — kết nối hiện tại của bạn có thể đang bị treo. Kết nối vào Redis:

redis-cli -h 127.0.0.1 -p 6379

Sau đó chạy:

SCRIPT KILL

Nếu script chưa ghi dữ liệu nào, Redis sẽ dừng nó ngay lập tức:

OK

Xong. Máy chủ đã được giải phóng và sẵn sàng nhận lệnh bình thường trở lại.

Bước 2: Nếu SCRIPT KILL Trả Về Lỗi

Bạn nhận được thông báo này thay vào đó?

(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait it to finish or kill the server in a non destructive way.

Redis đang bảo vệ bạn khỏi tình trạng dữ liệu bị ghi dở. Một script đã thực hiện thao tác ghi không thể bị hủy an toàn giữa chừng. Có hai hướng xử lý:

  • Chờ script hoàn thành — nếu script đang tiến triển và sẽ kết thúc, hãy để nó chạy. Theo dõi trạng thái bằng redis-cli --latency hoặc xem log của Redis.
  • Khởi động lại Redis — nếu script thực sự bị kẹt trong vòng lặp vô hạn, khởi động lại là lựa chọn duy nhất. Chọn cách phù hợp:
# Linux systemd
sudo systemctl restart redis

# macOS Homebrew
brew services restart redis

# Direct process
sudo service redis-server restart

Về lệnh SHUTDOWN NOSAVE: lệnh này dừng máy chủ và loại bỏ tất cả dữ liệu đã ghi kể từ lần lưu RDB/AOF gần nhất. Chỉ dùng khi bạn chắc chắn rằng những thay đổi trong bộ nhớ đó có thể mất đi một cách an toàn.

Bước 3: Tìm Script Gây Ra Sự Cố

Sau khi máy chủ đã phản hồi bình thường, hãy tìm hiểu nguyên nhân gây ra trạng thái BUSY. Kiểm tra slow log:

redis-cli SLOWLOG GET 10

Tìm các mục EVAL hoặc EVALSHA trong kết quả. Thời gian thực thi từ 5.000.000 microseconds (5 giây) trở lên chính là thủ phạm. Đối chiếu với log ứng dụng để tìm đoạn code nào đã kích hoạt nó.

Bước 4: Sửa Script

Hầu hết lỗi BUSY đều có cùng nguyên nhân gốc: nhồi nhét quá nhiều xử lý vào một script Lua duy nhất. Dưới đây là cách khắc phục các trường hợp phổ biến.

Chia nhỏ các vòng quét không giới hạn:

-- XẤU: lặp cho đến khi cursor == 0, không có giới hạn trên
local cursor = 0
repeat
  local result = redis.call('SCAN', cursor, 'MATCH', 'prefix:*')
  cursor = tonumber(result[1])
  -- xử lý các key...
until cursor == 0

-- TỐT HƠN: chuyển vòng lặp quét về tầng ứng dụng hoàn toàn
-- Không thực hiện quét key đầy đủ bên trong một lần gọi EVAL

Chuyển logic nặng về code ứng dụng:

# Python redis-py — quét theo từng batch 100 key
import redis

r = redis.Redis()
cursor = 0
while True:
    cursor, keys = r.scan(cursor, match='prefix:*', count=100)
    for key in keys:
        # xử lý key
        pass
    if cursor == 0:
        break

Script Lua phát huy tác dụng nhất ở một việc: đọc-sửa-ghi nguyên tử trên một tập key nhỏ, xác định trước. Hãy đẩy việc xử lý hàng loạt, quét dữ liệu, và bất cứ thứ gì có số lần lặp thay đổi theo dữ liệu trở lại ứng dụng của bạn.

Bước 5: Điều Chỉnh lua-time-limit (Tùy Chọn)

Mặc định 5 giây cho script một khoảng thời gian hợp lý trước khi Redis bắt đầu từ chối lệnh. Bạn có thể thay đổi trong redis.conf:

# redis.conf
lua-time-limit 5000   # milliseconds

# Hoặc thay đổi trực tiếp mà không cần khởi động lại:
redis-cli CONFIG SET lua-time-limit 5000

Tăng giá trị này chỉ là trì hoãn vấn đề. Chỉ tăng khi bạn có một script cụ thể đã được kiểm tra kỹ với giới hạn thời gian thực thi rõ ràng — không dùng như giải pháp tạm thời cho một vòng lặp chưa được kiểm tra.

Xác Nhận Đã Khắc Phục

Kiểm tra nhanh sau khi dừng script hoặc khởi động lại máy chủ:

redis-cli PING
# Kết quả mong đợi: PONG

redis-cli INFO server | grep redis_version
# Nên trả về thông tin phiên bản mà không bị treo

Sau đó xác nhận cả đọc lẫn ghi đều hoạt động:

redis-cli SET test-key "hello"
# OK
redis-cli GET test-key
# "hello"
redis-cli DEL test-key

Cả ba lệnh phản hồi ngay lập tức? Vậy là ổn rồi.

Phòng Tránh Tái Phát

  • Kiểm thử với dữ liệu có quy mô như production. Script quét 100 key chỉ mất vài microseconds. Cùng script đó với 10 triệu key sẽ dễ dàng vượt 5 giây — và bạn sẽ không biết cho đến khi quá muộn.
  • Đặt timeout phía client để ứng dụng không bị chặn vô thời hạn khi chờ một lệnh EVAL bị kẹt:
# Python redis-py
r = redis.Redis(socket_timeout=5.0)

# Node ioredis
const client = new Redis({ commandTimeout: 5000 });
  • Không dùng vòng lặp không giới hạn trong Lua. Nếu số lần lặp phụ thuộc vào kích thước dữ liệu, hãy xử lý ở tầng ứng dụng bằng các lần gọi SCAN theo batch.
  • Theo dõi slow log trên production. Cảnh báo nếu bất kỳ lệnh EVAL nào vượt quá 1–2 giây — phát hiện script chậm ở 2 giây tốt hơn nhiều so với xử lý máy chủ đóng băng ở giây thứ 6.
  • Với Redis 7.0+, cân nhắc dùng FUNCTION thay vì EVAL. Functions hỗ trợ flag no-writes, cho phép SCRIPT KILL hoạt động ngay cả sau khi bắt đầu thực thi — loại bỏ hoàn toàn tình huống UNKILLABLE.

Related Error Notes