Redis 實戰 —— 14. Redis 的 Lua 指令碼程式設計

滿賦諸機發表於2021-02-03

簡介

Redis 從 2.6 版本開始引入使用 Lua 程式語言進行的伺服器端指令碼程式設計功能,這個功能可以讓使用者直接在 Redis 內部執行各種操作,從而達到簡化程式碼並提高效能的作用。 P248

在不編寫 C 程式碼的情況下新增新功能 P248

通過使用 Lua 對 Redis 進行指令碼程式設計,我們可以避免一些減慢開發速度或者導致效能下降對常見陷阱。 P248

將 Lua 指令碼載入 Redis P249
  • SCRIPT LOAD 命令可以將指令碼載入 Redis ,這個命令接受一個字串格式的 Lua 指令碼為引數,它會把指令碼儲存起來等待之後使用,然後返回被儲存指令碼的 SHA1 校驗和
  • EVALSHA 命令可以呼叫之前儲存的指令碼,這個命令接收指令碼的 SHA1 校驗和以及指令碼所需的全部引數
  • EVAL 命令可以直接執行指定的指令碼,這個命令接收指令碼字串以及指令碼所需的全部引數。這個命令除了會執行指令碼之外,還會將被執行的指令碼快取到 Redis 伺服器裡面

由於 Lua 的資料傳入和傳出限制, Lua 與 Redis 需要進行相互轉換。因為指令碼在返回各種不同型別的資料時可能會產生含糊不清的結果,所以我們應該儘量顯式的返回字串。 P250

我們可以在 官方文件 中找到 Redis 和 Lua 不同型別之間的轉換表:

Redis 型別轉換至 Lua 型別

Redis Lua
integer reply number
bulk reply string
multi bulk reply table (may have other Redis data types nested)
status reply table with a single ok field containing the status
error reply table with a single err field containing the error
Nil bulk reply false boolean type
Nil multi bulk reply false boolean type

Lua 型別轉換至 Redis 型別

Lua Redis
number integer reply (the number is converted into an integer)
string bulk reply
table (array) multi bulk reply (truncated to the first nil inside the Lua array if any)
table with a single ok field status reply
table with a single err field error reply
boolean false Nil bulk reply
boolean true integer reply with value of 1
建立新的狀態訊息 P251
  • Lua 指令碼跟單個 Redis 命令以及 MULTI/EXEC 事務一樣,都是原子操作
  • 已經對結構進行了修改的 Lua 指令碼將無法被中斷
    • 不執行任何寫命令對只讀指令碼:可以在指令碼對執行時間超過 lua-time-limit 選項指定的時間之後,執行 SCRIPT KILL 命令殺死正在執行對指令碼
    • 有寫命令的指令碼:殺死指令碼將導致 Redis 儲存的資料進入一種不一致的狀態。在這種情況下

使用 Lua 重寫鎖和訊號量 P254

如果我們事先不知道哪些鍵會被讀取和寫入,那麼就應該使用 WATCH/MULTI/EXEC 事務或者鎖,而不是 Lua 指令碼。因此,在指令碼里面對未被記錄到 KEYS 引數中的鍵進行讀取或者寫入,可能會在程式遷移至 Redis 叢集的時候出現不相容或者故障。 P254

獲取鎖在目前已不需要使用 Lua 指令碼實現,可以直接使用 SET ,並用 PXNX 選項即可在鍵不存在的時候設定帶過期時間的值。釋放鎖時為了保證釋放的時自己獲取的鎖,需要使用 Lua 指令碼實現。相關程式碼已在 實現自動補全、分散式鎖和計數訊號量 中實現。

移除 WATCH/MULTI/EXEC 事務 P258

一般來說,如果只有少數幾個客戶端嘗試對被 WATCH 命令監視對資料進行修改,那麼事務通常可以在不發生明顯衝突或重試的情況下完成。但是,如果操作需要進行好幾次通訊往返,或者操作發生衝突的概率較高,又或者網路延遲較大,那麼客戶端可能需要重試很多次才能完成操作。 P258

使用 Lua 指令碼替代事務不僅可以保證客戶端嘗試的執行都可以成功,還能降低通訊開銷,大幅提高 TPS 。同時由於 Lua 指令碼沒有多次通訊往返,所以執行速度也會明顯快於細粒度鎖的版本。

Lua 指令碼可以提供巨大的效能優勢,並且能在一些情況下大幅地簡化程式碼,但執行在 Redis 內部但 Lua 指令碼只能訪問位於 Lua 指令碼之內或者 Redis 資料庫之內的資料,而鎖或者 WATCH/MULTI/EXEC 事務並沒有這一限制。 P263

使用 Lua 對列表進行分片 P263

分片列表的構成 P263

為了能夠對分片列表的兩端執行推入操作和彈出操作,在構建分片列表時除了需要儲存組成列表的各個分片之外,還需要記錄列表第一個分片的 ID 以及最後一個分片的 ID 。當分片列表為空時,這兩個字串儲存的分片 ID 將是相同的。 P263

組成分片列表的每個分片都會被命名為 <listname>:<shardid> ,並按照順序進行分配。具體來說,如果程式總是從左端彈出元素,並從右端推入元素,那麼最後一個分配的索引就會逐漸增大,並且新分片的 ID 也會變得越來越大。如果程式總是從右端彈出元素,並從左端推入元素,那麼第一個分片的索引就會逐漸減少,並且新分片的 ID 也會變得越來越小。 P264

當分片列表包含多個列表時,位於分片兩端的列表可能是被填滿的,但位於兩端之間的其他列表總是被填滿的。 P264

將元素推入分片列表 P265

Lua 指令碼根據命令 LPUSH/RPUSH 找到列表的第一個分片或者最後一個分片,然後將元素推入分片對應的列表中,若分片已達個數上限(可以取配置中的 list-max-ziplist-entries 的值 - 1 作為上限),則會自動產生一個新的分片,繼續推入,並更新第一個分片或者最後一個分片的分片 ID 。當推入操作執行完畢後,它會返回被推入元素的數量。 P265

從分片裡面彈出元素 P266

Lua 指令碼根據命令 LPOP/RPOP 找到列表的第一個分片或者最後一個分片,然後在分片非空的情況下,從分片裡面彈出一個元素,如果列表在執行彈出操作之後不再包含任何元素,那麼程式就對記錄著列表兩端分片資訊的字串鍵進行修改(注意只有列表端分片為空時才修改對應的字串鍵,而整個列表為空時,不做調整) P267

對分片列表執行阻塞彈出操作 P267

這一段書上講得看不懂,也不知道為什麼需要書中的花式操作才能完成。

個人覺得分片列表的阻塞彈出其實並不需要列表自身的阻塞彈出,我們可以不斷執行上述 Lua 指令碼實現的彈出元素的操作,若彈出成功,則直接返回,若彈出失敗,則睡 1 ms 後繼續執行彈出操作,直至彈出成功或者達到超時時間。這樣我們對 Redis 對操作只在 Lua 指令碼中,原子性保證了一定會彈出分片列表兩端的元素。

本文首發於公眾號:滿賦諸機(點選檢視原文) 開源在 GitHub :reading-notes/redis-in-action

相關文章