用Lua定製Redis命令

2021-09-14 02:37:54 字數 3951 閱讀 1136

原文:

猿人課堂 2019-03-26

lua 指令碼例項

一些思考

redis作為乙個非常成功的資料庫,提供了非常豐富的資料型別和命令,使用這些,我們可以輕易而高效地完成很多快取操作,可是總有一些比較特殊問題或需求需要解決,這時候可能就需要我們自己定製自己的 redis 資料結構和命令。

「執行緒安全」問題

我們都知道 redis 是單執行緒的,可是它怎麼會有 執行緒安全 問題呢?

我們正常理解的執行緒安全問題是指單程序多執行緒模型內部多個執行緒操作程序內共享記憶體導致的資料資源充突。而 redis 的執行緒安全問題的產生,並不是來自於 redis 伺服器內部。

redis 作為資料伺服器,就相當於多個客戶端的共享記憶體,多個客戶端就相當於同一程序下的多個執行緒,如果多個客戶端之間沒有良好的資料同步策略,就會產生類似執行緒安全的問題。

典型場景是:

導致這個問題的原因就是雖然 redis 是單執行緒的,能保證命令的序列化,但由於其執行效率很高,多個客戶端的命令之間不做好請求同步,同樣會造成命令的順序錯亂。

當然這個問題也很好解決,給使用者狀態加鎖就行了,使同一時間內只能有乙個客戶端操作使用者狀態。不過加鎖我們就需要考慮鎖粒度、死鎖等問題了,無疑新增了程式的複雜性,不利於維護。

redis 作為乙個極其高效的記憶體資料伺服器,其命令執行速度極快,之前看過阿里雲 redis 的乙個壓測結果,執行效率可以達到 10w寫qps, 60w讀qps,那麼,它的效率問題又來自何處呢?

答案是網路,做 web 的都知道,效率優化要從網路做起,服務端又是優化**,又是優化資料庫,不如網路連線的一次優化,而網路優化最有效的就是減少請求數。我們要知道執行一次記憶體訪問的耗時約是 100ns,而不同機房之間來回一次約需要 500000ns,其中的差距可想而知。

redis在單機內效率超高,但工業化部署總不會把伺服器和 redis 放在同一臺機器上,如果觸碰到效率瓶頸的話,那就是網路。

典型場景就是我們從 redis 裡讀出一條資料,再使用這條資料做鍵,讀取另外一條資料。這樣來來回回,便有兩次網路往返。

導致這種問題的原因就是 redis 的普通命令沒有服務端計算的能力,無法在伺服器進行復合命令操作,雖然有 redis 也提供了 pipeline 的特性,但它需要多個命令的請求和響應之間沒有依賴關係。想簡化多個相互依賴的命令就只能將資料拉回客戶端,由客戶端處理後再請求 redis。

綜上,我們要更高效更方便的使用 redis 就需要自己「定製」一些命令了。

萬幸 redis 內嵌了 lua 執行環境,支援 lua 指令碼的執行,通過執行 lua 指令碼,我們可以把多個命令復合為乙個 lua 指令碼,通過 lua 指令碼來實現上文中提到的 redis 命令的次序性和 redis 服務端計算。

lualua 是乙個簡潔、輕量、可擴充套件的指令碼語言,它的特性有:

輕量:原始碼包只有核心庫,編譯後體積很小。

高效:由 ansi c 寫的,啟動快、執行快。

內嵌:可內嵌到各種程式語言或系統中執行,提公升靜態語言的靈活性。如 openresty 就是將 lua 嵌入到 nginx 中執行。

而且完全不需要擔心語法問題,lua 的語法很簡單,分分鐘使用不成問題。

redis 在 2.6 版本後,啟動時會建立 lua 環境、載入 lua 庫、定義 redis 全域性**、儲存 redis.pcall 等 redis 命令,以準備 lua 指令碼的執行。

乙個典型的 lua 指令碼執行步驟如下:

檢查指令碼是否執行過,沒執行過使用指令碼的 sha1 校驗和生成乙個 lua 函式;

為函式繫結超時、錯誤處理勾子;

建立乙個偽客戶端,通過這個偽客戶端執行 lua 中的 redis 命令;

處理偽客戶端的返回值,最終返回給客戶端;

雖然 lua 指令碼使用的是偽客戶端,但 redis 處理它會跟普通客戶端一樣,也會將執行的 redis 命令進行 rdb aof 主從複製等操作。

lua 指令碼的使用可以通過 redis 的evalevalsha命令。

eval 適用於單次執行 lua 指令碼,執行指令碼前會由指令碼內容生成 sha1 校驗和,在函式表內查詢函式是否已定義,如未定義執行成功後 redis 會在全域性表裡快取這個指令碼的校驗和為函式名,後續再次執行此命令就不會再建立新的函式了。

而要使用 ==evalsha ==命令,就得先使用 ==script load ==命令先將函式載入到 redis,redis 會返回此函式的 sha1 校驗和, 後續就可以直接使用這個校驗和來執行命令了。

以下是使用上述命令的例子:

127.0.0.1:6379> eval "return 'hello'" 0 0

"hello"

127.0.0.1:6379> script load "return redis.pcall('get', ar**[1])"

"20b602dcc1bb4ba8fca6b74ab364c05c58161a0a"

127.0.0.1:6379> evalsha 20b602dcc1bb4ba8fca6b74ab364c05c58161a0a 0 test

"zbs"

eval 命令的原型是eval script numkeys key [key ...] arg [arg ...],在 lua 函式內部可以使用keys[n]ar**[n]引用鍵和引數,需要注意 keys 和 ar** 的引數序號都是從1開始的。

還需要注意在 lua 指令碼中,redis 返回為空時,結果是false,而 不是null

下面寫幾個 lua 指令碼的例項,用來介紹語法的,僅供參考。

// 使用: eval script 2 a b

local tmpkey = redis.call('hget', keys[1], keys[2]);

return redis.call('get', tmpkey);

// 使用: eval script 2 list count

local list = {};

local item = false;

local num = tonumber(keys[2]);

while (num > 0)

do item = redis.call('lpop', keys[1]);

if item == false then

break;

end;

table.insert(list, item);

num = num - 1;

end;

return list;

local elements = redis.call('zrank', keys[1], 0, key[2]);

local detail = {};

for index,ele in elements do

local info = redis.call('hgetall', ele);

table.insert(detail, info);

end;

return detail;

基本使用語法就是如此,更多應用就看各個具體場景了。

實現之外,還要一些東西要思考:

首先來總結一下 redis 中 lua 的使用場景:

可以使用 lua 指令碼實現原子性操作,避免不同客戶端訪問 redis 伺服器造成的資料衝突。

在前後多次請求的結果有依賴時,可以使用 lua 指令碼把多個請求集成為乙個請求。

使用 lua 指令碼,我們還需要注意:

c 中用lua指令碼執行redis命令

直接貼出 實現執行lua指令碼的方法,用到的第三方類庫是 stackexchange.redis nuget上有 注 下面的 是簡化後的,實際使用要修改,using system using system.collections.generic using system.linq using sys...

c 中用lua指令碼執行redis命令

直接貼出 實現執行lua指令碼的方法,用到的第三方類庫是 stackexchange.redis nuget上有 注 下面的 是簡化後的,實際使用要修改,csharp view plain copy using system using system.collections.generic usin...

用Dockerfile定製映象

從剛才的 docker commit 的學習中,我們可以了解到,映象的定製實際上就是定製每一層所新增的配置 檔案。如果我們可以把每一層修改 安裝 構建 操作的命令都寫入乙個指令碼,用這個指令碼來構建 定製映象,那麼之前提及的無法重複的問題 映象構建透明性的問題 體積的問題就都會解決。這個指令碼就是 ...