Redis 7 thêm Functions, và câu hỏi đầu tiên ai cũng hỏi là "khác gì Lua script". Bài này đo cả hai trên cùng một logic.

Kết quả sau khi khởi động lại, so sánh tốc độ, và các bảo đảm Functions thêm vào

Khác biệt lớn nhất: khởi động lại

Tôi nạp một script và một thư viện function làm cùng một việc, rồi khởi động lại Redis.

trước                        sau
-----------------------      -------------------------
script cache: 1              script cache: 0
thư viện function: 1         thư viện function: 1

EVALSHA <sha cũ>  ->  NOSCRIPT No matching script. Please use EVAL.
FCALL tang_va_han ->  7      (chạy bình thường)

Script biến mất. Function còn nguyên và chạy được ngay.

Lý do: function nằm trong chính dữ liệu. Nó đi theo RDB, theo AOF, và theo bản sao khi đồng bộ. Script thì chỉ nằm trong bộ nhớ tạm của tiến trình — không lưu, không nhân bản, mất khi khởi động lại.

Đó là lý do mọi thư viện khách hàng dùng Lua đều phải tự xử lý NOSCRIPT rồi nạp lại. Với Functions, lớp đó không cần nữa.

Nhưng không nhanh hơn chút nào

EVALSHA   20.256 ops/s
FCALL     20.520 ops/s     chênh 1,3%

Nằm trong nhiễu đo.

Điều này hợp lý khi biết cơ chế: Functions dùng cùng một máy Lua, cùng một cách chạy, cùng tính nguyên tử — và cùng đặc tính chặn máy chủ đã đo ở phần trước. Nó là một lớp quản lý bên trên, không phải một bộ thực thi khác.

Nên đừng chuyển sang Functions vì lý do hiệu năng. Lý do là vận hành.

Cái Functions có mà script không có

Cờ no-writes được thi hành thật.

redis.register_function{
  function_name = 'lay_hoac_dat',
  callback = lay_hoac_dat,
  flags = {'no-writes'}
}

Hàm này khai no-writes nhưng bên trong có gọi SET. Kết quả:

ERR Write commands are not allowed from read-only scripts

Redis chặn lúc chạy. Bạn không thể vô tình viết một hàm khai là chỉ đọc mà thực ra có ghi.

FCALL_RO từ chối ngay trước khi chạy.

fcall_ro tang_va_han  ->  ERR Can not execute a script with write flag using *_ro command

Lỗi này xuất hiện trước khi hàm chạy một dòng nào, dựa trên khai báo. Nghĩa là bạn có thể cho một ứng dụng chỉ-đọc quyền FCALL_RO và chắc chắn nó không ghi được gì — kể cả nếu thư viện function bị thay đổi sau đó mà quên khai cờ.

Với EVAL_RO bạn cũng có điều tương tự, nhưng phải nhớ dùng đúng lệnh ở mọi nơi gọi. Với Functions, cờ nằm cùng mã và đi theo mã.

Tên có nghĩa thay cho mã băm.

FCALL tang_va_han 1 dem 5 100

so với

EVALSHA 61636018f4e6... 1 dem 5 100

Đọc nhật ký lệnh chậm, đọc MONITOR, đọc commandstats — tất cả đều hiện tên hàm thay vì một chuỗi băm không nói gì.

Nạp trùng tên bị chặn.

FUNCTION LOAD           ->  ERR Library 'thuvien' already exists
FUNCTION LOAD REPLACE   ->  thuvien

Bạn phải nói rõ ý định thay thế. Với SCRIPT LOAD thì mọi lần nạp đều thành công và bạn tích thêm một mục cache — chính là chỗ rò bộ nhớ đo được ở phần trước.

FUNCTION DUMP để triển khai.

561 byte

Một khối nhị phân chứa toàn bộ thư viện, FUNCTION RESTORE được sang instance khác. Đây là thứ đưa mã phía máy chủ vào quy trình triển khai bình thường thay vì phải chạy một script nạp lúc khởi động ứng dụng.

Cấu trúc một thư viện

#!lua name=thuvien

local function tang_va_han(keys, args)
  local v = redis.call('INCRBY', keys[1], args[1])
  redis.call('EXPIRE', keys[1], args[2])
  return v
end

redis.register_function('tang_va_han', tang_va_han)

Ba điểm khác cú pháp so với EVAL:

  • Dòng đầu bắt buộc: #!lua name=<tên thư viện>.
  • Tham số là keysargs truyền vào hàm, không phải biến toàn cục KEYSARGV.
  • Phải gọi redis.register_function; hàm không đăng ký thì không gọi được từ ngoài — dùng làm hàm phụ bên trong thư viện.

Điểm cuối là thứ thật sự mới: một thư viện có nhiều hàm dùng chung mã. Với EVAL, mỗi script là một hòn đảo và mã dùng chung phải sao chép.

Nên dùng cái nào

Functions, nếu bạn viết mã phía máy chủ mới và dùng Redis 7 trở lên. Không có lý do kỹ thuật nào để chọn EVAL cho mã mới.

Script, nếu bạn phải chạy trên Redis 6, hoặc dùng dịch vụ quản lý chưa mở FUNCTION LOAD — một số nhà cung cấp chặn nó vì lý do bảo mật.

Đừng vội chuyển đổi cái đang chạy tốt. Lợi ích là vận hành, không phải hiệu năng, và việc chuyển đổi có rủi ro thật. Chuyển khi bạn đang sửa mã đó vì lý do khác.

Và điều quan trọng nhất vẫn giữ nguyên từ phần trước: Functions chặn máy chủ đúng như script. Cùng một luồng, cùng tính nguyên tử, cùng cái giá. Mọi quy tắc về độ dài script áp nguyên vào đây.

Thử ba mươi giây

Xem instance của bạn có sẵn Functions không và ai đang dùng gì:

redis-cli function list
redis-cli function stats
redis-cli info memory | grep -E 'number_of_libraries|number_of_functions|used_memory_functions|number_of_cached_scripts'

FUNCTION STATS cho biết có hàm nào đang chạy ngay lúc này — hữu ích khi nghi có script treo, vì nó trả lời được cả khi máy chủ đang bận và mọi lệnh khác nhận BUSY.

Và nếu number_of_cached_scripts lớn còn number_of_libraries bằng 0, bạn đang ở đúng tình huống bài này mô tả: mã phía máy chủ sống trong bộ nhớ tạm, mất sau mỗi lần khởi động lại, và không ai nhìn thấy nó trong bất kỳ bản sao lưu nào.

Phần sau: Pub/Sub — đo xem bản tin gửi khi không ai nghe đi về đâu.