日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

【Redis】Redis中使用Lua脚本

發布時間:2023/12/9 数据库 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【Redis】Redis中使用Lua脚本 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Lua是一種輕量小巧的腳本語言,用標準C語言編寫并以源代碼形式開放,其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。

Lua具體語法參考:https://www.runoob.com/lua/lua-tutorial.html

腳本的原子性

Redis使用單個Lua解釋器去運行所有腳本,并且Redis也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其他腳本或 Redis命令被執行。這和使用MULTI/EXEC包圍的事務很類似。

在其他別的客戶端看來,腳本的效果要么是不可見的,要么就是已完成的。

另一方面,這也意味著,執行一個運行緩慢的腳本并不是一個好主意。寫一個跑得很快很順溜的腳本并不難,因為腳本的運行開銷非常少,但是當你不得不使用一些跑得比較慢的腳本時,請小心,因為當這些蝸牛腳本在慢吞吞地運行的時候,其他客戶端會因為服務器正忙而無法執行命令。

eval命令的使用

eval和evalsha命令是從Redis2.6.0版本開始引入的,使用內置的Lua解釋器,可以對Lua腳本進行求值。

eval命令的說明:

> help evalEVAL script numkeys key [key ...] arg [arg ...]summary: Execute a Lua script server sidesince: 2.6.0group: scripting

參數說明:

  • script:一段Lua腳本程序,這段Lua腳本不需要也不應該定義函數,它運行在Redis服務器中。
  • numkeys:鍵名參數的個數。
  • key[]: 鍵名參數,表示在腳本中所用到的那些Redis鍵(key),這些鍵名參數可以在Lua中通過全局變量KEYS數組,用1為基址的形式訪問(KEYS[1]、KEYS[2],以此類推)。
  • arg[]:不是鍵名參數的附加參數,可以在Lua中通過全局變量ARGV數組訪問,訪問的形式和KEYS變量類似(ARGV[1]、ARGV[2],諸如此類)。

舉例說明:

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 a b c d 1) "a" 2) "b" 3) "c" 4) "d"

返回結果是Redis multi bulk replies的Lua數組,這是一個Redis的返回類型,其他客戶端庫(如JAVA客戶端)可能會將他們轉換成數組類型。

Lua中執行redis命令

在Lua中,可以通過內置的函數redis.call()和redis.pcall()來執行redis命令。

redis.call()和redis.pcall()兩個函數的參數可以是任意的Redis命令:

> eval "return redis.call('set','foo','bar')" 0 OK

需要注意的是,上面這段腳本的確實現了將鍵foo的值設為bar的目的,但是,它違反了EVAL命令的語義,因為腳本里使用的所有鍵都應該由KEYS數組來傳遞,就像這樣:

> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK

要求使用正確的形式來傳遞鍵(key)是有原因的,因為不僅僅是EVAL這個命令,所有的Redis命令,在執行之前都會被分析,借此來確定命令會對哪些鍵進行操作。

因此,對于EVAL命令來說,必須使用正確的形式來傳遞鍵,才能確保分析工作正確地執行。除此之外,使用正確的形式來傳遞鍵還有很多其他好處,它的一個特別重要的用途就是確保Redis集群可以將你的請求發送到正確的集群節點。

redis.call()與redis.pcall()很類似,他們唯一的區別是當redis命令執行結果返回錯誤時,redis.call()將返回給調用者一個錯誤,而redis.pcall()會將捕獲的錯誤以Lua表的形式返回。

下面的例子演示了redis.call()與redis.pcall()的區別:

> eval "return redis.call('set1',KEYS[1],'bar')" 1 foo (error) ERR Error running script (call to f_d968406ee98123006fa91fd2ee764d4f7f859dd7): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script> eval "return redis.pcall('set1',KEYS[1],'bar')" 1 foo (error) @user_script: 1: Unknown Redis command called from Lua script> eval "return type(redis.call('set1',KEYS[1],'bar'))" 1 foo (error) ERR Error running script (call to f_c62b83c8313fd8f2557865e37d2bb5133f1789af): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script> eval "return type(redis.pcall('set1',KEYS[1],'bar'))" 1 foo "table"

Lua數據類型和Redis數據類型之間轉換

當Lua通過call()或pcall()函數執行Redis命令的時候,命令的返回值會被轉換成Lua數據結構。

同樣地,當Lua腳本在Redis內置的解釋器里運行時,Lua腳本的返回值也會被轉換成Redis協議(protocol),然后由EVAL將值返回給客戶端。

數據類型之間的轉換遵循這樣一個設計原則:如果將一個Redis值轉換成Lua值,之后再將轉換所得的Lua值轉換回Redis值,那么這個轉換所得的Redis 值應該和最初時的Redis值一樣。

換句話說,Lua類型和Redis類型之間存在著一一對應的轉換關系。

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

從Lua轉換到Redis有一條額外的規則,這條規則沒有和它對應的從Redis轉換到Lua的規則:

  • Lua boolean true -> Redis integer reply with value of 1. / Lua 布爾值 true 轉換成 Redis 整數回復中的 1

Lua中整數和浮點數之間沒有什么區別。因此,我們始終將Lua的數字轉換成整數的回復,這樣將舍去小數部分。如果你想從Lua返回一個浮點數,你應該將它作為一個字符串,比如ZSCORE命令。

以下是幾個類型轉換的例子:

> eval "return 10" 0 (integer) 10> eval "return {1,2,{3,'Hello World!'}}" 0 1) (integer) 1 2) (integer) 2 3) 1) (integer) 32) "Hello World!"> eval "return redis.call('get','foo')" 0 "bar"

最后一個例子展示如果是Lua直接命令調用它是如何可以從redis.call()或redis.pcall()接收到準確的返回值。

下面的例子我們可以看到浮點數和nil將怎么樣處理:

> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) "foo"

正如你看到的3.333被轉換成了3,并且nil后面的字符串bar沒有被返回回來。

可以使用tostring()函數將數字轉字符串:

> eval "return tostring(3.3333)" 0 "3.3333"

有兩個輔助函數從Lua返回Redis的類型:

  • redis.error_reply(error_string):returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
  • redis.status_reply(status_string):returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.

使用redis.error_reply()函數與直接返回一個table效果一樣:

> eval "return {err='My Error'}" 0 (error) My Error> eval "return redis.error_reply('My Error')" 0 (error) My Error

EVALSHA

EVAL命令要求你在每次執行腳本的時候都發送一次腳本主體(script body)。Redis有一個內部的緩存機制,因此它不會每次都重新編譯腳本,不過在很多場合,付出無謂的帶寬來傳送腳本主體并不是最佳選擇。

為了減少帶寬的消耗,Redis實現了EVALSHA命令,它的作用和EVAL一樣,都用于對腳本求值,但它接受的第一個參數不是腳本,而是腳本的SHA1校驗和(sum)。

如果服務器還記得給定的SHA1校驗和所指定的腳本,那么執行這個腳本,如果服務器不記得給定的SHA1校驗和所指定的腳本,那么它返回一個特殊的錯誤,提醒用戶使用EVAL代替EVALSHA。

以下是示例:

> set foo bar OK> eval "return redis.call('get','foo')" 0 "bar"> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 "bar"> evalsha ffffffffffffffffffffffffffffffffffffffff 0 (error) NOSCRIPT No matching script. Please use EVAL.

客戶端庫的底層實現可以一直樂觀地使用EVALSHA來代替EVAL,并期望著要使用的腳本已經保存在服務器上了,只有當NOSCRIPT錯誤發生時,才使用 EVAL命令重新發送腳本,這樣就可以最大限度地節省帶寬。

這也說明了執行EVAL命令時,使用正確的格式來傳遞鍵名參數和附加參數的重要性:因為如果將參數硬寫在腳本中,那么每次當參數改變的時候,都要重新發送腳本,即使腳本的主體并沒有改變,相反,通過使用正確的格式來傳遞鍵名參數和附加參數,就可以在腳本主體不變的情況下,直接使用EVALSHA 命令對腳本進行復用,免去了無謂的帶寬消耗。

腳本緩存

Redis保證所有被運行過的腳本都會被永久保存在腳本緩存當中,這意味著,當EVAL命令在一個Redis實例上成功執行某個腳本之后,隨后針對這個腳本的所有EVALSHA命令都會成功執行。

刷新腳本緩存的唯一辦法是顯式地調用SCRIPT FLUSH命令,這個命令會清空運行過的所有腳本的緩存。通常只有在云計算環境中,Redis實例被改作其他客戶或者別的應用程序的實例時,才會執行這個命令。

緩存可以長時間儲存而不產生內存問題的原因是,它們的體積非常小,而且數量也非常少,即使腳本在概念上類似于實現一個新命令,即使在一個大規模的程序里有成百上千的腳本,即使這些腳本會經常修改,即便如此,儲存這些腳本的內存仍然是微不足道的。

事實上,用戶會發現Redis不移除緩存中的腳本實際上是一個好主意。比如說,對于一個和Redis保持持久化鏈接(persistent connection)的程序來說,它可以確信,執行過一次的腳本會一直保留在內存當中,因此它可以在流水線中使用EVALSHA命令而不必擔心因為找不到所需的腳本而產生錯誤。

Redis提供了以下幾個SCRIPT命令,用于對腳本子系統(scripting subsystem)進行控制:

  • SCRIPT FLUSH:清除所有腳本緩存
  • SCRIPT EXISTS:根據給定的腳本校驗和,檢查指定的腳本是否存在于腳本緩存
  • SCRIPT LOAD:將一個腳本裝入腳本緩存,但并不立即運行它
  • SCRIPT KILL:殺死當前正在運行的腳本

可用庫

Redis Lua解釋器可用加載以下Lua庫:

  • base lib.
  • table lib.
  • string lib.
  • math lib.
  • debug lib.
  • struct lib.
  • cjson lib.
  • cmsgpack lib.
  • bitop lib.
  • redis.sha1hex function.

每一個Redis實例都擁有以上的所有類庫,以確保您使用腳本的環境都是一樣的。

struct,CJSON和cmsgpack都是外部庫,所有其他庫都是標準Lua庫。

CJSON庫為Lua提供極快的JSON處理:

> eval 'return cjson.encode({["foo"]= "bar"})' 0 "{\"foo\":\"bar\"}"> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}" "bar"> eval "local table = {} table['foo']='bar' table['hello']='world' return cjson.encode(table)" 0 "{\"hello\":\"world\",\"foo\":\"bar\"}"

沙箱(sandbox)和最大執行時間

腳本應該僅僅用于傳遞參數和對Redis數據進行處理,它不應該嘗試去訪問外部系統(比如文件系統),或者執行任何系統調用。

除此之外,腳本還有一個最大執行時間限制,它的默認值是5秒鐘,一般正常運作的腳本通常可以在幾分之幾毫秒之內完成,花不了那么多時間,這個限制主要是為了防止因編程錯誤而造成的無限循環而設置的。

最大執行時間的長短由lua-time-limit選項來控制(以毫秒為單位),可以通過編輯redis.conf文件或者使用CONFIG GET和CONFIG SET命令來修改它。

當一個腳本達到最大執行時間的時候,它并不會自動被Redis結束,因為Redis必須保證腳本執行的原子性,而中途停止腳本的運行意味著可能會留下未處理完的數據在數據集(data set)里面。

因此,當腳本運行的時間超過最大執行時間后,以下動作會被執行:

  • Redis記錄一個腳本正在超時運行
  • Redis開始重新接受其他客戶端的命令請求,但是只有SCRIPT KILL和SHUTDOWN NOSAVE兩個命令會被處理,對于其他命令請求,Redis服務器只是簡單地返回BUSY錯誤。
  • 可以使用SCRIPT KILL命令將一個僅執行只讀命令的腳本殺死,因為只讀命令并不修改數據,因此殺死這個腳本并不破壞數據的完整性
  • 如果腳本已經執行過寫命令,那么唯一允許執行的操作就是SHUTDOWN NOSAVE,它通過停止服務器來阻止當前數據集寫入磁盤

流水線(pipeline)上下文(context)中的EVALSHA

在流水線請求的上下文中使用EVALSHA命令時,要特別小心,因為在流水線中,必須保證命令的執行順序。

一旦在流水線中因為EVALSHA命令而發生NOSCRIPT錯誤,那么這個流水線就再也沒有辦法重新執行了,否則的話,命令的執行順序就會被打亂。

為了防止出現以上所說的問題,客戶端庫實現應該實施以下的其中一項措施:

  • 總是在流水線中使用EVAL命令
  • 檢查流水線中要用到的所有命令,找到其中的EVAL命令,并使用SCRIPT EXISTS命令檢查要用到的腳本是不是全都已經保存在緩存里面了。如果所需的全部腳本都可以在緩存里找到,那么就可以放心地將所有EVAL命令改成EVALSHA命令,否則的話,就要在流水線的頂端(top)將缺少的腳本用SCRIPT LOAD 命令加上去。

總結

以上是生活随笔為你收集整理的【Redis】Redis中使用Lua脚本的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。