用Lua定製Redis命令

前言

Redis做爲一個很是成功的數據庫,提供了很是豐富的數據類型和命令,使用這些,咱們能夠輕易而高效地完成不少緩存操做,但是總有一些比較特殊的問題或需求須要解決,這時候可能就須要咱們本身定製本身的 Redis 數據結構和命令。css

文章歡迎轉載,請尊重做者勞動成果,帶上原文連接:http://www.cnblogs.com/zhenbianshu/p/8416162.htmlhtml


Redis命令問題

線程安全問題

咱們都知道 Redis 是單線程的,但是它怎麼會有 線程安全 問題呢?nginx

咱們正常理解的線程安全問題是指單進程多線程模型內部多個線程操做進程內共享內存致使的數據資源充突。而 Redis 的線程安全問題的產生,並非來自於 Redis 服務器內部。web

Redis 做爲數據服務器,就至關於多個客戶端的共享內存,多個客戶端就至關於同一進程下的多個線程,若是多個客戶端之間沒有良好的數據同步策略,就會產生相似線程安全的問題。redis

典型場景是:數據庫

  • Redis 內存儲了一個用戶的狀態: user5277=idle
  • 客戶端鏈接 A 讀取了用戶狀態,獲取到用戶的空閒狀態 status = get("user5277")
  • 客戶端鏈接 B 也一樣讀取了用戶狀態;
  • 客戶端鏈接 A 給用戶安排了一個任務,並將 Redis 內用戶狀態置爲忙碌 set("user5277", "busy")
  • 客戶端鏈接 B 一樣設置用戶爲忙碌狀態。
  • 但是此時用戶卻被同時分配了兩個任務。

致使這個問題的緣由就是雖然 Redis 是單線程的,能保證命令的序列化,但因爲其執行效率很高,多個客戶端的命令之間不作好請求同步,一樣會形成命令的順序錯亂。編程

固然這個問題也很好解決,給用戶狀態加鎖就好了,使同一時間內只能有一個客戶端操做用戶狀態。不過加鎖咱們就須要考慮鎖粒度、死鎖等問題了,無疑添加了程序的複雜性,不利於維護。緩存

效率問題

Redis 做爲一個極其高效的內存數據服務器,其命令執行速度極快,以前看過阿里雲 Redis 的一個壓測結果,執行效率能夠達到 10W寫QPS, 60W讀QPS,那麼,它的效率問題又來自何處呢?安全

答案是網絡,作 Web 的都知道,效率優化要從網絡作起,服務端又是優化代碼,又是優化數據庫,不如網絡鏈接的一次優化,而網絡優化最有效的就是減小請求數。咱們要知道執行一次內存訪問的耗時約是 100ns,而不一樣機房之間來回一次約須要 500000ns,其中的差距可想而知。服務器

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

典型場景就是咱們從 Redis 裏讀出一條數據,再使用這條數據作鍵,讀取另一條數據。這樣來來回回,便有兩次網絡往返。

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

綜上,咱們要更高效更方便的使用 Redis 就須要本身「定製」一些命令了。


內嵌Lua的執行

萬幸 Redis 內嵌了 Lua 執行環境,支持 Lua 腳本的執行,經過執行 Lua 腳本,咱們能夠把多個命令複合爲一個 Lua 腳本,經過 Lua 腳原本實現上文中提到的 Redis 命令的次序性和 Redis 服務端計算。

Lua

Lua 是一個簡潔、輕量、可擴展的腳本語言,它的特性有:

  • 輕量:源碼包只有核心庫,編譯後體積很小。
  • 高效:由 ANSI C 寫的,啓動快、運行快。
  • 內嵌:可內嵌到各類編程語言或系統中運行,提高靜態語言的靈活性。如 OpenResty 就是將 Lua 嵌入到 nginx 中執行。

並且徹底不須要擔憂語法問題,Lua 的語法很簡單,分分鐘使用不成問題。

執行步驟

Redis 在 2.6 版本後,啓動時會建立 Lua 環境、載入 Lua 庫、定義 Redis 全局表格、存儲 redis.pcall 等 Redis 命令,以準備 Lua 腳本的執行。

一個典型的 Lua 腳本執行步驟以下:

  1. 檢查腳本是否執行過,沒執行過使用腳本的 sha1 校驗和生成一個 Lua 函數;
  2. 爲函數綁定超時、錯誤處理勾子;
  3. 建立一個僞客戶端,經過這個僞客戶端執行 Lua 中的 Redis 命令;
  4. 處理僞客戶端的返回值,最終返回給客戶端;

交互時序如圖

雖然 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', ARGV[1])"
"20b602dcc1bb4ba8fca6b74ab364c05c58161a0a"

127.0.0.1:6379> EVALSHA 20b602dcc1bb4ba8fca6b74ab364c05c58161a0a 0 test
"zbs"

EVAL 命令的原型是 EVAL script numkeys key [key ...] arg [arg ...],在 Lua 函數內部可使用 KEYS[N]ARGV[N] 引用鍵和參數,須要注意 KEYS 和 ARGV 的參數序號都是從 1 開始的。

還須要注意在 Lua 腳本中,Redis 返回爲空時,結果是 false,而 不是 nil


Lua 腳本實例

下面寫幾個 Lua 腳本的實例,用來介紹語法的,僅供參考。

  • Redis 裏 hashSet A 的 字段 B 的值是 C,取出 Redis 裏鍵爲 C 的值。
// 使用: EVAL script 2 A B

local tmpKey = redis.call('HGET', KEYS[1], KEYS[2]); 
return redis.call('GET', tmpKey);
  • 一次 lpop 出多個值,直到值爲 n,或 list 爲空(pipeline 也可輕易實現);
// 使用: 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;
  • 獲取 zset 內 score 最多的 n 個元素 對應 hashset 中的詳細信息;
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 腳本,咱們還須要注意:

  • 要保證安全性,在 Lua 腳本中不要使用全局變量,以避免污染 Lua 環境,雖然使用全局變量全報錯,Lua 腳本中止執行,但仍是在定義變量時添加 local 關鍵字。
  • 要注意 Lua 腳本的時間複雜度,Redis 的單線程一樣會阻塞在 Lua 腳本的執行中。
  • 使用 Lua 腳本實現原子操做時,要注意若是 Lua 腳本報錯,以前的命令一樣沒法回滾。
  • 一次發出多個 Redis 請求,但請求先後無依賴時,使用 pipeline,比 Lua 腳本方便。

小結

最近工做有了較大的變更,從業務到技術棧都跟原來徹底不一樣了,全部代碼和業務都脫離了本身掌控的感受真的很不爽,工做中全是「開局一個搜索引擎,語法全靠查」,天天還要熬到很晚熟悉新的東西,有點小累,果真換工做就是找罪受啊。不過走出溫馨區後的充實感也在提醒本身正在不停進步,倒也挺有成就感的。

剛接觸新的東西沒什麼沉澱,又不想寫一些《帶你三天精通 Java》這種水文,工做之餘的時間都被拿去補充工做須要的技術棧了,也沒時間研究些本身以爲有意思的東西,寫文章須要素材啊,爲了避免自砸招牌,最近可能會少更。。

關於本文有什麼問題能夠在下面留言交流,若是您以爲本文對您有幫助,能夠點擊下面的 推薦 支持一下我,博客一直在更新,歡迎 關注

參考:

Redis 設計與實現 » Lua 腳本

Redis 與 Lua 腳本

Redis的Lua腳本編程的實現和應用

相關文章
相關標籤/搜索