Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Redis

Thành Thạo Redis Scripting Với Lua: Hướng Dẫn Thực Hành

Hello, Lua!

Script Redis Lua đầu tiên của chúng ta chỉ trả về một giá trị mà không thực sự tương tác với Redis theo bất kỳ cách có ý nghĩa nào:

local msg = "Hello, world!"
return msg

Đây đơn giản nhất có thể. Dòng đầu thiết lập một biến local với thông điệp của chúng ta, và dòng thứ hai trả về giá trị đó từ Redis server đến client. Lưu file này cục bộ như hello.lua và chạy nó như sau:

redis-cli --eval hello.lua

Vấn Đề Kết Nối?

Ví dụ redis-cli này giả định bạn đang chạy Redis server cục bộ. Nếu bạn làm việc với server từ xa như RedisGreen, bạn sẽ cần chỉ định thông tin host và port. Tìm nút Connect trên dashboard RedisGreen của bạn để nhanh chóng sao chép thông tin đăng nhập cho server của bạn.

Xem thêm: Could not connect to Redis at 127.0.0.1:6379: Connection refused.

Chạy lệnh này sẽ in ra "Hello, world!". Đối số đầu tiên của EVAL là script lua hoàn chỉnh — ở đây chúng ta dùng lệnh cat để đọc script từ file. Đối số thứ hai là số lượng Redis key mà script sẽ truy cập. Script "Hello World" đơn giản của chúng ta không truy cập key nào, nên chúng ta dùng 0.

Truy Cập Key Và Đối Số

Giả sử chúng ta đang xây dựng một URL shortener. Mỗi khi một URL đến, chúng ta muốn lưu trữ nó và trả về một số duy nhất có thể dùng để truy cập URL sau này.

Chúng ta sẽ dùng một Lua script để lấy một ID duy nhất từ Redis dùng INCR và ngay lập tức lưu URL trong một hash được key bởi ID duy nhất đó:

local link_id = redis.call("INCR", KEYS[1])
redis.call("HSET", KEYS[2], link_id, ARGV[1])
return link_id

Chúng ta truy cập Redis lần đầu tiên ở đây, dùng hàm call(). Các đối số của call() là các lệnh gửi đến Redis: trước INCR <key>, rồi HSET <key> <field> <value>. Hai lệnh này sẽ chạy tuần tự — Redis sẽ không làm gì khác trong khi script này thực thi, và nó sẽ chạy cực nhanh.

Chúng ta truy cập hai bảng (table) Lua, KEYS và ARGV. Tables là mảng liên hợp, và là cơ chế duy nhất của Lua để cấu trúc dữ liệu. Với mục đích của chúng ta, bạn có thể nghĩ về chúng như tương đương với array trong bất kỳ ngôn ngữ nào bạn thoải mái nhất, nhưng lưu ý hai đặc điểm của Lua khiến người mới bị rối:

  • Tables là one-based (cơ sở 1), tức là index bắt đầu từ 1. Vì vậy phần tử đầu tiên trong mytable là mytable[1], thứ hai là mytable[2], v.v.

  • Tables không thể giữ giá trị nil. Nếu một phép toán tạo ra table [ 1, nil, 3, 4 ], kết quả sẽ là [ 1 ] — table bị cắt ngắn (truncated) tại giá trị nil đầu tiên.

Khi chúng ta gọi script này, chúng ta cũng cần truyền giá trị cho các bảng KEYS và ARGV. Trong Redis protocol gốc, lệnh trông như sau:

EVAL $incrset.lua 2 links:counter links:url https://malcolmgladwellbookgenerator.com/

Khi gọi EVAL, sau script chúng ta cung cấp 2 là số lượng KEYS sẽ được truy cập, sau đó liệt kê KEYS của chúng ta, và cuối cùng cung cấp giá trị cho ARGV.

Thông thường khi chúng ta xây dựng app với Redis Lua script, thư viện client Redis sẽ lo việc chỉ định số lượng key. Khối code trên được hiển thị để đầy đủ, nhưng đây là cách dễ dàng hơn làm điều này ở command line:

redis-cli --eval incrset.lua links:counter links:urls , https://malcolmgladwellbookgenerator.com/

Khi dùng --eval như trên, dấu phẩy phân tách mục KEYS[] từ ARGV[].

Chỉ để làm rõ, đây là script gốc của chúng ta lại, lần này với KEYS và ARGV được mở rộng:

local link_id = redis.call("INCR", "links:counter")
redis.call("HSET", "links:urls", link_id, "https://malcolmgladwellbookgenerator.com")
return link_id

Khi viết Lua script cho Redis, mọi key được truy cập nên chỉ được truy cập bằng bảng KEYS. Bảng ARGV dùng cho truyền tham số — ở đây là giá trị URL chúng ta muốn lưu.

Logic Điều Kiện: increx Và hincrex

Ví dụ trên lưu link cho URL shortener của chúng ta, nhưng chúng ta cũng cần theo dõi số lần một URL được truy cập. Để làm vậy, chúng ta sẽ giữ một counter trong một hash trong Redis. Khi một user đến với một link identifier, chúng ta sẽ kiểm tra xem nó có tồn tại không, và tăng counter cho nó nếu có:

if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then
  return redis.call("HINCRBY", KEYS[1], ARGV[1], 1)
else
  return nil
end

Mỗi khi ai đó click vào shortlink, chúng ta chạy script này để theo dõi link đó được chia sẻ lại. Chúng ta gọi script dùng EVAL và truyền links:visits cho key đơn của chúng ta và link identifier trả về từ script trước đó làm đối số đơn.

Script sẽ trông gần như giống nhau mà không có hash. Đây là script tăng một key Redis chuẩn chỉ khi nó tồn tại:

if redis.call("EXISTS",KEYS[1]) == 1 then
  return redis.call("INCR",KEYS[1])
else
  return nil
end

SCRIPT LOAD Và EVALSHA

Hãy nhớ rằng khi Redis đang chạy một Lua script, nó sẽ không chạy bất cứ thứ gì khác. Những script tốt nhất đơn giản mở rộng từ vựng Redis hiện có của các thao tác dữ liệu atomic nhỏ với ít logic nhất cần thiết. Bug trong Lua script có thể khóa hoàn toàn Redis server — tốt nhất giữ cho mọi thứ ngắn gọn và dễ debug.

Dù chúng thường khá ngắn, chúng ta không cần chỉ định toàn bộ Lua script mỗi lần muốn chạy một script. Trong một ứng dụng thực, bạn sẽ đăng ký (register) từng Lua script của bạn với Redis khi ứng dụng boot (hoặc khi deploy), sau đó gọi các script sau đó bằng định danh SHA-1 duy nhất của chúng.

redis-cli SCRIPT LOAD "return 'hello world'"
# "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis-cli EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
# "hello world"

Một lời gọi tường minh đến SCRIPT LOAD thường không cần thiết trong ứng dụng chạy vì EVAL ngầm load script được truyền cho nó. Một ứng dụng có thể cố gắng EVALSHA một cách lạc quan và fallback về EVAL chỉ khi script không được tìm thấy.

Nếu bạn là lập trình viên Ruby, hãy xem Wolverine của Shopify, đơn giản hóa việc load và lưu trữ Lua script cho Ruby app. Đối với lập trình viên PHP, Predis hỗ trợ thêm Lua script để gọi như thể chúng là lệnh Redis bình thường. Nếu bạn dùng các công cụ này hoặc công cụ khác để chuẩn hóa tương tác với Lua, hãy cho tôi biết — tôi quan tâm để biết những gì khác đang có ngoài đó.

Khi Nào Nên Dùng Lua?

Hỗ trợ Lua của Redis chồng chéo một chút với block WATCH/MULTI/EXEC, nhóm các thao tác để chúng được thực thi cùng nhau. Vậy làm sao chọn dùng cái này hơn cái kia? Mỗi thao tác trong block MULTI cần độc lập, nhưng với Lua, các thao tác sau có thể phụ thuộc vào kết quả của thao tác trước. Việc dùng Lua script cũng có thể tránh race condition có thể làm đói slow client khi WATCH được dùng.

Từ những gì chúng ta thấy ở RedisGreen, hầu hết app dùng Lua cũng sẽ dùng MULTI/EXEC, nhưng không ngược lại. Hầu hết Lua script thành công đều nhỏ, và chỉ triển khai một tính năng đơn lẻ mà app của bạn cần nhưng không phải là một phần của từ vựng Redis.

Thăm Quán Thư Viện

Redis Lua interpreter load bảy thư viện: base, table, string, math, debug, cjson, và cmsgpack. Các thư viện đầu tiên là thư viện chuẩn cho phép bạn làm các thao tác cơ bản bạn mong đợi từ bất kỳ ngôn ngữ nào. Hai thư viện cuối cho Redis hiểu JSON và MessagePack — đây là tính năng cực kỳ hữu ích, và tôi luôn thắc mắc tại sao tôi không thấy nó được dùng nhiều hơn.

Web app với public API có xu hướng có JSON rải rác khắp nơi. Vậy có thể bạn có một đống JSON blob lưu trong key Redis bình thường và bạn muốn truy cập một số giá trị cụ thể bên trong chúng, như thể bạn đã lưu chúng như hash. Với hỗ trợ JSON của Redis, điều đó dễ dàng:

if redis.call("EXISTS", KEYS[1]) == 1 then
  local payload = redis.call("GET", KEYS[1])
  return cjson.decode(payload)[ARGV[1]]
else
  return nil
end

Ở đây chúng ta kiểm tra xem key có tồn tại không và nhanh chóng trả về nil nếu không. Sau đó lấy giá trị JSON ra khỏi Redis, parse nó với cjson.decode(), và trả về giá trị được yêu cầu.

redis-cli set apple '{ "color": "red", "type": "fruit" }'
# OK
redis-cli --eval json-get.lua apple , type
# "fruit"

Load script này vào Redis server của bạn cho phép bạn đối xử với giá trị JSON lưu trong Redis như thể chúng là hash. Nếu object của bạn đủ nhỏ, điều này thực sự khá nhanh, ngay cả khi chúng ta phải parse giá trị mỗi lần truy cập.

Nếu bạn đang làm việc trên internal API cho một hệ thống đòi hỏi hiệu suất, bạn có khả năng chọn MessagePack thay vì JSON, vì nó nhỏ hơn và nhanh hơn. May mắn thay với Redis (như ở hầu hết nơi), MessagePack gần như là drop-in replacement cho JSON:

if redis.call("EXISTS", KEYS[1]) == 1 then
  local payload = redis.call("GET", KEYS[1])
  return cmsgpack.unpack(payload)[ARGV[1]]
else
  return nil
end

Xử Lý Số (Crunching Numbers)

Lua và Redis có hệ thống kiểu (type system) khác nhau, vì vậy quan trọng để hiểu giá trị có thể thay đổi như thế nào khi vượt qua ranh giới Redis-Lua. Khi một số từ Lua quay lại Redis client, nó trở thành integer — bất kỳ chữ số nào sau dấu thập phân đều bị loại bỏ:

local indiana_pi = 3.2
return indiana_pi

Khi bạn chạy script này, Redis sẽ trả về integer 3 — bạn mất các phần thú vị của pi. Có vẻ đơn giản, nhưng mọi thứ trở nên rắc rối hơn khi bạn bắt đầu tương tác với Redis ở giữa script. Một ví dụ:

local indiana_pi = 3.2
redis.call("SET", "pi", indiana_pi)
return redis.call("GET", "pi")

Giá trị kết quả ở đây là một string: "3.2" Tại sao? Redis không có kiểu numeric chuyên dụng. Khi chúng ta đầu tiên SET giá trị, Redis lưu nó như một string, mất mọi ghi nhận về việc Lua ban đầu nghĩ giá trị là float. Khi chúng ta lấy giá trị ra sau đó, nó vẫn là string.

Giá trị trong Redis được truy cập với GET/SET nên được coi là string trừ khi các phép toán numeric như INCR và DECR được chạy trên chúng. Các phép toán numeric đặc biệt này sẽ thực sự trả về integer reply (và thao tác giá trị lưu trữ theo quy tắc toán học), nhưng "kiểu" của giá trị lưu trong Redis vẫn là một string value.

Gotchas: Tóm Tắt

Đây là những lỗi phổ biến nhất chúng ta thấy khi làm việc với Lua trong Redis:

  • Tables là one-based trong Lua, khác với hầu hết ngôn ngữ phổ biến. Phần tử đầu tiên trong bảng KEYS là KEYS[1], thứ hai là KEYS[2], v.v.

  • Giá trị nil kết thúc một table trong Lua. Vì vậy [ 1, 2, nil, 3 ] sẽ tự động trở thành [1, 2]. Đừng dùng giá trị nil trong table.

  • redis.call sẽ ném exception-style Lua error, trong khi redis.pcall sẽ tự động bẫy bất kỳ lỗi nào và trả về chúng như table có thể kiểm tra.

  • Số Lua được chuyển thành integer khi gửi đến Redis — mọi thứ sau dấu thập phân bị mất. Chuyển bất kỳ số floating point nào thành string trước khi trả về.

  • Hãy chắc chắn chỉ định tất cả key bạn dùng trong Lua script của bạn trong bảng KEYS, nếu không script của bạn có thể bị lỗi trong các phiên bản Redis tương lai.

  • Lua script giống như bất kỳ thao tác nào khác trong Redis: không có gì khác chạy khi chúng đang được thực thi. Hãy nghĩ về script như một cách mở rộng từ vựng của Redis server — giữ chúng ngắn gọn và đi thẳng vào vấn đề.

Đọc Thêm

Có rất nhiều tài nguyên tuyệt vời cho Lua và Redis trực tuyến — đây là một vài cái tôi dùng:

  • EVAL Docs
  • RedisGreen's Lua Script Library
  • Lua Reference Manual
  • Lua Tutorial Directory