原文:
猿人課堂 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 的eval
和evalsha
命令。
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 的學習中,我們可以了解到,映象的定製實際上就是定製每一層所新增的配置 檔案。如果我們可以把每一層修改 安裝 構建 操作的命令都寫入乙個指令碼,用這個指令碼來構建 定製映象,那麼之前提及的無法重複的問題 映象構建透明性的問題 體積的問題就都會解決。這個指令碼就是 ...