SET key value NX PX 30000 là công thức khoá phân tán được chép lại nhiều nhất. Bài này đo xem nó bảo đảm gì, và đo chính xác chỗ nó ngừng bảo đảm.
Khoá làm đúng việc của nó
10 worker cùng vào một vùng tô màu:
không khoá 599 lần hai người cùng vào | 0,3 giây
SET NX PX + Lua 0 lần | 3,1 giây
Chậm hơn 10 lần — đó đúng là cái giá của việc xếp hàng, không phải chi phí của Redis.
Công thức đầy đủ gồm hai nửa:
SET khoa <token-ngau-nhien> NX PX 30000
và khi giải phóng, phải dùng Lua:
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
DEL trần là sai: nếu khoá của bạn đã hết hạn và người khác đã lấy, DEL sẽ xoá khoá của họ. Mệnh đề Lua so token trước khi xoá, và nó nguyên tử — đúng lý do Lua tồn tại, như phần 14.
Nhưng chỉ khi công việc ngắn hơn TTL
| TTL | Công việc | Lần trùng vùng tô màu | Lần mất khoá trước khi xong |
|---|---|---|---|
| 1000 ms | 50 ms | 0 | 0 |
| 200 ms | 500 ms | 48 | 50 |
| 200 ms | 800 ms | 49 | 50 |
50 trên 50 lần lấy khoá đều mất khoá trước khi làm xong việc.
Dấu vết thật với ba worker, TTL 200 ms, công việc 800 ms:
worker 0 lấy được lúc 613,082
worker 1 lấy được lúc 613,283 <- đúng 200 ms sau, khi TTL hết
worker 2 lấy được lúc 613,483
worker 0 xong lúc 613,884 <- cả ba cùng trong vùng tô màu
cả ba giải phóng trả về :0 <- "tôi không còn là chủ"
Ba tiến trình cùng lúc, mỗi tiến trình đều tin rằng mình đang giữ khoá.
Mệnh đề bảo vệ đúng, nhưng muộn
Chú ý cả ba lần giải phóng đều trả về :0. Mệnh đề Lua hoạt động hoàn hảo: nó từ chối xoá khoá của người khác.
Nhưng nó chỉ cho biết sau khi đã xong. Nó không ngăn được việc ba tiến trình vừa cùng ghi vào cùng một chỗ.
Đây là điểm cốt lõi: khoá dựa trên TTL bảo đảm loại trừ lẫn nhau chỉ trong khoảng TTL. Vượt quá, nó im lặng ngừng bảo vệ. Không có lỗi, không có ngoại lệ, không có gì trong nhật ký cho tới khi bạn tự kiểm tra giá trị trả về.
Và công việc vượt TTL không phải chuyện hiếm: một lần dừng thu gom rác, một truy vấn cơ sở dữ liệu chậm bất thường, một lần máy ảo bị đình chỉ. Tiến trình không biết mình vừa đứng im ba giây.
Redlock không gỡ được điều này
Redlock lấy khoá trên N Redis độc lập và coi là thành công nếu quá bán đồng ý. Nó giải quyết một vấn đề thật: một Redis chết không làm mất khoá của mọi người.
Nhưng nó không đụng tới vấn đề đo được ở trên. Vấn đề đó không nằm ở việc lấy khoá; nó nằm ở chỗ thời gian trôi giữa lúc lấy và lúc dùng, và Redlock cũng dựa trên đồng hồ và TTL y hệt.
Đây chính là nội dung phê bình nổi tiếng của Martin Kleppmann, và phép đo ở trên là minh hoạ trực tiếp: kể cả với một Redis duy nhất hoàn toàn khoẻ mạnh, không mất gói tin, không phân vùng mạng, khoá vẫn hỏng khi công việc dài hơn TTL.
Vậy làm gì
Nếu khoá chỉ để tối ưu — tránh làm việc trùng lặp, giảm tải — thì SET NX PX là đủ. Hai tiến trình cùng làm một việc thỉnh thoảng chỉ tốn tài nguyên, không sai.
Phần lớn cách dùng khoá trong thực tế thuộc nhóm này: chỉ một máy chạy công việc định kỳ, chỉ một tiến trình làm mới bộ đệm.
Nếu khoá để bảo đảm tính đúng đắn — chỉ một tiến trình được ghi — thì khoá Redis không đủ, và Redlock cũng không. Bạn cần một trong hai:
- Rào chắn bằng số thứ tự. Mỗi lần lấy khoá cấp một số tăng dần; hệ thống lưu trữ từ chối ghi mang số nhỏ hơn số nó đã thấy. Tiến trình bị treo quay lại với số cũ sẽ bị từ chối. Điều này đòi hệ thống lưu trữ hợp tác — Redis một mình không làm được.
- Đưa tính đúng đắn về nơi lưu dữ liệu. Ràng buộc duy nhất, cập nhật có điều kiện, giao dịch. Nếu cơ sở dữ liệu của bạn từ chối ghi trùng thì bạn không cần khoá.
Nếu vẫn dùng khoá Redis, ba việc giảm thiểu:
- Đặt TTL rộng rãi hơn nhiều so với thời gian làm việc dự kiến. Không phải gấp đôi — gấp mười.
- Gia hạn định kỳ từ một luồng riêng (
PEXPIREvới cùng mệnh đề bảo vệ token). Đây là điều các thư viện khoá tốt tự làm. - Kiểm giá trị trả về lúc giải phóng. Nhận
0nghĩa là bạn vừa làm việc mà không có khoá — đáng ghi cảnh báo, vì nó là dấu hiệu TTL đang quá ngắn.
Điểm 3 rẻ nhất và ít được làm nhất. Ở phép đo trên, con số đó đi từ 0 lên 50/50 — nó là chỉ báo sớm hoàn hảo.
Thử ba mươi giây
Xem khoá của bạn có đang hết hạn giữa chừng không:
# do thoi gian lam viec that so voi TTL dang dat
redis-cli --scan --pattern 'lock:*' | while read k; do
echo "$(redis-cli pttl "$k") ms $k"
done | sort -n | head -20
Rồi trong mã, đổi phần giải phóng để đếm:
released = r.eval(SCRIPT, 1, key, token)
if released == 0:
metrics.increment("lock.mat_khoa_truoc_khi_xong")
Bộ đếm đó lớn hơn 0 nghĩa là hệ thống của bạn đang chạy đúng tình huống trong bài này — nhiều tiến trình cùng trong vùng tô màu — và không ai biết. Đó là con số đáng đặt cảnh báo hơn bất kỳ chỉ số nào khác về khoá.
Phần sau: bộ đệm — các mẫu dùng Redis làm bộ đệm và chỗ mỗi mẫu hỏng.