Có hai cách ghi lại một công thức trong bếp nhà hàng. Cách thứ nhất: viết vội lên tờ khăn giấy, tiện tay dùng trong ca — nhưng hết ca người ta dọn bàn là nó biến mất, và bếp bên cạnh chẳng có bản nào. Cách thứ hai: in nó vào cuốn sách công thức chính thức của nhà hàng — nó sống qua mọi ca, sao gửi được sang chi nhánh khác, và ai đọc cũng thấy đúng tên món chứ không phải một mã vạch vô nghĩa. Món ăn nấu ra thì y hệt nhau; khác biệt nằm hoàn toàn ở chỗ công thức được lưu ở đâu và ai quản được nó. Đó chính xác là khác biệt giữa Lua script và Functions của Redis, và bài này đo cả hai trên cùng một logic.
Redis 7 thêm Functions, và câu hỏi đầu tiên ai cũng hỏi là "khác gì Lua script".
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ộ — đúng cuốn sách công thức. Script thì chỉ nằm trong bộ nhớ tạm của tiến trình — tờ khăn giấy — 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à
keysvàargstruyền vào hàm, không phải biến toàn cụcKEYSvàARGV. - 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.
Mẫu số chung: logic là hiện vật được lưu và khai báo, không phải chuỗi tạm nạp lúc chạy
Cái làm Functions hơn EVAL — cùng tốc độ nhưng bền hơn, có tên, có hợp đồng — là một quy luật thiết kế chung: mã phía máy chủ nên là một hiện vật được lưu trữ, phiên bản hoá, và khai báo hợp đồng, chứ không phải một chuỗi vô danh nạp vào bộ nhớ tạm lúc chạy rồi mất khi khởi động lại. Và cái cờ no-writes được thi hành thật minh hoạ vế thứ hai: một hợp đồng khai báo cùng mã và được hệ thống ép buộc thắng một quy ước bạn phải nhớ ở mọi nơi gọi — vì quy ước hỏng đúng chỗ có người quên, còn hợp đồng thi hành thì không quên được.
Trong thế giới Java, đây đúng là @Transactional(readOnly = true): bạn khai báo ý định chỉ-đọc trên method và container thi hành nó, thay vì tin rằng mọi lối gọi đều nhớ không ghi — đúng vai trò no-writes. Rộng hơn, đây là giá trị của một hiện vật biên dịch và đóng gói (một jar có phiên bản, triển khai qua quy trình) so với eval một chuỗi lúc chạy; và của một kiểu/giao diện khai báo một lần được trình biên dịch kiểm khắp nơi so với một quy ước đặt tên.
Còn ở cơ sở dữ liệu, đây chính là stored procedure và ràng buộc khai báo: một CHECK/NOT NULL/FOREIGN KEY sống trong lược đồ, đi theo dữ liệu và được engine ép ở mọi đường ghi — mạnh hơn hẳn việc kiểm cùng luật đó trong từng service và hy vọng không ai quên. Function của Redis nằm trong RDB/AOF y như một stored procedure nằm trong CSDL: logic trở thành một phần của dữ liệu được sao lưu và nhân bản.
Trong Go, tinh thần ấy là kiểm thoả mãn interface lúc biên dịch (var _ Iface = (*T)(nil)) hay các build constraint khai báo — hợp đồng được máy kiểm, không phải tài liệu nhắc.
Mẫu số chung: logic phía máy chủ đáng là một hiện vật được lưu, nhân bản, đặt tên và mang hợp đồng khai báo mà runtime thi hành — không phải một chuỗi tạm bạn nạp lại mỗi lần và một quy ước bạn phải nhớ ở mọi nơi. Câu hỏi đúng khi chọn giữa hai cơ chế chạy cùng tốc độ không phải "cái nào nhanh hơn" mà là "cái nào biến ý định của tôi thành thứ được lưu cùng dữ liệu và được ép buộc thay vì phải ghi nhớ" — vì đó mới là chỗ hệ thống lớn lên mà không mục ruỗng.
Muốn tự 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.